1. 程式人生 > >POJ-1182 食物鏈 (並查集)&&並查集的深層次思想

POJ-1182 食物鏈 (並查集)&&並查集的深層次思想

解題思路:

這道題是並查集題目中的經典。。。而且比普通並查集提高了一個檔次,下面在基礎並查集的前提上講解並查集的真正用法。

基礎回顧:

find()函式找根結點的兩種寫法如下:

第一種遞迴:

int find(int x)
{
	return x == pre[x] ? x : find(pre[x]);
}

第二種:

int find(int x)
{
	int root, temp;
	root = x;
	while(root != pre[root])
		root = pre[root];
	while(x != root)
	{
		temp = pre[x];
		pre[temp] = root;
		x = temp;
	}
	return root;
}

上面2種是最基本的查詢操作。 下面我們通過這道題來講解一下並查集的深層次應用。

輸入:動物個數n以及k句話,接著輸入k行,每一行形式為:d x y,在輸入時可以先判斷題目所說的條件2和3,即: 1>若(x>n||y>n):即當前的話中x或y比n大,則假話數目sum加1. 2>若(x2&&xy):即當前的話表示x吃x,則假話數目sum加1.

而不屬於這兩種情況外的話語要利用並查集進行判斷當前的話是否與此前已經說過的話相沖突.

struct node
{
	int parent;                     //p[i].parent表示節點i的父節點
	int relation;                   //p[i].relation表示節點i與其父節點(即p[i].parent)的關係
}p[50010];

此處relation有三種取值(假設節點x的父節點為rootx,即p[x].parent=rootx): p[x].relation=0 ……表示節點x與其父節點rootx的關係是:同類 p[x].relation=1 ……表示節點x與其父節點rootx的關係是:被根結點吃 p[x].relation=2 ……表示節點x與其父節點rootx的關係是:吃根結點

初始化函式為:

void init(int n)
{
	int i;
	for(i = 1;i <= n; ++i)
	{
		p[i].parent = i;            //初始時集合編號就設定為自身
		p[i].relation = 0;        //因為p[i].parent=i,即節點i的父親節點就是自身,所以此時節點i與其父親節點的關係為同類(即p[i].relation=0)
	}
}

下面詳細講解並查集的兩個重要操作:查詢和合並.

查詢操作: 在查詢時因為節點不僅有父親節點域,而且還有表示節點與其父親節點的關係域,查詢過程中對父親節點域的處理和簡單的並查集處理一樣,即在查詢過程中同時實現路徑壓縮,但正是由於路徑壓縮,使得表示節點與其父親節點的關係域發生了變化,所以在路徑壓縮過程中節點和其對應的父節點的關係域發生了變化(因為路徑壓縮之前節點x的父親節點為rootx的話,那麼在路徑壓縮之後節點x的父親節點就變為了節點rootx的父親節點rootxx,所以此時p[x].relation儲存的應該是節點x與現在父親節點rootxx的關係),此處可以畫圖理解一下:

在這裡插入圖片描述在這裡插入圖片描述 很明顯查詢之前節點x的父親節點為rootx,假設此時p[x].relation=1(即表示x的父親節點rootx吃x)且p[rootx].relation=0(即表示rootx和其父親節點rootxx是同類),由這兩個關係可以推出rootxx吃x,而合併以後節點x的父親節點為rootxx(實現了路徑壓縮),且節點x的父親節點rootxx吃x,即查詢之後p[x].relation=1。

合併操作: 在將元素x與y所在的集合合併時,假設元素x所在的集合編號為rootx,元素y所在的集合編號為rooty,合併時直接將集 合rooty掛到集合rootx上,即p[rooty].parent=rootx,此時原來集合rooty中的根節點rooty的關係域也應隨之發生變化, 因為合併之前rooty的父親節點就是其自身,故此時p[rooty].relation=0,而合併之後rooty的父親節點為rootx,所以此時需判斷root x與rooty的關係,即更新p[rooty]的值,同理畫圖理解: 在這裡插入圖片描述在這裡插入圖片描述

此時假設假設p[x].relation=0(即x與rootx的關係是同類),p[y].relation=1(即rooty吃y),則有: 1>輸入d=1時,即輸入的x和y是同類,則有上述關係可以推出rooty吃rootx,即p[rooty].relation=2; 2>輸入d=2時,即輸入的x吃y,則有上述關係可以推出rooty與rootx是同類(因為rooty吃y,x吃y,則rooty與x是同類,又rootx與x是同類),即p[rooty].relation=0; 當然,這只是一種可能,其它的可能情況和上面一樣分析。

