并查集

目录
  • 并查集
    • 1.基础知识
      • 1.1 边带权并查集
      • 1.2 扩展域并查集
      • 1.3 并查集判断奇环
    • 2. 例题
      • 2.1 边带权并查集
      • 2.2 拓展域并查集
      • 2.3 路径压缩思想运用--从并查集思想出发

并查集

1.基础知识

1.1 边带权并查集

适用条件: 这样的方法适用于维护存在距离关系的情况,比如说统计相互间的大小关系,距离关系
具体方法: 设置一个d数组,d[u]表示u到fa[u]的距离(或者表示u和fa[u]的属性是否相同)
在两个地方需要维护:

  1. 在做get操作时维护a到pa之间的所有点的d值,这样求得的d值是这些点到pa的距离
// 查询+路径压缩+更新边权
int get(int x)
{
    if (x == fa[x]) return x;
    int root = get(fa[x]);  // 找到根,并不断更新d数组
    d[x] += d[fa[x]];//这是维护距离的情况,如果维护属性那就d[x] ^= d[fa[x]]
    return fa[x] = root;
}
  1. 在合并的时候需要维护d[px] ( 或d[dy] ),这样求得的是px到root的距离
int px = get(x), py = get(y);
fa[px] = py;
d[px] = sz[y]  // 这里的公式需要推导,一般都是d'[x] - d[y] = t => (d[x] + d[px]) - d[y] = t, 然后推出d[px]的式子(x的父节点是px, px的父节点是root)

1.2 扩展域并查集

适用条件: 这样的方法适用于维护一个点只有少数几个属性的情况。同时使用扩展域并查集而不是2-sat的情况是,当前的一个条件可以推出它的4个命题(原命题、逆否命题、逆命题、否命题)。如果只能推出它的2个命题(原命题、逆否命题),那么使用2-sat。异或能够导出4个命题,与/或能导出2个命题。
具体方法: 扩展域就是把原来的点u替换为所有他的属性点,比如说u有3个属性a、b、c,那么就把u点替换为3个点ua,ub,uc。然后现在有i和j两个点,如果说i和j是相同的,那么说明 ia == ja, ib == jb ,ic == jc,即把ia和ja连一条边,ib和jb连一条边,ic和jc连一条边;如果说i吃j,那么把ia和jb连一条边,ib和jc连一条边,ic和ja连一条边。这里的连边表示等价关系。每次操作时,需要先检查所连的边是否是正确的,如果连边出现错误(比如是i和j是相同的,然而ia和jb相连,而相同隐含ia和ja相同,那么ja和jb相同,很明显,j的两个属性点不能相同;同时,检查的时候只需要检查u的一个属性点即可,因为u的所有属性点地位都是等价的),说明本次操作是错误的。强调:这里的连边都为merge操作,即把fa[py]=py,每次检查的本质是判断同一个物体的两个不同属性点是否相同,相同就是错误操作。

1.3 并查集判断奇环

这个方法是建立在:边带权并查集+并查集判断环的基础之上的,具体方法是维护一个d数组(和边带权并查集一样),然后按照并查集判断环的方法,如果px == py, 那么判断d[x] ^ d[y] == 1, 如果成立,那么出现奇环。

2. 例题

2.1 边带权并查集

acwing238. 银河英雄传说
两个操作:
1、M i j,表示让第i号战舰所在列的全部战舰保持原有顺序,接在第j号战舰所在列的尾部。
2、C i j,表示询问第i号战舰与第j号战舰当前是否处于同一列中,如果在同一列中,它们之间间隔了多少艘战舰。

战舰数目N≤30000,指令数目T≤500000

/*
本题要求去距离,因此可以考虑并查集的边带权处理
设d[x]为x到根fa[x]的距离
则每次把集合x和集合y合并时,需要知道y集合的大小,因此需要维护一个size[y]来表示y集合的大小
每次合并时,更新d[x]为size[y]
然后压缩路径
*/

#include 

using namespace std;

int t, n;
char str[3];

int const N = 3e4 + 10;
int fa[N], size[N];
int d[N];  // d[i]表示i到fa[i]的距离

