知识总结

并查集

并查集是一种用于管理元素所属集合的数据结构,实现为一个森林,其中每棵树表示一个集合,树中的节点表示对应集合中的元素。

并查集支持两种操作:

  • 合并(Union):合并两个元素所属集合(合并对应的树)。
  • 查询(Find):查询某个元素所属集合(查询对应的树的根节点),这可以用于判断两个元素是否属于同一集合。

初始化

初始时,每个元素都位于一个单独的集合,表示为一棵只有根节点的树。方便起见,我们将每个结点的根结点就设成自己。

for(int i = 1; i <= n; i++)
    a[i] = i;

查询 & 路径压缩

查询元素属于哪个集合需要沿着树向上移动,直至找到根节点。

而由于查询过程中经过的每个元素都属于该集合,我们可将其直接连到根节点以加快后续查询。

int a[maxn]; // a[i] 记录 i 所属的集合编号 
int find(int x){ // 找 x 的集合编号 
    if(a[x] == x) return x; // x 就是本族老大,直接返回 x 
    else return a[x] = find(a[x]);  // 往上找 a[x] 的老大,沿着路径更新 a[x] 所在集合编号
}

非递归版路径压缩

/*非递归版的 int find(int x) */
int find(int x){
        int t = x; // t: 当前查询的结点 
        while(a[x] != x){ // 没到顶点 
                x = a[x]; // 一直找到顶点结束 
        }
        // 路径压缩:沿着初始 x(t暂存) -> 顶点 x 都更新老大为 x
        while(a[t] != t){
                int s = a[t];  // s 暂存 t 的父亲(下个点)
                a[t] = x; // 更新路径上每个点的老大为 x 
                t = s; // 更新当前点为 t 的父亲 s
        } 
        return x;
} 

合并

即合并两棵树,只需要将一棵树的根节点连到另一棵树的根节点。

int fx = find(x), fy = find(y); // fx: x 所属的集合编号, fy: y 所属的集合编号
if(fx != fy){ // 若不是同一个集合的 
    a[fx] = fy; // 合并 x 所在集合和 y 所在集合(用 y 的集合编号更新 x 的老大的集合编号) 
} 

典型例题

亲戚

思路

  1. 初始化每个节点就是自己所在族(集合)的老大。
  2. 根据输入亲戚关系进行集合的合并操作。
  3. 合并完成后查询所问两个元素的祖先结点(所在集合编号)是否相同。

代码

#include <bits/stdc++.h>
#define maxn 5005
using namespace std;

int a[maxn]; // a[i] 记录 i 的族号 

int find(int x){ // 找 x 的族号
        if(a[x] == x) return x; // x 就是本族老大,直接返回 x 
        else return a[x] = find(a[x]);  // 往上找 a[x] 的老大,找到后更新 a[x] 
}

int main(){
        int n, m, p;
        scanf("%d %d %d", &n, &m, &p);
        for(int i = 1; i <= n; i++)
                a[i] = i; // 先初始化单个人为一个族,族号为自己 
        int x, y;
        while(m--){
                scanf("%d %d", &x, &y);
                int fx = find(x), fy = find(y); // fx: x 的族号, fy: y 的族号
                if(fx != fy){ // 若不是同一族的 
                        a[fx] = fy; // 合并 x 所在族和 y 所在族(用 y 的族号更新 x 族老大的族号) 
                } 
        }        
        while(p--){
                scanf("%d %d", &x, &y);
                if(find(x) == find(y)) // 查找 x 和 y 的族号进行匹配 
                        printf("Yes\n");
                else
                        printf("No\n");
        } 
        return 0;
}

村村通

思路

  1. 使用并查集记录集合总数 cnt,初始时 cnt = n,后面每合并两个集合 cnt–。
  2. 如果要想所有村都相通最后应该只有一个集合,因此用最后的集合数 cnt 减去 1 就是还需要连接的道路数。

代码

#include <bits/stdc++.h>
#define maxn 1005
using namespace std;

int a[maxn];

int find(int x){
        if(a[x] == x) return x;
        return a[x] = find(a[x]);
}
int main(){
        int n, m, x, y;
        while(1){
                scanf("%d", &n);
                if(!n) return 0;
                scanf("%d", &m);
                for(int i = 1; i <= n; i++){
                        a[i] = i;
                } 
                int cnt = n; // 初始集合总数为 n 个 
                while(m--){
                        scanf("%d %d", &x, &y);
                        int fx = find(x), fy = find(y);
                        if(fx != fy){
                                a[fx] = fy; // 合并两个集合 
                                cnt--; // 集合总数减 1 
                        }
                }
                printf("%d\n", cnt - 1); // 集合数 - 1 即为所需道路数 
        }
        return 0;
}

木材仓库

思路

  1. 可以使用 STL 中的 set 来实现不同长度木材集合的维护。
  2. 使用 find(x) 在 set 中查询 x 是否存在,insert(x) 在 set 中插入新的 x,empty() 判断集合是否为空。
  3. 查找要取出的木棍,可以用 lower_bound(x) 查询 >= x 的最近元素地址,使用迭代器存其位置再去其前后值比较大小,求出最接近的,用 erase() 删除。

代码

#include <bits/stdc++.h>
using namespace std;