當元素x與元素y在同一集合時,則不需要合併,因為此時x與y的父親節點相同,可以分情況討論: 1>d=1時,即x與y是同類時,此時要滿足這要求,則必須滿足p[x].relation=p[y].relation,這很容易推出來. 2>d=2時,即表示x吃y,此時要滿足這要求,則也必須滿足一定的條件,如x和root是同類(即p[x].relation=0),此時要滿足x吃y,則必須滿足root吃y,即p[y].relation=1,可以像上面一樣畫圖來幫助理解.

關係域更新:

當然,這道題理解到這裡思路已經基本明確了,剩下的就是如何實現,在實現過程中,我們發現,更新關係域是一個很頭疼的操作,網上各種分析都有,但是都是直接給出個公式,至於怎麼推出來的都是一筆帶過,讓我著實頭疼了很久,經過不斷的看discuss,終於明白了更新操作是通過什麼來實現的。下面講解一下 仔細再想想,rootx-x 、x-y、y-rooty,是不是很像向量形式?於是我們可以大膽的從向量入手:

tx ty

| —|

x ~ y

對於集合裡的任意兩個元素x,y而言,它們之間必定存在著某種聯絡,因為並查集中的元素均是有聯絡的(這點是並查集的實質,要深刻理解),否則也不會被合併到當前集合中。那麼我們就把這2個元素之間的關係量轉化為一個偏移量(大牛不愧為大牛!~YM)。

由上面可知: x->y 偏移量0時 x和y同類

x->y 偏移量1時 x被y吃

x->y 偏移量2時 x吃y

有了這個假設,我們就可以在並查集中完成任意兩個元素之間的關係轉換了。

不妨繼續假設,x的當前集合根節點rootx,y的當前集合根節點rooty,x->y的偏移值為d-1(題中給出的詢問已知條件)

(1)如果rootx和rooty不相同,那麼我們把rooty合併到rootx上,並且更新relation關係域的值(注意:p[i].relation表示i的根結點到i的偏移量!!!!(向量方向性一定不能搞錯)) 此時 rootx->rooty = rootx->x + x->y + y->rooty,這一步就是大牛獨創的向量思維模式 上式進一步轉化為: rootx->rooty = (relation[x]+d-1+3-relation[y])%3 = relation[rooty],(模3是保證偏移量取值始終在[0,2]間) (2)如果rootx和rooty相同(即x和y在已經在一個集合中,不需要合併操作了,根結點相同),那麼我們就驗證x->y之間的偏移量是否與題中給出的d-1一致 此時 x->y = x->rootx + rootx->y 上式進一步轉化為:x->y = (3-relation[x]+relation[y])%3, 若一致則為真,否則為假。 分析到這裡,這道題已經從思想過渡到實現了。剩下的就是一些細節問題,自己處理一下就好了。 PS:做完這題,就可以去秒了大部分基礎的並查集了,嘿嘿

程式碼如下:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 50010
 
struct node
{
	int pre;
	int relation;
};
node p[N];
 
int find(int x) //查詢根結點
{
	int temp;
	if(x == p[x].pre)
		return x;
	temp = p[x].pre; //路徑壓縮
	p[x].pre = find(temp);
	p[x].relation = (p[x].relation + p[temp].relation) % 3; //關係域更新
	return p[x].pre; //根結點
}
 
int main()
{
	int n, k;
	int ope, a, b;
	int root1, root2;
	int sum = 0; //假話數量
	scanf("%d%d", &n, &k);
	for(int i = 1; i <= n; ++i) //初始化
	{
		p[i].pre = i;
		p[i].relation = 0;
	}
	for(int i = 1; i <= k; ++i)
	{
		scanf("%d%d%d", &ope, &a, &b);
		if(a > n || b > n) //條件2
		{
			sum++;
			continue;
		}
		if(ope == 2 && a == b) //條件3
		{
			sum++;
			continue;
		}
		root1 = find(a);
		root2 = find(b);
		if(root1 != root2) // 合併
		{
			p[root2].pre = root1;
			p[root2].relation = (3 + (ope - 1) +p[a].relation - p[b].relation) % 3;
		}
		else
		{
			if(ope == 1 && p[a].relation != p[b].relation)
			{
				sum++;
				continue;
			}
			if(ope == 2 && ((3 - p[a].relation + p[b].relation) % 3 != ope - 1))
			{
				sum++;
				continue;}
		}
	}
	printf("%d\n", sum);
	return 0;
}