「笔记」不仅仅是并查集

写在前面

并查集是一种树形的数据结构。

并查集能维护连通性,传递性。通俗的说:亲戚的亲戚是亲戚。

并查集一般支持两个操作:查询两个元素是否在同一个联通块内,合并两个联通块。

普通并查集

来道例题

P3367 【模板】并查集

初始化

初始状态下每个元素只与自己联通,我们用 (fa_i) 表示 (i) 所处联通块内的根节点,显然初始化 (fa_i = i)

寻找根节点

不断跳 (fa) 即可。

代码只有一行:

int Find(int x) { return fa[x] == x ? x : Find(fa[x]); }

复杂度 (O(n)).

合并

假设我们要合并 (u,v)。显然只需要让 (u,v) 所在联通块内的点都指向同一个根节点即可。

我们可以先找到 (u,v) 所在的根节点 (uf, vf),然后使 (fa_{uf} = vf),这样所有的点指向的跟结点就均为 (vf)

Code:

void Hb(int x, int y) { 
    int xf = Find(x), yf = Find(y);
    if(xf != yf) fa[xf] = yf; 
}

复杂度 (O(n))

查询

也比较简单,看看两个元素的根节点是否相同即可。

Code:

bool Check(int x, int y) { return Find(x) == Find(y); } 

复杂度 (O(n))

路径压缩

按照上面的方法操作,如果出题人特意构造数据,会把我们卡成 (O(n^2))

我们考虑优化一下,让每个元素直接指向它的根节点,免去跳多步的麻烦。

优化后代码:

int Find(int x) { return fa[x] == x ? x : fa[x] = Find(fa[x]); }

稍微一改即可。

带权并查集

看道例题 P4079 [SDOI2016]齿轮

对每个结点另存一个值 (k_u = 1)

(u,v) 之间出现传动比关系时,令 (fa_u = v),且 (k_u = frac{x}{y})

同样带权并查集也可以进行路径压缩

若使 (fa_u = fa_{fa_u}),则 (k_u = k_u imes k_{fa})

联通块合并

已知 (k_u,k_v),现在要连接 (u,v),即 (k_u^, = frac{x}{y}),我们要让 (fa_u o fa_v),设值为 (k_{fa_u})

因为要保证转动比合法且不变。所以 (k_u imes k_{fa_u} = f_u^, imes f_v),解得 (k_{fa_u} = frac{f_u^, imes f_v}{k_u})

同一联通块内的两点判断是否有解:看看这个式子是否成立即可 (k_u imes k_{fa_u} = f_u^, imes f_v)

