題目描述
在一個尚未發現的宇宙中,有一個行星中的一個國家,只有數學家居住。在這個國家中,總共有N個數學家,有趣的是,每個數學家都住在他們自己的城市裏,更有趣的是,沒有兩個城市的道路是相連的,因爲數學家之間可以通過網絡在線交流和審查學術論文。當然,城市也會從1到N進行標識。
在一位數學家決定用智能手機寫一篇學術論文之前,生活都是完美的。此時,智能手機將“self-evident”自動翻譯爲了“Pictionary”並就此發表,不久,整個國家就發現了圖片這個詞並開始想相互見面和玩耍,因此,他們開始了城市間的道路建設工作。
根據時間表,這個工作將一共持續M天,第一天,在以M爲最大公約數的城市之間進行施工,第二天在以M-1爲最大公約數的城市之間進行施工,以此類推,直到第M天,將在所有對的城市之間進行施工。即,如果GCD(a,b)=m-i+1,那麼在第i天,城市A和B之間將進行道路施工。
由於數學家們忙於建築工作,他們請你幫忙確定給定的一對數學家之間第幾天可以見面。
輸入格式
第一行輸入三個正整數N,M,Q(1≤N,Q≤100000,1≤M≤N)。
接下來輸入Q行,每行兩個數字a和b,表示這兩個城市的數學家想一起玩耍。
輸出格式
對於第i行的兩個數學家,輸出他們最早能見面的天數。
樣例輸入輸出
Sample Input 1
8 3 3
2 5
3 6
4 8
Sample Output 1
3
1
2
Sample Input 2
25 6 1
20 9
Sample Output 2
4
Sample Input 3
9999 2222 2
1025 2405
3154 8949
Sample Output 3
1980
2160
題解
看了一下洛谷上的題解,都是大佬,只能弱弱地膜拜
本蒟蒻只能用非常弱智的方法,靠強大的毅力,碼出這道題。。。
乍一看這題目像是圖論中夾雜數論,貌似很難的樣子 (本來也很難 )。實際上是在求兩個問題:①何時加邊;②何時連通
加邊操作其實挺容易的,就是找出所有gcd(a, b) = i的數對,兩兩之間連一條邊。但是不用這麼複雜。如果用上並查集維護的話,就不需要找出所有。設想一下,如果i與k * i之間有連邊,那麼所有i的倍數都會處在同一集合裏了,這樣一來,加邊操作就容易多了,一個兩重循環就可以搞定了。
詳細參見如下代碼:
for (int i = m; i; -- i)
for (int j = i << 1; j <= n; j += i)
if (findSet (i) != findSet (j))
unionSet (i, j, m - i + 1); //i 與 j 是在 m - i + 1 時連通
加完邊,合併完集合後,就可以開始上圖論的部分了。
仔細一想,題目其實就是在讓你求兩個點之間,最長路徑最短的一條路徑,這時候我想到了bfs,於是就有了如下代碼:
#include <cstdio>
#include <iostream>
#include <vector>
#include <queue>
#include <set>
using namespace std;
#define INf 0x7f7f7f7f
const int N = 100000;
int n, m, query;
int fa[N + 5];
vector < pair < int, int > > G[N + 5];
struct cmp {
bool operator () (const P p1, const P p2) const {
return p1.second > p2.second;
}
};
void makeSet () {
for (int i = 1; i <= n; ++ i)
fa[i] = i;
}
int findSet (const int x) {
if (fa[x] != x)
fa[x] = findSet (fa[x]);
return fa[x];
}
void unionSet (const int x, const int y, const int val) {
int u = findSet (x), v = findSet (y);
fa[u] = v;
G[u].push_back( make_pair (v, val) );
G[v].push_back( make_pair (u, val) );
}
int bfs (const int s, const int e) {
priority_queue < P, vector < P >, cmp > q;
q.push( make_pair (s, 0) );
int dis[N + 5];
for (int i = 1; i <= n; ++ i)
dis[i] = INf;
dis[s] = 0;
while (! q.empty()) {
int u = q.top().first, distance = q.top().second;
q.pop();
if (u == e)
return distance;
for (int i = 0; i < G[u].size(); ++ i) {
int v = G[u][i].first, distance_ = max ( G[u][i].second, distance );
if (dis[v] > distance_) {
dis[v] = distance_;
q.push( make_pair (v, distance_) );
}
}
}
}
int main () {
//freopen ("pictionary.in", "r", stdin);
//freopen ("pictionary.out", "w", stdout);
scanf ("%d %d %d", &n, &m, &query);
makeSet ();
for (int i = m; i; -- i)
for (int j = i << 1; j <= n; j += i)
if (findSet (i) != findSet (j))
unionSet (i, j, m - i + 1);
for (int i = 1; i <= query; ++ i) {
int x, y;
scanf ("%d %d", &x, &y);
int dis = bfs (x, y);
printf ("%d\n", dis);
}
return 0;
}
恭喜你,16分到手了(洛谷數據)
既然不能用暴搜,那麼要怎麼做呢??
受並查集的影響,聯想到了樹。基於樹的結構和並查集的優化,我們可以只加較短的邊,並且,只有父親節點加邊。這樣,我們就可以構造出一棵樹了。
值得注意的是,樹根不可以隨便亂選。如果只加了單向邊,那麼就無法建成一顆完整的樹;如果是加了雙向變,那麼可能會導致答案路徑上的邊比原有的長,算出錯誤的答案。
於是,我開始在並查集中動手腳進行更改,詳細參見以下代碼:
void makeSet () {
for (int i = 1; i <= n; ++ i)
fa[i] = i;
}
int findSet (const int x) {
if (fa[x] != x)
fa[x] = findSet (fa[x]);
return fa[x];
}
void unionSet (const int x, const int y, const int val) {
int u = findSet (x), v = findSet (y);
if (u == v)
return ;
if (rnk[u] > rnk[v]) {
fa[v] = u;
G[u].push_back( make_pair (v, val) );
root[v] = true;
}
else {
fa[u] = v;
G[v].push_back( make_pair (u, val) );
root[u] = true;
if (rnk[u] == rnk[v])
++ rnk[v];
}
}
然後,我們再把樹建起來,再找到兩個點到lca的路徑上的最長路徑就好了。
參考代碼
#include <cstdio>
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
#define INf 0x7f7f7f7f
const int N = 100000;
int n, m, query;
int fa[N + 5], rnk[N + 5], dis[N + 5], dep[N + 5];
bool root[N + 5];
vector < pair < int, int > > G[N + 5];
void makeSet () {
for (int i = 1; i <= n; ++ i)
fa[i] = i;
}
int findSet (const int x) {
if (fa[x] != x)
fa[x] = findSet (fa[x]);
return fa[x];
}
void unionSet (const int x, const int y, const int val) {
int u = findSet (x), v = findSet (y);
if (u == v)
return ;
if (rnk[u] > rnk[v]) {
fa[v] = u;
G[u].push_back( make_pair (v, val) );
root[v] = true;
}
else {
fa[u] = v;
G[v].push_back( make_pair (u, val) );
root[u] = true;
if (rnk[u] == rnk[v])
++ rnk[v];
}
}
void buildTree (const int x, const int depth, const int father_) {
fa[x] = father_;
dep[x] = depth;
for (int i = 0; i < G[x].size(); ++ i) {
dis[ G[x][i].first ] = G[x][i].second;
buildTree (G[x][i].first, depth + 1, x);
}
}
int get_dis (int x, int y) {
if (dep[x] > dep[y])
swap (x, y);
if (x == y)
return 0;
int distance = max ( get_dis (x, fa[y]), dis[y] );
return distance;
}
int main () {
//freopen ("pictionary.in", "r", stdin);
//freopen ("pictionary.out", "w", stdout);
scanf ("%d %d %d", &n, &m, &query);
makeSet ();
for (int i = m; i; -- i)
for (int j = i << 1; j <= n; j += i)
if (findSet (i) != findSet (j))
unionSet (i, j, m - i + 1);
for (int i = 1; i <= n; ++ i)
if (root[i] == false)
buildTree (i, 1, i);
for (int i = 1; i <= query; ++ i) {
int x, y;
scanf ("%d %d", &x, &y);
int dis = get_dis (x, y);
printf ("%d\n", dis);
}
return 0;
}
我這裏用的是暴力爬山法,其實犇犇們可以用倍增。。。
後記
另外,根據我們老師的口胡,還有一種題解,只用並查集就可以。只用把每次新加入集合的需要查詢的點進行判斷,然後賦值答案就可以了,但由於本蒟蒻實在是太辣雞了,所以沒有碼出來,若果有大佬會這種方法,歡迎來鄙視我。。。