set<int> s;
int main(){
        int n, op, len;
        scanf("%d", &n);
        while(n--){
                scanf("%d %d", &op, &len);
                if(op == 1){
                        if(s.find(len) != s.end()){
                                printf("Already Exist\n");
                        }
                        else{
                                s.insert(len);        
                        }
                }
                else{
                        if(s.empty()){
                                printf("Empty\n");
                        }
                        else{ // 查找要取出的木棍 
                                set<int>::iterator i = s.lower_bound(len), j = i;
                                if(j != s.begin()) j--; // 如果不是首地址,取前个位置
                                // (*j): 小于 x 最近的   (*i) : 大于等于 x 最近的
                                if(i != s.end()){ // 存在 >= x 的数,再进行 (*j) 和 (*i) 的比较 
                                        if(len - (*j) > (*i) - len){ // 后面的 (*i) 更佳 
                                                j = i;  // j 存最佳元素的迭代器 
                                        }
                                } 
                                printf("%d\n", (*j));
                                s.erase(j); // 取出位置 j 的木棍 
                        }
                }
        }
        return 0;
}

集合

思路

  1. 初始 [a, b] 中每个数一个集合,则可把集合总数初始化为 (b – a + 1),后面每进行一次合并操作时,将集合数量减 1 即可。
  2. 用筛法筛出 b 以内所有的质数,枚举所有拥有公共质因数的数(即某个质因数的所有倍数),若这两个倍数在 [a, b] 范围中,且质因数大于等于 p,且两个数不属于同一集合,执行并查集合并操作,集合数量减 1。
  3. 最后输出合并完后的集合数就是答案。

代码

#include<bits/stdc++.h>
#define maxn 100005
using namespace std;

int a, b, p, s, f[maxn];
bool prm[maxn];

int find(int x){
        if(f[x] == x) return x;
        return f[x] = find(f[x]);
}

void is_prm(int b){
        for(int i = 2; i <= b; i++){ // 遍历 b 以内所有可能的质数 
                if(!prm[i]){ // 是质数 
                        for(int j = 2 * i; j <= b; j += i){
                                prm[j] = 1; // 把合数标记成 1 
                                int f1 = find(j), f2 = find(j-i);
                                if(i >= p && j-i >= a && f1 != f2){
                                        f[f1] = f2;
                                        s--;
                                }
                        }
                }
        }
} 

int main(){
        scanf("%d %d %d", &a, &b, &p);
        for(int i = a; i <= b; i++){
                f[i] = i;
        }
        s = b - a + 1; // 初始的集合数 
        // 对 b 以内大于等于 p 的质因数,求出其 b 以内的所有倍数,合并到一个集合 
        is_prm(b);
        cout << s;
        return 0;
}

程序自动分析

思路

  1. 容易想到将所有可以相等的条件放在一个集合内,这样最后判断某个问题是否可被满足时,只需要看有没有不等的条件又属于同一个集合的,即非法情况,一旦出现一组,即可结束判断。
  2. 由于条件编号下标范围很大,直接用大数组是存不下的,因此首先需要对读入的若干约束条件进行离散化编码(映射到一个更小的数据范围),可以先将所有读到过的约束条件编号存入一个数组中,然后进行排序去重,再重新查找元素下标进行编号,这样编码范围最多 2 * n 个。

代码

#include <bits/stdc++.h>
#define maxn 1000005 
using namespace std;

int n, t, f[2 * maxn], b[maxn * 2];

struct node{
        int x, y, e;
}a[maxn];

bool cmp(node a, node b){
        return a.e > b.e; // 把 e 为 1 的操作先放前面统一处理 
}

int find(int x){
        if(f[x] == x) return x; // x 就是祖先,直接返回它的值
        return f[x] = find(f[x]); // 用找到的 f[x] 的祖先更新 x 的爸爸 
}

int main(){
        scanf("%d", &t);
        while(t--){
                scanf("%d", &n);
                int s = 0; // 计数        X 条件个数 
                for(int i = 1; i <= n; i++){
                        scanf("%d %d %d", &a[i].x, &a[i].y, &a[i].e);
                        b[s++] = a[i].x;
                        b[s++] = a[i].y;
                }
                sort(b, b + s); // 将条件排序
                int k = unique(b, b + s) - b; // 更新去重后的元素个数 
                for(int i = 1; i <= n; i++){
                        a[i].x = lower_bound(b, b + k, a[i].x) - b; // 求出 a[i].x 离散化后的位置
                        a[i].y = lower_bound(b, b + k, a[i].y) - b; // 求出 a[i].x 离散化后的位置
                }
                // 并查集,合并相等的项到同一个集合
                for(int i = 0; i < k; i++){
                        f[i] = i; // 先初始化 i 就是自己的爸爸 
                } 
                sort(a + 1, a + 1 + n, cmp); // 将 e = 1 的项排序在前面
                bool flag = 1; // 假设条件都满足
                for(int i = 1; i <= n; i++){
                        int fx = find(a[i].x), fy = find(a[i].y);
                        if(a[i].e){ // Xx = Xy 的情况 
                                if(fx != fy){ // 若祖先不同 
                                        f[fx] = fy; // 合并到 fy 所在集合 
                                }
                        }
                        else{ // Xx != Xy 的情况 
                                if(fx == fy){ // 发现同属一个集合,不合理 
                                        flag = 0;  // 标记非法 
                                        break; // 结束该组数据判断 
                                }
                        } 
                } 
                if(flag) printf("YES\n");
                else printf("NO\n");
        }        
        return 0;
}

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注