/*
Work by: Suzt_ilymics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#define LL long long
#define orz cout<<"lkp AK IOI!"<<endl

using namespace std;
const int MAXN = 1e5+5;
const int INF = 1e9+7;
const int mod = 1e9+7;
const double lim = 1e-10;

int T, n, m;
int fa[1010];
double k[1010];

int read(){
    int s = 0, f = 0;
    char ch = getchar();
    while(!isdigit(ch))  f |= (ch == '-'), ch = getchar();
    while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
    return f ? -s : s;
}

int Find(int x) {
    if(fa[x] == x) return x;
    int tmp = Find(fa[x]); // 带权并查集路径压缩 
    k[x] *= k[fa[x]];
    return fa[x] = tmp;
}

int main()
{
    T = read(); int cnt = 0;
    while(++cnt <= T) {
        n = read(), m = read();
        for(int i = 1; i <= n; ++i) fa[i] = i, k[i] = 1.00;
        bool flag = false;
        for(int i = 1, u, v, x, y; i <= m; ++i) {
            u = read(), v = read(), x = read(), y = read();
            int uf = Find(u), vf = Find(v);
            if(uf != vf) {
                fa[uf] = vf;
                k[uf] *= (1.0 * x * k[v]) / (1.0 * y * k[u]);
            } else {
                double K = (k[u] / k[v]);
                if(abs(K - (1.0 * x / y)) > lim) flag = true;
            }
        }
        if(flag) printf("Case #%d: No
", cnt);
        else printf("Case #%d: Yes
", cnt);
    }
    return 0;
}

种类并查集

两道例题:P1525 [NOIP2010 提高组] 关押罪犯P2024 [NOI2001] 食物链

在这种情况下,我们需要维护这样一种关系:

敌人的敌人是朋友。

常见的做法是将原并查集扩大一倍规模,并划分为两个种类。

在同个种类的并查集中合并,和原始的并查集没什么区别,仍然表达他们是朋友这个含义。

考虑在不同种类的并查集中合并的意义,其实就表达 他们是敌人 这个含义了。

按照并查集美妙的 传递性,我们就能具体知道某两个元素到底是 敌人 还是 朋友 了。

至于某个元素到底属于两个种类中的哪一个,由于我们不清楚,因此两个种类我们都试试。

如果是食物链这道题,我们应该把原并查集扩大三倍规模。只有敌人的敌人的敌人才是朋友。修改的时候也要同时修改三个种类之间的关系。我们也可以继续推广到更大的规模。

放一下两个例题的代码:

关押罪犯 Code:

#include<iostream>
#include<cstdio>
#include<algorithm>

using namespace std;
const int MAXN = 2e4+4;
const int MAXM = 1e5+5;
struct edge{
	int from, to, w;
	bool operator < (const edge &b) const {return w < b.w; }
}e[MAXM];

int read(){
	int s = 0, w = 1;
	char ch = getchar();
	while(ch < '0' || ch > '9') { if(ch == '-') w = -1; ch = getchar(); }
	while(ch >= '0' && ch <= '9') s = (s << 1) + (s << 3) + ch - '0', ch = getchar();
	return s * w;
}

int n, m;
int fa[MAXN << 1];

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

int main()
{
	n = read(), m = read();
	for(int i = 1; i <= 2 * n; ++i) fa[i] = i;
	for(int i = 1; i <= m; ++i){
		e[i].from = read(), e[i].to = read(), e[i].w = read();
	}
	sort(e + 1, e + m + 1);
	for(int i = m; i >= 0; --i){
		int uf = find(e[i].from), vf = find(e[i].to);
		if(uf == vf) {
			printf("%d", e[i].w);
			break;
		} 
		else{
			fa[find(e[i].from)] = find(e[i].to + n);
			fa[find(e[i].to)] = find(e[i].from + n);
		}
	}
	return 0;
}

食物链 Code:

/*
Work by: Suzt_ilymics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#define LL long long
#define orz cout<<"lkp AK IOI!"<<endl

using namespace std;
const int MAXN = 1e6+5;
const int INF = 1e9+7;
const int mod = 1e9+7;

struct edge {
    int from, to, nxt;
}e[MAXN << 1];
int head[MAXN], num_edge = 1;

int n, k, Ans = 0;
int fa[MAXN];

int read(){
    int s = 0, f = 0;
    char ch = getchar();
    while(!isdigit(ch))  f |= (ch == '-'), ch = getchar();
    while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
    return f ? -s : s;
}

int Find(int x) { return fa[x] == x ? x : fa[x] = Find(fa[x]); }

int main()
{
//    freopen("P2024_2.in","r",stdin);
    n = read(), k = read();
    for(int i = 1; i <= 3 * n; ++i) fa[i] = i;
    for(int i = 1, opt, x, y; i <= k; ++i) {
        opt = read(), x = read(), y = read();
        if(x > n || y > n) { Ans++; continue; } 
        if(opt == 1) {
            if(Find(x + n) == Find(y) || Find(x) == Find(y + n)) Ans++;
            else {
                fa[Find(x)] = Find(y);
                fa[Find(x + n)] = Find(y + n);
                fa[Find(x + 2 * n)] = Find(y + 2 * n);
            }
        } else {
            if(Find(x) == Find(y) || Find(x) == Find(y + n)) Ans++;
            else {
                fa[Find(x + n)] = Find(y);
                fa[Find(x + 2 * n)] = Find(y + n);
                fa[Find(x)] = Find(y + 2 * n);
            }
        }
    }
    printf("%d", Ans);
    return 0;
}

原文地址:https://www.cnblogs.com/Silymtics/p/15017554.html