[COCI 2017/2018 Round #5] pictionary题解(并查集 + Lca)

题目描述

在一个尚未发现的宇宙中,有一个行星中的一个国家,只有数学家居住。在这个国家中,总共有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;
} 

我这里用的是暴力爬山法,其实犇犇们可以用倍增。。。

后记

另外,根据我们老师的口胡,还有一种题解,只用并查集就可以。只用把每次新加入集合的需要查询的点进行判断,然后赋值答案就可以了,但由于本蒟蒻实在是太辣鸡了,所以没有码出来,若果有大佬会这种方法,欢迎来鄙视我。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值