1. 程式人生 > >並查集與帶權並查集

並查集與帶權並查集

並查集演算法

概要

並查集作為演算法競賽中較為簡單、易用的資料結構,適用於由時序併入的動態集合查詢。並查集中的兩個主要操作就是“合併集合”與“查詢集合”

演算法

用集合中的某個元素來代表這個集合,該元素稱為集合的代表元
一個集合內的所有元素組織成以代表元為根的樹形結構。
在並查集演算法中,合併操作是將該元素所在樹連線在被合併元素所在樹上。
對於查詢操作,即是路經查詢到樹根,確定代表元的過程。

判斷兩個元素是否屬於同一集合,只需要看他們的代表元是否相同即可。

路徑壓縮

對於不相交集合的操作,一般採用兩種啟發式優化的方法:
1. 按秩合併:使包含較少結點的樹根指向包含較多結點的樹根。
2. 路徑壓縮:使路徑查詢上的每個點都直接指向根結點。

在大多數場景中,路徑壓縮就能滿足時間要求。

時間複雜度

對於有n項,m次操作的並查集(其中有f次查詢),執行時間時間複雜度為:
1. 樸素的並查集:O(n2)
2. 帶按秩合併的並查集:O(mlgn)
3. 帶路徑壓縮的並查集:O(n+f(log2+f/nn))
4. 帶路徑壓縮的按秩合併並查集:O(mα(n))
其中α(n)Ackerman函式反函式,對於實際運用中,可認為α(n)4

具體實現

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]s[i],分別表示i之下的塊數和i所在堆的數量。在路徑壓縮時,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;
}

我們需要新增兩種屬性cnt[i]trans[i],分別表示該堆數量和轉移次數。在路徑壓縮時,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-1r之間)
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;
}

與上一題類似,由於資料範圍較大,需要首先進行離散化。由於只有奇偶兩種狀態,所以只需要用01表示狀態即可。在路徑壓縮時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的原因(否則就是一種鏈式關係了)。
Relation_Graph
關於num[fb] = (3 - num[v] + num[u] + (p - 1)) % 3的推導,我們可以畫圖來理解。
我們能夠獲得的資訊是num[fa] num[u] num[v]u v之間的關係,有一個很好的性質就是在該模3系中,關係是可以逆推的,即如果把從vfb的鏈反向,那麼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;
}

可持久化並查集

其他雜題

(待填坑)