// 查询+路径压缩+更新边权
int get(int x)
{
    if (x == fa[x]) return x;
    int root = get(fa[x]);  // 找到根,并不断更新d数组
    d[x] += d[fa[x]];
    return fa[x] = root;
}

int main()
{
    cin >> t;
    
    // 初始化
    for (int i = 1; i <= 30000; ++i) fa[i] = i, size[i] = 1;
    while (t--)
    {
        int a, b;
        scanf("%s%d%d", str, &a, &b);
        
        // 合并操作
        if (str[0] == 'M')
        {
            a = get(a), b = get(b);
            d[a] = size[b];  // 计算a到b的距离
            size[b] += size[a];  // 更新b的大小
            fa[a] = b;  // 更新a的父节点
        }
        else   // 询问操作
        {
            if (get(a) != get(b)) printf("-1\n");
            else printf("%d\n", max(0, abs(d[a] - d[b]) - 1));
        }
    }
    return 0;
}

acwing239奇偶游戏
小A和小B在玩一个游戏。
首先,小A写了一个由0和1组成的序列S,长度为N。
然后,小B向小A提出了M个问题。
在每个问题中,小B指定两个数 l 和 r,小A回答 S[l~r] 中有奇数个1还是偶数个1。
机智的小B发现小A有可能在撒谎。
例如,小A曾经回答过 S[1~3] 中有奇数个1, S[4~6] 中有偶数个1,现在又回答 S[1~6] 中有偶数个1,显然这是自相矛盾的。
请你帮助小B检查这M个答案,并指出在至少多少个回答之后可以确定小A一定在撒谎。
即求出一个最小的k,使得01序列S满足第 1~k个回答,但不满足第 1~k+1个回答。

//边带权
/*
本题告诉l~r之间的奇数个数,其实是告诉了S[l-1]与S[r]的奇偶关系(S[i]为1~i之间的1的个数)
那么本题就只有2种状态,即0和1;
可以使用边带权的并查集,维护一个d[x]表示x与fa[x]的奇偶关系
如果d[x]为0,那么x与fa[x]的奇偶性相同;否则不同
*/
#include
using namespace std;

int const N = 2e4 + 10;
int cnt = 1;
unordered_map S;
int n, m;
int fa[N], d[N];

// 离散化
int mapping(int x)
{
    if (!S.count(x)) S[x] = cnt++;
    return S[x];
}

int get(int x)
{
    if (x == fa[x]) return x;
    int root = get(fa[x]);
    d[x] ^= d[fa[x]];
    return fa[x] = root;
}

int main()
{
    cin >> n >> m;
    for (int i = 1 ; i <= 2 * m; ++i) fa[i] = i;
    int res = m;
    for (int i = 1; i <= m; ++i)
    {
        int a, b;
        string type;
        cin >> a >> b >> type;
        int t = 0;
        if (type == "odd") t = 1;
        a = mapping(a - 1), b = mapping(b);  // 离散化
        int pa = get(a), pb = get(b);  // 找父节点
        if (pa != pb)  // 父节点不同,更新距离
        {
            fa[pa] = pb;
            d[pa] = d[a] ^ d[b] ^ t;
        }
        else   // 父节点相同,判断是否奇偶性相同
        {
            if (d[a] ^ d[b] != t)
            {
                res = i - 1;
                break;
            }
        }
    }
    cout << res << endl;
    return 0;
}

acwing240食物链
A吃B,B吃C,C吃A,一共有N个动物,给出K个关系,问其中有多少关系是假的。
若D=1,则表示X和Y是同类。
若D=2,则表示X吃Y。
1≤N≤50000,
0≤K≤100000

/* 设置一个共同的起点,d[x]表示x到起点的距离,距离只有%3后才有意义
那么X吃Y就是(d[x] - d[y]) % 3 == 1, X和Y同类就是(d[x] - d[y]) % 3 == 0, 
d的更新关系为:
1.在做get操作时:d[x] += d[fa[x]];
2.在合并时:d[px]= ((t + d[y] - d[x]) % 3 + 3) % 3*/
#include 

using namespace std;

int const N = 5e4 + 10;
int n, k, fa[N], d[N];

