在一个尚未发现的宇宙中,有一个行星中的一个国家,只有数学家居住。在这个国家中,总共有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
#include
#include
#include
#include
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
#include
#include
#include
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;
}
我这里用的是暴力爬山法,其实犇犇们可以用倍增。。。
另外,根据我们老师的口胡,还有一种题解,只用并查集就可以。只用把每次新加入集合的需要查询的点进行判断,然后赋值答案就可以了,但由于本蒟蒻实在是太辣鸡了,所以没有码出来,若果有大佬会这种方法,欢迎来鄙视我。。。