並查集與帶權並查集
並查集演算法
概要
並查集作為演算法競賽中較為簡單、易用的資料結構,適用於由時序併入的動態集合查詢。並查集中的兩個主要操作就是“合併集合”與“查詢集合”
演算法
用集合中的某個元素來代表這個集合,該元素稱為集合的代表元。
一個集合內的所有元素組織成以代表元為根的樹形結構。
在並查集演算法中,合併操作是將該元素所在樹連線在被合併元素所在樹上。
對於查詢操作,即是路經查詢到樹根,確定代表元的過程。
判斷兩個元素是否屬於同一集合,只需要看他們的代表元是否相同即可。
路徑壓縮
對於不相交集合的操作,一般採用兩種啟發式優化的方法:
1. 按秩合併:使包含較少結點的樹根指向包含較多結點的樹根。
2. 路徑壓縮:使路徑查詢上的每個點都直接指向根結點。
在大多數場景中,路徑壓縮就能滿足時間要求。
時間複雜度
對於有項,次操作的並查集(其中有次查詢),執行時間時間複雜度為:
1. 樸素的並查集:
2. 帶按秩合併的並查集:
3. 帶路徑壓縮的並查集:
4. 帶路徑壓縮的按秩合併並查集:
其中為Ackerman函式反函式,對於實際運用中,可認為
具體實現
void init(int n) { for (int i = 0 ; i <= n; i++) { f[i] = i; rank[i] = 0;} }
int find(int x) { return x == f[x] ? x : f[x] = find(f[x]); }
void union(int x, iny y){
x = find(x), y = find(y);
if(x != y){
if(rank[x] > rank[y]) f[y] = x;
else{
f[x] = y;
if(rank[x] == rank[y])
rank[y]++;
}
}
}
注意在合併時,我們只需考慮這臺電腦與之前已經修復的電腦能否通訊。
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long ll;
const int N = 1005;
ll f[N], x[N], y[N];
bool vis[N];
int n;
ll d;
bool Con(int u, int v){
return (x[u] - x[v]) * (x[u] - x[v]) + (y[u] - y[v]) * (y[u] - y[v]) <= d * d;
}
int find(int x){return f[x] == x? x: f[x] = find(f[x]);}
int main()
{
scanf("%d%lld", &n, &d);
for(int i = 1; i <= n; i++){
f[i] = i;
scanf("%lld%lld",&x[i], &y[i]);
}
char ch;
int u, v;
while(getchar(), scanf("%c", &ch) != EOF){
if(ch == 'O'){
scanf("%d", &u);
if(!vis[u]){
for(int i = 1; i <= n; i++){
if(!vis[i]) continue;
if(Con(i, u)){
int fa = find(i), fb = find(u);
f[fa] = fb;
}
}
vis[u] = true;
}
}
else{
scanf("%d%d", &u, &v);
int fa = find(u), fb = find(v);
if(vis[u] && vis[v] && fa == fb) puts("SUCCESS");
else puts("FAIL");
}
}
return 0;
}
帶權並查集
概要
在並查集的基礎上,對其中的每一個元素賦有某些值。在對並查集進行路徑壓縮和合並操作時,這些權值具有一定屬性,即可將他們與父節點的關係,變化為與所在樹的根結點關係。
統計
我們需要新增兩種屬性與,分別表示之下的塊數和所在堆的數量。在路徑壓縮時,cnt[i] += cnt[f[i]] ,另外在連線操作時,需要動態更新cnt[find(u)]和s[find(v)]的資訊。
#include <cstdio>
#include <cstring>
const int N = 30005;
int f[N], cnt[N], s[N];
int find(int x){
if(x != f[x]){
int fa = f[x];
f[x] = find(f[x]);
cnt[x] += cnt[fa];
}
return f[x];
}
int main()
{
for(int i = 0; i < N; i++) {
f[i] = i;
s[i] = 1;
}
int n;
scanf("%d", &n);
char ch;
int u, v;
for(int i = 0; i < n; i++){
getchar();
scanf("%c", &ch);
if(ch == 'M'){
scanf("%d%d", &u, &v);
int fa = find(u), fb = find(v);
if(fa != fb){
f[fa] = fb;
cnt[fa] = s[fb];
s[fb] += s[fa];
}
}
else{
scanf("%d", &u);
find(u);
printf("%d\n", cnt[u]);
}
}
return 0;
}
我們需要新增兩種屬性和,分別表示該堆數量和轉移次數。在路徑壓縮時,trans[x] += trans[fa]。在合併時,動態更新被合併樹的堆數量,並增加合併樹的轉移次數cnt[fy] += cnt[fx],trans[fx++]。
#include <cstdio>
#include <algorithm>
#include <cstring>
const int N = 10005;
int f[N], trans[N], cnt[N];
int n, m, qry, fx, fy, u, v;
char ch[5];
int find(int x){
if(x != f[x]){
int fa = f[x];
f[x] = find(f[x]);
trans[x] += trans[fa];
}
return f[x];
}
void init(int n){
for(int i = 1; i <= n; i++){
f[i] = i;
trans[i] = 0;
cnt[i] = 1;
}
}
int main()
{
int T;
scanf("%d", &T);
for(int kase = 1; kase <= T; kase++){
scanf("%d%d", &n, &qry);
init(n);
printf("Case %d:\n", kase);
for(int i = 0; i < qry; i++){
scanf("%s", ch);
if(ch[0] == 'T'){
scanf("%d%d", &u, &v);
fx = find(u), fy = find(v);
if(fx != fy){
f[fx] = fy;
cnt[fy] += cnt[fx];
trans[fx] ++;
}
}
else{
scanf("%d", &u);
v = find(u);
printf("%d %d %d\n", v, cnt[v], trans[u]);
}
}
}
return 0;
}
區間統計
需要注意:
1. 此類問題需要對所有值統計設定相同的初值,但初值的大小一般沒有影響。
2. 對區間[l, r]進行記錄時,實際上是對 (l-1, r]操作,即l = l - 1。(即勢差是在l-1和r之間)
3. 在進行路徑壓縮時,可和統計類問題相似的cnt[x] += cnt[fa](因為勢差是直接累計到根結點的)
4. 在合併操作中,對我們需要更新cnt[fb](由於fb連線到了fa上),動態更新的公式是cnt[fb] = cnt[u - 1] - cnt[v] + d,為了理解這個式子,我們進行如下討論:
- 更新cnt[fb]的目的是維護被合併的樹(fb)相對於合併樹(fa)之間的勢差。
- cnt[fb] - cnt[fa]兩者之間的關係,並不能直接建立,而是通過cnt[u - 1] - cnt[v]之間的關係建立。
- 可知 cnt[v]儲存結點v與結點fb之間的勢差; cnt[u-1]儲存結點u-1與結點fa之間的勢差;d是更新資訊中結點u-1與結點v之間的勢差
- 所以cnt[fb]的值為從結點fb與結點v(-cnt[v]),加上從結點v到結點u(d),最後加上從結點u-1到結點fa(cnt[u - 1])
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <climits>
using namespace std;
typedef long long ll;
const int N = 200005;
int f[N];
ll cnt[N];
int find(int x)
{
if(x != f[x]){
int fa = f[x];
f[x] = find(f[x]);
cnt[x] += cnt[fa];
}
return f[x];
}
void init(int n)
{
for(int i = 0; i <= n; i++){
f[i] = i;
cnt[i] = 0;
}
}
int main()
{
int n, m;
while(scanf("%d%d", &n, &m) != EOF){
int ans = 0;
init(n);
int u, v, d, fa, fb;
for(int i = 0; i < m; i++){
scanf("%d%d%d", &u, &v, &d);
fa = find(u - 1), fb = find(v);
if(fa == fb){
if(cnt[u - 1] + d != cnt[v]) ans++;
}
else{
f[fb] = fa;
cnt[fb] = cnt[u - 1] - cnt[v] + d;
}
}
printf("%d\n", ans);
}
return 0;
}
與上一題類似,由於資料範圍較大,需要首先進行離散化。由於只有奇偶兩種狀態,所以只需要用0和1表示狀態即可。在路徑壓縮時num[x] = num[x] ^ num[fa](模2系)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <climits>
using namespace std;
typedef long long ll;
const int N = 200005;
const int LEN = 7;
int f[N], num[N], des[2 * N];
struct Query{
int u, v, d;
}q[N];
int find(int x)
{
if(x != f[x]){
int fa = f[x];
f[x] = find(f[x]);
num[x] = num[x] ^ num[fa];
}
return f[x];
}
void init(int n)
{
for(int i = 0; i <= n; i++){
f[i] = i;
num[i] = 0;
}
}
int main()
{
int n, m;
while(scanf("%d%d", &n, &m) != EOF){
bool conf = false;
int ans = 0, cnt = 0;
int u, v, fa, fb;
char s[LEN];
for(int i = 0; i < m; i++){
scanf("%d%d%s", &q[i].u, &q[i].v, s);
if(s[0] == 'o') q[i].d = 1;
else q[i].d = 0;
des[cnt++] = q[i].u - 1;
des[cnt++] = q[i].v;
}
sort(des, des + cnt);
cnt = unique(des, des + cnt) - des;
init(cnt);
for(int i = 0; i < m; i++){
if(!conf){
u = lower_bound(des, des + cnt, q[i].u - 1) - des;
v = lower_bound(des, des + cnt, q[i].v) - des;
fa = find(u), fb = find(v);
if(fa == fb){
if((num[u] + q[i].d) % 2 != num[v]) conf = true;
}
else{
f[fb] = fa;
num[fb] = (2 + num[u] - num[v] + q[i].d) % 2;
}
if(!conf) ans++;
}
}
printf("%d\n", ans);
}
return 0;
}
種類並查集
注意到座位編號是1~300,所以是一個模300系。相對於區間統計類的並查集,這裡的pos[i]可以被理解為每個人的種類。其他操作類似於區間統計類的並查集。
#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
const int N = 50005;
const int MOD = 300;
int f[N], pos[N];
int find(int x){
if(x != f[x]){
int fa = f[x];
f[x] = find(f[x]);
pos[x] = (pos[x] + pos[fa]) % MOD;
}
return f[x];
}
void init(int n){
for(int i = 0; i <= n; i++){
f[i] = i;
pos[i] = 0;
}
}
int main()
{
int n, m, ans;
while(scanf("%d%d", &n, &m) != EOF){
init(n);
int u, v, d, fa, fb;
ans = 0;
for(int i = 0; i < m; i++){
scanf("%d%d%d", &u, &v, &d);
fa = find(u), fb = find(v);
if(fa == fb){
if(pos[v] != (pos[u] + d) % MOD){
ans++;
}
}
else{
f[fb] = fa;
pos[fb] = (MOD - pos[v] + pos[u] + d) % MOD;
}
}
printf("%d\n", ans);
}
return 0;
}
可參考:http://blog.csdn.net/c0de4fun/article/details/7318642
對於這三種種類,同類可以用0表示,其他兩種分別用1表示該結點被父節點吃,2表示該節點吃父節點。
該題之所以能用並查集進行路徑壓縮,是因為存在A吃B,B吃C,C吃A的三角關係。這是我們能在路徑壓縮中使用num[x] = (num[x] + num[fa]) % 3和更新時使用num[fb] = (3 - num[v] + num[u] + (p - 1)) % 3的原因(否則就是一種鏈式關係了)。
關於num[fb] = (3 - num[v] + num[u] + (p - 1)) % 3的推導,我們可以畫圖來理解。
我們能夠獲得的資訊是num[fa] num[u] num[v]與u v之間的關係,有一個很好的性質就是在該模3系中,關係是可以逆推的,即如果把從v到fb的鏈反向,那麼fb相對於v的關係就是3-num[v]
如圖所示,我們在這些關係的基礎上,要獲得fb相對於fa的關係,直接將“關係”進行(反轉)相加即可。
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 50005;
int f[N], num[N];
int find(int x){
if(x != f[x]){
int fa = f[x];
f[x] = find(f[x]);
num[x] = (num[x] + num[fa]) % 3;
}
return f[x];
}
int main()
{
int n, q, ans = 0;
scanf("%d%d", &n, &q);
for(int i = 0; i < n; i++) f[i] = i;
for(int i = 0; i < q; i++){
int p, u, v;
scanf("%d%d%d", &p, &u, &v);
if(u > n || v > n) ans++;
else if(p == 2 && u == v) ans++;
else{
int fa = find(u), fb = find(v);
if(fa == fb){
if(num[v] != (num[u] + (p - 1)) % 3)
ans++;
}
else{
f[fb] = fa;
num[fb] = (3 - num[v] + num[u] + (p - 1)) % 3;
}
}
}
printf("%d\n", ans);
return 0;
}
基礎操作與食物鏈非常相似,但是有可能出現一個或多個異常。在以下方法中,我們列舉每個人是異常的情況,觀察有多少個人可能是裁判。如果有多個人成為裁判使體系融洽,那麼不能確定;如果沒有一個人成為裁判後體系融洽,那麼就沒有答案;如果只有一個人可能成為裁判,那麼我們得出判斷的輪數,就是其他人成為裁判時,使體系不融洽的最大輪數。
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 505;
const int M = 2005;
struct Res{
int u, v;
int r;
}r[M];
int f[N], num[N], err[N];
int n, m;
int find(int x){
if(x != f[x]){
int fa = f[x];
f[x] = find(f[x]);
num[x] = (num[x] + num[fa]) % 3;
}
return f[x];
}
void init(int n){
for(int i = 0; i < n; i++) {
f[i] = i;
num[i] = 0;
}
}
int main(){
while(scanf("%d%d", &n, &m) != EOF){
for(int i = 0; i < m; i++){
char ch;
scanf("%d%c%d", &r[i].u, &ch, &r[i].v);
if(ch == '=') r[i].r = 0;
else if(ch == '<') r[i].r = 1;
else if(ch == '>') r[i].r = 2;
}
memset(err, -1, sizeof(err));
for(int i = 0; i < n; i++){
init(n);
for(int j = 0; j < m; j++){
if(i == r[j].u || i == r[j].v) continue;
int fa = find(r[j].u), fb = find(r[j].v);
if(fa == fb){
if(num[r[j].v] != (num[r[j].u] + r[j].r) % 3){
err[i] = j + 1;
break;
}
}
else{
f[fb] = fa;
num[fb] = (num[r[j].u] - num[r[j].v] + 3 + r[j].r) % 3;
}
}
}
int cnt = 0, ans1 = 0, ans2 = 0;
for(int i = 0; i < n; i++){
if(err[i] == -1){
cnt ++;
ans1 = i;
}
ans2 = max(ans2, err[i]);
}
if(cnt == 0) printf("Impossible\n");
else if(cnt > 1) printf("Can not determine\n");
else printf("Player %d can be determined to be the judge after %d lines\n", ans1, ans2);
}
return 0;
}
模2系,只需注意最後三種情況的判斷。
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 100005;
int f[N], num[N];
int find(int x){
if(x != f[x]){
int fa = f[x];
f[x] = find(f[x]);
num[x] = (num[x] + num[fa]) % 2;
}
return f[x];
}
void init(int n){
for(int i = 0; i <= n; i++){
f[i] = i;
num[i] = 0;
}
}
int main()
{
int T, n, q;
scanf("%d", &T);
for(int i = 0; i < T; i++){
scanf("%d%d", &n, &q);
init(n);
for(int i = 0; i < q; i++){
char ch;
int u, v;
getchar();
scanf("%c%d%d", &ch, &u, &v);
int fa = find(u), fb = find(v);
if(ch == 'D'){
f[fb] = fa;
num[fb] = (1 - num[v] + num[u]) % 2;
}
else{
if(fa != fb) printf("Not sure yet.\n");
else if(num[u] != num[v]) printf("In different gangs.\n");
else printf("In the same gang.\n");
}
}
}
return 0;
}
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 2005;
int f[N], num[N];
int n, m, u, v;
int find(int x)
{
if(x != f[x]){
int fa = f[x];
f[x] = find(f[x]);
num[x] = (num[x] + num[fa]) % 2;
}
return f[x];
}
void init(int n)
{
for(int i = 0 ; i <= n; i++){
f[i] = i;
num[i] = 0;
}
}
int main(){
int T;
scanf("%d", &T);
for(int kase = 1; kase <= T; kase++){
bool conf = false;
scanf("%d%d", &n, &m);
init(n);
for(int i = 0; i < m; i++){
scanf("%d%d", &u, &v);
if(!conf){
int fa = find(u), fb = find(v);
if(fa == fb){
if(num[u] == num[v]) conf = true;
}
else{
f[fb] = fa;
num[fb] = 1 - num[v] + num[u];
}
}
}
if(kase > 1) puts("");
printf("Scenario #%d:\n", kase);
if(conf) puts("Suspicious bugs found!");
else puts("No suspicious bugs found!");
}
return 0;
}
其他與並查集相關的問題
逆向並查集
由於題目的特殊性,我們可以逆向構建並查集。初始化cnt = n如果發現兩者不屬於同一集合,有cnt–。對於每一步,有ans[i - 1] = cnt
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 10005;
const int M = 100005;
int f[N];
int a[M], b[M], ans[M];
int find(int x){return f[x] == x? x: f[x] = find(f[x]);}
int main()
{
int n, m;
while(scanf("%d%d", &n, &m) != EOF){
for(int i = 0; i <= n; i++)
f[i] = i;
for(int i = 1; i <= m; i++)
scanf("%d%d", &a[i], &b[i]);
int cnt = n, fa, fb;
ans[m] = n;
for(int i = m; i >= 1; i--){
fa = find(a[i]), fb = find(b[i]);
if(fa != fb){
cnt--;
f[fb] = fa;
}
ans[i - 1] = cnt;
}
for(int i = 1; i <= m; i++)
printf("%d\n", ans[i]);
}
return 0;
}
可持久化並查集
其他雜題
(待填坑)