int get(int x) {
    if (x == fa[x]) return x;
    int root = get(fa[x]);
    d[x] += d[fa[x]];
    return fa[x] = root;
}

int main() {
    cin >> n >> k;
    int res = 0;
    for (int i = 1; i <= n; ++i) fa[i] = i;
    while (k--) {
        int D, x, y, t = 0;
        scanf("%d %d %d", &D, &x, &y);
        if (x > n || y > n || (D == 2 && x == y)) {
            res ++;
            continue;
        }
        if (D == 2) t = 1;
        int px = get(x), py = get(y);
        if (px == py) {
            if (((d[x] - d[y]) % 3 + 3) % 3 != t) res++;
        }
        else {
            fa[px] = py;
            d[px]= ((t + d[y] - d[x]) % 3 + 3) % 3;
        }
    }
    cout << res << endl;
    return 0;
}

acwing257关押罪犯
现在有n个囚犯,有m对关系,每对囚犯间有c的仇恨值。如果把两个有k仇恨值的囚犯关押在一起,那么就会付出k的代价。现在希望最大的代价最小,问这个代价是多少。

// 二分答案+并查集判断奇环
// 说下并查集判断奇环的思路:如果两个罪犯的仇恨值大于limit,那么两个罪犯之间连一条边
// 然后d[x]维护x到fa[x]的距离的奇偶性,奇数为1,偶数为0
#include 

using namespace std;

int const N = 2e4 + 10, M = 1e5 + 10;
int n, m, fa[N], d[N];
struct Man {
    int u, v, c;
}man[M];

int get(int x) {
    if (x == fa[x]) return x;
    int root = get(fa[x]);
    d[x] ^= d[fa[x]];
    return fa[x] = root;
}

bool merge(int u, int v) {
    int pu = get(u), pv = get(v);
    if (pu == pv && !(d[u] ^ d[v])) return 0;  // 出现奇环

    // 否则,连边
    fa[pu] = pv;  
    d[pu] = 1 ^ d[v] ^ d[u];
    return 1;
}

// 判断是否出现奇环
bool check(int limit) {
    for (int i = 1; i <= m; ++i) 
        if (man[i].c > limit) 
            if (!merge(man[i].v, man[i].u)) return 0;
    return 1;
    
}

int main () {
    cin >> n >> m;
    int l = 0, r = 1e9;
    for (int i = 1; i <= m; ++i) cin >> man[i].u >> man[i].v >> man[i].c;
    while (l < r) {
        int mid = (l + r) >> 1;
        for (int i = 1; i <= n; ++i) fa[i] = i, d[i] = 0;
        if (check(mid)) r = mid;  // 是二分图,说明设置太大
        else l = mid + 1;
    }
    cout << l;
    return 0;
}

2.2 拓展域并查集

acwing240食物链

// 一个点分为几个属性点,每次检查x的不同属性点是否连边,每次合并就把i和j的对应属性点连边
#include 

using namespace std;

int const N = 5e4 + 10;
int fa[N * 3], n, k;

int get(int x) {
    if (fa[x] == x) return x;
    return fa[x] = get(fa[x]);
}

// 检查
bool check(int x, int y) {
    int px = get(x), py = get(y);
    return px == py;
}

// 合并
void merge(int x, int y) {
    int px = get(x), py = get(y);
    fa[px] = py;
}

int main() {
    cin >> n >> k;
    for (int i = 1; i <= n * 3; ++i) fa[i] = i;  // 初始化要3个属性点,因此为3*n
    int res = 0;
    while (k--) {
        int D, x, y, t;
        scanf("%d %d %d", &D, &x, &y);
        if (x > n || y > n || (D == 2 && x == y)) {  // 第一种情况特判
            res++;
            continue;
        }
        if (D == 1) {  // 同类
            if (check(x, y + n) || check(x, y + 2 * n)) res++;  // 检查是否x1和y2,y3连边
            else merge(x, y), merge(x + n, y + n) , merge(x + 2 * n, y + 2 * n);  // 连边
        }
        else {  // x吃y
            if (check(x, y) || check(x, y + 2 * n)) res++;  // 检查x1是否和y1,y3连边
            else merge(x, y + n), merge(x + n, y + 2 * n), merge(x + 2 * n, y);  // 连边
        }
    }
    printf("%d", res);
    return 0;
}

