1. 程式人生 > 其它 >並查集的兩類擴充套件

並查集的兩類擴充套件

介紹

本篇部落格是我的Luogu Blog上兩篇博文的總和,但優化了閱讀體驗,主要介紹種類並查集和帶權並查集

種類並查集原文

帶權並查集原文

1.帶權並查集部分

我們在做並查集的一些題的時候,有時候會遇到這樣的問題

  • \(X\)\(Y\) 之間差了幾代,如果不在一個家族裡就輸出 \(-1\)

資料小的話,模擬水水還能過,但是我們既然學了並查集這種神奇的演算法,就要用正解去 \(\color{#228B22}{AC}\)

來看例題

P1196 銀河英雄傳說

題意和我上面敘述的差不多,我們來考慮解題方式

判斷是否在同一列以及如何一次操作合併兩個佇列的問題我們用普通的並查集就能實現,難點是求兩個點之間的距離

在上數學課的時候,我們會學一種特殊的距離求法,存兩個點到另一個定點的距離,相減來求距離,我們就用這種方法來實現在並查集中求距離

我們先設定幾個陣列

f[i]表示i的祖先

front[i]表示i到祖宗的距離(即隔了幾代)

num[i]表示以i為隊頭家族的長度

在合併倆個家族的時候,我們只需要對隊頭(祖宗)進行操作即可

我們把祖宗的 front 加上要合併的家族的任意節點的 num ,原來祖宗的 num 歸零,現在祖宗的 num +原來祖宗的 num ,完成合並的操作

		f[fa]=fb;
		front[fa]+=num[fb];
		num[fb]+=num[fa];
		num[fa]=0;

但為了之上的所有操作,我們需要先找到你的祖宗......

inline int find(int x)
{
	if(f[x]==x) return f[x];
	int fx=find(f[x]);
	front[x]=front[x]+front[front[fx]];//仔細想想這行有啥用
	return fx;
}

上面的那一行加上去的程式碼實際上是維護了 front 陣列,讓它始終是正確的值(即更新的i到隊頭的距離)

完整Code

#include<cstdio>
#include<iostream>
#include<cmath>
using namespace std;
int t;
char inm;
int a,b;
int f[30005];//i的祖先 
int front[30005];//表示i到目前隊頭的距離 
int num[30005];//num[i]表示以i為隊頭的佇列長度 
inline int find(int x)
{
	if(f[x]==x) return f[x];
	int fx=find(f[x]);
	front[x]+=front[f[x]];
	return f[x]=fx;
}
int main()
{
	cin>>t;
	for(int i=1; i<=30005;i++)
	{
		f[i]=i;
		front[i]=0;
		num[i]=1;
	}
	while(t--)
	{
		cin>>inm>>a>>b;
		int fa=find(a);
		int fb=find(b);
		if(inm=='M')
		{
			f[fa]=fb;
			front[fa]+=num[fb];
			num[fb]+=num[fa];
			num[fa]=0;
		}
		if(inm=='C')
		{
			if(fa==fb)
			{
				cout<<abs(front[a]-front[b])-1<<endl;
			}
			else
			{
				cout<<-1<<endl;
			}
		}
	}
	return 0;
}

2.種類並查集部分

P2024 [NOI2001] 食物鏈

乍看此題沒有思路(幾乎所有人都會這樣),事實上比賽的時候拿分的關鍵不是演算法能否寫對,而是你能否找到這道題該用什麼演算法,我們不做過多解釋,直接開始思路講解

A 吃 B,B 吃 C,C 吃 A,我們整理一下,用“天敵”和“同類”“獵物”來表示關係,不難發現他們之間的關係

(同類表示同一種動物,天敵表示誰吃誰,獵物表示誰被誰吃)

  1. 若X為Y的同類,他們所有的天敵和獵物就會在一個集合裡

  2. 若X為Y的天敵,Y的天敵和X就會在一個集合裡,Y的獵物和X的天敵也是(畫個圖試試),X的獵物和Y也是

我們開一個\(3\)倍空間的f[3n]陣列(為甚是\(3\)倍看完就懂了)

我們把這個陣列劃分為\(3\)

  • 1.\(1-n\) 的範圍儲存這個動物的同類

  • 2.\(n+1-2n\) 的範圍儲存這個動物的天敵

  • 3.\(2n+1-3n\) 的範圍儲存這個動物的獵物

這樣我們就可以方便的進行查詢與合併操作了

接下來考慮如何判斷輸入的話是否為假話

分情況討論

First 當X與Y是同類時

  • 1.\(x>n\) 或者 \(y>n\)

  • 2.\(x+n\)\(y\) 在同一個集合(天敵關係)

  • 3.\(x+2n\)\(y\) 在同一個集合(獵物關係)

Second 當X與Y是吃和被吃關係時

  • 1.同上

  • 2.\(x\)\(y\) 在同一個集合(同種不可能互相殘殺)

  • 3.\(x\)\(y\) 相等(自己吃自己?)

  • 4.\(x+n\)\(y\) 在同一個集合(反了)

解決了這一步,我們考慮合併

在第一種情況時,合併操作很簡單,我們只需要把關於他倆的所有關係的集合都合併即可

join(x,y);
join(x+n,y+n);
join(x+2*n,y+2*n);

在第二種情況時

join(x+2*n,y);//x的獵物和y合併(天敵的獵物就是同類) 
join(x,y+n);//x和y的原有天敵合併 
join(x+n,y+2*n);//x的天敵和y的獵物合併 

至此,我們解決了所有問題

#include<cstdio>
#include<iostream>
using namespace std;
int f[150010];
int n,k,ans;
int a,x,y;
inline int find(int xx)
{
	if(f[xx]==xx)
	{
		return f[xx];
	}
	return f[xx]=find(f[xx]);
}
inline void join(int xx,int yy)
{
	int f1=find(xx),f2=find(yy);
	if(f1!=f2) f[f1]=f2;
	return;
}
int main()
{
	cin>>n>>k;
	for(register int i=1; i<=3*n;i++) f[i]=i;
	while(k--)
	{
		scanf("%d%d%d",&a,&x,&y);
		if(a==1)//x和y是同類 
		{
			if(x>n||y>n||find(x+n)==find(y)||find(x+2*n)==find(y)) ans++;//1.x,y大於n 2.y是x天敵 3.y是x獵物 
			else
			{
				join(x,y);
				join(x+n,y+n);
				join(x+2*n,y+2*n);//所有的都是相同的 
			}
		}
		if(a==2)
		{
			if(x==y||x>n||y>n||find(x)==find(y)||find(x+n)==find(y))//1.同上 2.x和y相等(自己吃自己) 3.x和y是同類 4.y是x的天敵 
			{
				ans++;
			}
			else
			{
				join(x+2*n,y);//x的獵物和y合併(天敵的獵物就是同類) 
				join(x,y+n);//x和y的原有天敵合併 
				join(x+n,y+2*n);//x的天敵和y的獵物合併 
			}
		}
	}
	cout<<ans;
}

結束語

在翻這個題的題解的時候,看到了一段讓我印象深刻的文字

感覺說的好有道理,當我在學習一些高深的圖論和數論的時候,也經常會發出這樣的疑問,現在想起來,不過也是這樣罷了