[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;
} 

我這裏用的是暴力爬山法,其實犇犇們可以用倍增。。。

後記

另外,根據我們老師的口胡,還有一種題解,只用並查集就可以。只用把每次新加入集合的需要查詢的點進行判斷,然後賦值答案就可以了,但由於本蒟蒻實在是太辣雞了,所以沒有碼出來,若果有大佬會這種方法,歡迎來鄙視我。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章