acwing239奇偶游戏

// 这种属性点较少的问题,使用扩展域就会特别简单
#include 

using namespace std;

int const N = 4e4 + 10;
int fa[N], n, k, cnt;
unordered_map mp;

int mapping(int x) {
    if (!mp.count(x)) return mp[x] = ++cnt;
    else return mp[x];
}

int get(int x) {
    if (x == fa[x]) return x;
    return fa[x] = get(fa[x]);
} 

bool check(int x, int y) {
    int px = get(x), py = get(y);
    return px == py;
}

void merge(int x, int y) {
    int px = get(x), py = get(y);
    fa[px] = py;
}

int main () {
    cin >> n >> k;
    for (int i = 1; i <= k * 4; ++i) fa[i] = i;
    int res = k;
    for (int i = 1; i <= k; ++i) {
        int a, b;
        string op;
        cin >> a >> b >> op;
        a = mapping(a - 1), b = mapping(b);
        if (op == "even") {
            if (check(a, b + 2 * k)) {
                res = i - 1;
                break;
            }
            else merge(a, b), merge(a + 2 * k, b + 2 * k);
        }
        else {
            if (check(a, b)) {
                res = i - 1;
                break;
            }
            else merge(a, b + 2 * k), merge(b, a + 2 * k);
        }
    }
    cout << res;
    return 0;
}

acwing257关押罪犯

// 每个囚犯有两个属性,属性1:住在1号房,属性2:住在2号房。因此,可以贪心的思想
// 把囚犯的关系按照仇恨值从大到小排序,先判断仇恨值大的。如果这两个囚犯可以分到2个牢房,那就分到两个牢房
// 即:即囚犯1分到1号房和囚犯2分到2号房连一条边,囚犯1分到2号房和囚犯2分到1号房。
// 判断不合理,那就看是否囚犯1和囚犯2分到同一间房

#include 

using namespace std;

typedef pair PII;
int const N = 4e4 + 10, M = 1e5 + 10;
int n, m, fa[N];
struct Con {
    int u, v, c;
}C[M];

bool cmp(struct Con x, struct Con y) {
    return x.c > y.c;
}

int get(int x) {
    if (x == fa[x]) return x;
    return fa[x] = get(fa[x]);
}

bool check(int u, int v) {
    int pu1 = get(u), pv2 = get(v + n);  // 囚犯1分到1号房,囚犯2分到2号房
    fa[pu1] = pv2;
    
    int pu2 = get(u + n), pv1 = get(v);  // 囚犯1分到2号房,囚犯2分到1号房
    fa[pu2] = pv1;
    
    if (pu1 == pv1 || pu2 == pv2) return 0;  // 判断是否会分到同一间房
    return 1;
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= 2 * n ; ++i) fa[i] = i;
    for (int i = 1; i <= m; ++i) cin >> C[i].u >> C[i].v >> C[i].c;  // 排序
    sort(C + 1, C + 1 + m, cmp);
    int res = 0;
    for (int i = 1; i <= m; ++i) {
        if (!check(C[i].u, C[i].v)) {  // 检查是否合理
            res = C[i].c;
            break;
        }
    }
    cout << res;
    return 0;
}

acwing403平面
    若能将无向图 G=(V,E)画在平面上使得任意两条无重合顶点的边不相交,则称 G 是平面图。现在假设图中存在一个包含所有顶点的环,即存在哈密顿回路。请你判定它们是否是平面图。

/*
本题要求在一个哈密尔顿回路上判断平面图
由于是哈密尔顿回路,一旦发现两条边交叉,那么必然一条边在内侧,一条边在外侧
那么记内侧为0,外侧为1,则i^j=1,则可以使用扩展域并查集处理
m>3n*-6保证了m只能是n的量级,因此去任意两条边,判断这两条边是否为交叉,一旦交叉使用并查集处理
时间复杂度为O(T*m*m)
*/
#include 

using namespace std;

typedef pair PII;
int const N = 200 + 10, M = 1e4 + 10;
int fa[M * 2], n, m, t, id[M * 2];
PII e[M];  // first为a, second为b

int get(int x) {
     if (x == fa[x]) return x;
     return fa[x] = get(fa[x]);
}

// 判断是否交叉
bool isx(int x, int y) {
    int a = id[e[x].first], b = id[e[x].second];  // 转换成顺序标识
    int c = id[e[y].first], d = id[e[y].second];
    if (a > b) swap(a, b);
    if (c > d) swap(c, d);
    if (a == c || a == d || b == c || b == d) return 0;
    return ((a < c && c < b) ^ (a < d && d < b));
}

void merge(int x, int y) {
    int px = get(x), py = get(y);
    if (px != py) fa[px] = py;
}

// 判断是否成立
bool slove() {
    for (int i = 1; i <= m * 2; ++i) fa[i] = i;
    for (int i = 1; i <= m; ++i) {
        for (int j = i + 1; j <= m; ++j) {
            if (isx(i, j)) {  // 判断i和j是否可能交叉
                merge(i, j + m), merge(i + m, j);  // 交叉的话一个在里,一个在外
                if (get(i) == get(i + m)) return 0;  // 一个点的两个属性不能一样
            }
        }
    }
    return 1;
}

int main() {
    cin >> t;
    while (t--) {
        cin >> n >> m;
        for (int i = 1; i <= m; ++i) cin >> e[i].first >> e[i].second;  //记录一条边的两个点
        for (int i = 1, t; i <= n; ++i) {  // 记录每个点
            cin >> t;
            id[t] = i;
        }
        
        if (m > 3 * n - 6 || !slove()) cout << "NO\n";  // m > 3 * n - 6限制了m的量级为n
        else cout << "YES\n";
    }
    return 0;
}

2.3 路径压缩思想运用--从并查集思想出发

acwing145 超市
超市里有N件商品,每件商品都有利润pi和过期时间di,每天只能卖一件商品,过期商品不能再卖。
求合理安排每天卖的商品的情况下,可以得到的最大收益是多少。
0≤N≤10000,1≤pi,di≤10000

// 把商品按利润大小排序,优先考虑利润大的。当对于第i大利润的商品,如果它的过期时间为d,
// 那么它可以出售的时间为前1~d天,即去1~d天中离d最近的、没有出售货物的一天来出售
// 那么需要动态维护每一天d'的前缀1~d'中没有出售过货物的、最近的一天,我们记这天为t
// 而t具有前向传递性,即如果d的t用来出售货物了,那么d的新t为t-1的t。同理,如果t-1的t被出售
// 那么还可以继续向前传递,那么所有可以传递的t就可以变成一条链,这些链可以加一条边来连起来,记fa[d] = t
// 这样当d的t被出售时,我们借鉴线段树懒标记的思想,不去直接把t+1~d-1这些点的t重新标记,而只在t处重新打标记
// 然后每次我求d的t时,不断先前找,找到真正的t = root, 然后修改fa[d] = root.为了加快时间,我们可以把沿途所有的点的父节点都记为root
// 那就变成路径压缩,转而变成并查集的思想
#include 

using namespace std;

int const N = 1e4 + 10;
typedef pair PII;
PII a[N];
int n, fa[N];

int get(int x) {
    if (fa[x] == x) return x;
    return fa[x] = get(fa[x]);
}

int main() {
    while (scanf("%d", &n) != EOF) {
        for (int i = 1; i <= n; ++i) scanf("%d %d", &a[i].first, &a[i].second);  
        
        // 按照利润排序,大的在前
        sort(a + 1, a + n + 1);  
        reverse(a + 1, a + n + 1);
        
        for (int i = 0; i <= N; ++i) fa[i] = i;  // 这里要初始化0,为了让不合法的都压缩到0处
        
        int res = 0;
        for (int i = 1; i <= n; ++i) {
            int x = a[i].second;  // 每次看能不能插入1~x天范围内
            int px = get(x);  // 找到root
            if (px >= 1) res += a[i].first, fa[px] = fa[px - 1];  // 如果root>=1,就路径压缩
        }
        cout << res << endl;
    }
    return 0;
}

你可能感兴趣的:(并查集)