【并查集】练习题以及解答

核心代码(C++版本)

const int MAXNUM = 500000 + 10;
int id[MAXNUM];
int Size[MAXNUM];
int Count;
// 初始化
void make_set(int n){
    for(int i = 0 ; i < n; i++){
        id[i] = i;
        Size[i] = 1;
    }
    Count = n;
}

// 查找父节点
int Find(int p) {
    while (p != id[p]) {
        // 路径压缩,会破坏掉当前节点的父节点的尺寸信息,因为压缩后,当前节点的父节点已经变了
        id[p] = id[id[p]];
        p = id[p];
    }
    return p;
}

// 合并 p ,q节点
void Union(int p, int q) {
    int pRoot = Find(p);
    int qRoot = Find(q);

    if (pRoot == qRoot) {
        vaild = false;
        return;
    }
    // 按秩进行合并
    if (Size[pRoot] > Size[qRoot]) {
        id[qRoot] = pRoot;
        Size[pRoot] += Size[qRoot];
    } else {
        id[pRoot] = qRoot;
        Size[qRoot] += Size[pRoot];
    }
    // 每次合并之后,树的数量减1
    Count--;
}

// 森林中树的数量
int TreeNumber() {
    return Count;
}

HDU 1213 : How Many Tables

模版题,题意是问Ignatius生日派对上需要准备多少张桌子,规则是相互认识的朋友坐在一桌上,一共n个人,有m种关系。
也就是求出并查集中的树的个数。

HDU 1232 : 畅通工程

由题意是求至少需要加上多少条边使得每个城市都能与其他城市相通(至少有一条路径到达)。在已知的道路中可以形成并查集里的树,每棵树内每个城市都至少有一条道路能到达其他城市。此时可以看出所求的边数就是森林中树的个数减去1,也就是Count-1。

POJ 2524 : Ubiquitous Religions

题目求大学里有多少不同宗教的学生,每个学生最多只能申请一种宗教。许多学生不愿意表达自己的信念。 避免这些问题的一种方法是询问m(0 <= m <= n(n-1)/ 2)对学生,并询问他们是否相信同一个宗教。这里就是求并查集中树的个数,其中【询问他们是否相信同一个宗教,而不是问具体哪个宗教】,也就是注重点在于哪颗树,而不是树的序列号,这样我们直接计算树的数量即可。

POJ 1611 : The Suspects

题意:题意假设0号学生是传染非典的人,然后给出m组人的情况,在同一组都会被认为是传染非典的人,求传染非典的人数。实际上就是将同一个组通过并查集的【并】操作合并在一起,然后计算出和0号学生同一组的人数,也就是Size[Find(0)]的值。

POJ 2236 : Wireless Network

网络中的所有计算机全部断开。
(1)由于硬件限制,每台计算机只能直接与距离它不远的计算机进行通信
(2)如果计算机A和计算机B可以直接通信,或者计算机C可以同时与A和B通信B
O操作是修复第x号计算机,
S操作是测试x,y是否能够通信

解答:保存每个计算机的座标,对于O操作,表示修复X节点,则判断X和之前修复的节点的距离是否小于等于d,如果是则连接X和该节点,其中已经修复的节点使用map保存,然后之后遍历map即可。对于S操作,直接判断Find(X)==Find(Y).

LeetCode 128 : Longest Consecutive Sequence

求最长连续序列的长度。
Input: [100, 4, 200, 1, 3, 2]
Output: 4
Explanation: The longest consecutive elements sequence is [1, 2, 3, 4]. Therefore its length is 4.
在序列a[n]中,对于每一个a[i],检查a[i]-1和a[i]+1,这里使用map直接对应,如果存在则通过并查集的Union操作合并在一起。最终检查Size数组的最大值,或者通过中间变量Max时刻记录。

for(int i = 0 ; i < n ; i ++){
        scanf("%d", &a[i]);
        // 标记
        mp[a[i]] = 1;
        if(mp.find(a[i] + 1)!= mp.end())
            Union(a[i], a[i] + 1);
        if(mp.find(a[i] - 1)!= mp.end())
            Union(a[i], a[i] - 1);
    }
// 合并 p ,q节点
void Union(int p, int q) {
    int pRoot = Find(p);
    int qRoot = Find(q);

    if (pRoot == qRoot) {
        return;
    }
    // 按秩进行合并
    if (Size[pRoot] > Size[qRoot]) {
        id[qRoot] = pRoot;
        Size[pRoot] += Size[qRoot];
        if (Size[pRoot] > Max) {    // 如果合并后的树的秩比当前最大秩还要大,替换之
            Max = Size[pRoot];
        }
    } else {
        id[pRoot] = qRoot;
        Size[qRoot] += Size[pRoot];
        if (Size[qRoot] > Max) {    // 如果合并后的树的秩比当前最大秩还要大,替换之
            Max = Size[qRoot];
        }
    }
    // 每次合并之后,树的数量减1
    Count--;
}

LeetCode 200 : Number of Islands

题意:在一个平面上,1代表是陆地,0代表的是海水,相邻的1看作为同一陆地,求一共有多少块陆地。
(1)由于是一个二维的情况,但是每行都是m个字符,这样可以通过数值转为一维数组去处理,并计算陆地开始量,这是初始化过程。
(2)对于当前的s[i][j]。如果是0则忽略;如果是1,则判断其上下左右的是不是1,如果也是1,则合并到同一颗树下
(3)计算出并查集中最终剩下的树的数量。

// 初始化
void make_set(int n , int m){
    Count = 0 ;
    for(int i = 0 ; i < n ; i++){
        for(int j = 0 ; j < m ; j++){
            if(s[i][j] == '1')
                Count++;
            id[i * m + j] = i * m + j;
        }
    }
}
// 判断过程
for(int i = 0 ; i < n ; i++){
        for(int j = 0 ; j < m ; j++){
            if(s[i][j] =='0')
                continue;

            if(i + 1 < n && s[i + 1][j] == '1')
                Union(i * m + j , (i + 1) * m + j);

            if(i - 1 >= 0 && s[i - 1][j] == '1')
                Union(i * m + j , (i - 1) * m + j);

            if(j + 1 < m && s[i][j + 1] == '1')
                Union(i * m + j,i * m + j + 1);

            if(j - 1 >= 0 && s[i][j - 1] == '1')
                Union(i * m + j,i * m + j - 1);

 }

LeetCode 130 : Surrounded Regions

在平面上,只有’X’和’O’,给出初始的图,求最终的图,规则是如果’O’被’X’包围,则被包围的’O’都变成’X’。
(1)所有相连的’O’加入一个集合
1。如果’O’被’X’包围,那么所有相连的’O’行程一个独立的集合
2。如果相连的多个’O’最后在边界没有被’X’包围,那么他们都加入到一个孤立的isolateUnion中
(2)将不在isolateUnion中的所有’O’置换为’X’

// 初始化
void make_set(int n , int m){
    Count = 0 ;
    for(int i = 0 ; i < n ; i++){
        for(int j = 0 ; j < m ; j++){
            if(s[i][j] == 'O')
                Count++;
            id[i * m + j] = i * m + j;
        }
    }
    // 被孤立那一个
    id[n * m] = n * m;
}
// 不被包围的isolateUnion
int isolateUnion = n * m;

for(int i = 0 ; i < n ; i++){
    for(int j = 0 ; j < m ; j++){
        if(s[i][j] =='X')
            continue;

        int cur = i * m + j ;

        // 边缘
        if(i == 0 || j ==0 || i == n - 1 || j == m - 1){
            Union(cur, isolateUnion);
        }else{
            // 相邻
            if(i > 0 && s[i-1][j] == 'O'){
                Union(cur,cur - m);
            }

            if(i < n-1 && s[i+1][j] == 'O'){
                Union(cur,cur + m);
            }

            if(j > 0 && s[i][j-1] == 'O'){
                Union(cur,cur - 1);
            }

            if(j < m-1 && s[i][j+1] == 'O'){
                Union(cur,cur + 1);
            }
        }

    }
}
// 将被包围的'O'变成'X'
for(int i = 0 ; i < n ; i++){
    for(int j = 0 ; j < m ; j++){
        // 不在边缘区的
        if(Find(i * m + j) != Find(isolateUnion))
            s[i][j] = 'X';
    }
}
// 打印
for(int i = 0 ; i < n ; i++){
    for(int j = 0 ; j < m ; j++){
        printf("%c",s[i][j]);
    }
    printf("\n");
}

LeetCode 684 : Redundant Connection

求出【无向图】形成环的边,如果存在多条,则输出最后形成环的那条。题目保证数据必然会形成环。
按照常规的并查集操作,然后特判下当Find(i)结果相同时,即为形成环的条件,则记录点ansp,ansq,最终输出时使用。

vector<int> findRedundantConnection(vector<vector<int>>& edges) {
    int n = edges.size();
    make_set(n);
    // 保存答案
    int ansp, ansq;
    for(int i = 0 ; i < n ; i++){
        vector<int> te = edges[i];
        int p = te[0];
        int q = te[1];
        // 形成环则记录下来
        if(Find(p) == Find(q)){
            ansp = p;
            ansq = q;
        }else{ // 不会形成环则合并起来
            Union(p ,q);
        }
    }
    vector<int> ans;
    ans.push_back(ansp);
    ans.push_back(ansq);
    return ans;
}

LeetCode 685 : Redundant Connection II

相比于684,从【无向图】改为【有向图】。在这种情况下需要改动的有
(1)有向图不能使用秩合并,直接合并id[qRoot] = pRoot
(2)有向图需要判断图是不是一颗合法的树,这个需要判断所有点的入度是否是小于2。
(3)由于题意说明入度最多为2,则需要判断那个入度为2的点应该去除哪条边,只需要在其他边合并后在判断这两条边加入到树时会不会形成环则可以判断出应该去除哪条。

vector<int> findRedundantDirectedConnection(vector<vector<int> >& edges) {

    int n = edges.size();

    int ansp, ansq;
    bool vaild = true;
    int Point ;
    for(int i = 0 ; i < n ; i++){
        vector<int> te = edges[i];
        int q = te[1];
        // 入度加1
        indegree[q]++;
        // 找出入度为2的边,记录在Point
        if(indegree[q] >= 2)
            Point = q, vaild = false;
    }

    make_set(n);
    // 不存在入度为2的边
    if(vaild){
        for(int i = 0 ; i < n ; i++){
            vector<int> te = edges[i];
            int p = te[0];
            int q = te[1];
            if(Find(p) == Find(q)){
                ansp = p;
                ansq = q;
            }else{
                Union(p ,q);
            }
        }
    }else{ //存在入度为2的边
        // 记录连接Point的两个点pp[0],pp[1]
        int pp[2], cnt = 0;
        for(int i = 0 ; i < n ; i++){
            vector<int> te = edges[i];
            int p = te[0];
            int q = te[1];
            if(q == Point){
                pp[cnt++] = p;
                continue;
            }
            Union(p ,q);
        }
        if(Find(pp[0]) == Find(Point)){
            ansp = pp[0];
        }else{
            ansp = pp[1];
        }
        ansq = Point;
    }
    vector<int> ans;
    ans.push_back(ansp);
    ans.push_back(ansq);
    return ans;
}

HDU 3635 : Dragon Balls

题意:起初球i是被放在i号城市的,在年代更迭,世事变迁的情况下,球被转移了,而且转移的时候,连带该城市的所有球都被移动了:T A B(A球所在的城市的所有球都被移动到了B球所在的城市),Q A(问:A球在那城市?A球所在城市有多少个球呢?A球被转移了多少次呢?)
(1)由题意可知,不能按照秩合并,只能直接合并

// 合并 p ,q节点
void Union(int p, int q) {
    int pRoot = Find(p);
    int qRoot = Find(q);

    if (pRoot == qRoot) {
         return;
    }

    // 不能进行按秩合并,且在合并时,对第一个球的转移次数进行递增
    id[pRoot] = qRoot;
    trans[pRoot]++;
    Size[qRoot] += Size[pRoot];

    // 每次合并之后,树的数量减1
    Count--;
}

(2)路径压缩,需要添加第三个数组辅助记录转化次数,初始化都为0

// 查找父节点
int Find(int p) {
     while (id[p] != id[id[p]]) {   //如果q不是其所在子树的根节点的直接孩子
         trans[p] += trans[id[p]];   //更新trans数组,将q的父节点的转移数添加到q的转移数中
         id[p] = id[id[p]];          //对其父节点到其爷爷节点之间的路径进行压缩
      }
    return id[p];
}

而Q p时,先进行路径压缩Find(p),然后答案即为根节点id[p], 树的大小Size[id[p]],转移次数trans[p]。

POJ 1988 : Cube Stacking

这里包含两种操作,一个是将立方体X移动到立方体Y的栈顶,一个是计算立方体X所处的位置一共有多少个立方体。
这个和上面的HDU 3635比较相似,不同之处在于一个是求转移次数,另外一个是求所处的数量和。

// 合并 p ,q节点
void Union(int p, int q) {
    int pRoot = Find(p);
    int qRoot = Find(q);

    if (pRoot == qRoot) {
        return;
    }

    // 不能进行按秩合并
    id[pRoot] = qRoot;
    // p处记录相应的数量
    trans[pRoot] = Size[qRoot];
    // q处的数量相应得加到Size数组
    Size[qRoot] += Size[pRoot];
}

HDU 1856 : More is better

计算森林中所有树的秩的最大值。利用路径压缩和按照秩合并即可。用变量记录最大值。
(1)数据比较大,需要进行离散化,用map实现。
(2)当n=1时,应返回1.

// 合并 p ,q节点
void Union(int p, int q) {
    int pRoot = Find(p);
    int qRoot = Find(q);

    if (pRoot == qRoot) {
        return;
    }
    // 按秩进行合并
    if (Size[pRoot] > Size[qRoot]) {
        id[qRoot] = pRoot;
        Size[pRoot] += Size[qRoot];
        if (Size[pRoot] > Max) {    // 如果合并后的树的秩比当前最大秩还要大,替换之
            Max = Size[pRoot];
        }
    } else {
        id[pRoot] = qRoot;
        Size[qRoot] += Size[pRoot];
        if (Size[qRoot] > Max) {    // 如果合并后的树的秩比当前最大秩还要大,替换之
            Max = Size[qRoot];
        }
    }
    // 每次合并之后,树的数量减1
    Count--;
}

HDU 1272 : 小希的迷宫

HDU 1325 : Is It A Tree?

这两个题判断给出的图是不是一棵树。前者是无向图,后者是有向图。
对于图判断是否是树
(1)不存在环(对于有向图,不存在环路也就意味着不存在强连通子图)
(2)边的个数加上1等于顶点个数。即为edges + 1 = vertex。
(3)【有向图】还需要所有点的入度小于2。

第一条,在并查集中应该如何实现呢?
现在我们对并查集也有一定的认识了,其实很容易我们就能够想出,当两个顶点的根节点相同时,就代表添加了这一条边后会出现环路。这很好解释,如果两个顶点的根节点是相同的,代表这两个顶点已经是连通的了,对于已经连通的两个顶点,再添加一条边,必然会产生环路。

第二条呢?
图中的边数,我们可以在每次进行真正合并操作之前(也就是,在确认两个待合并的顶点的根节点不相同时)进行记录。然后顶点数,也就是整个合并过程中参与进来的顶点个数了,可以使用一个布尔数组来进行记录,出现后将相应位置设为true,最后进行一轮统计即可。

需要注意的点:
(1)空树是Yes的情况
(2)此时无法按照秩合并。

// 合并 p ,q节点
void Union(int p, int q) {
    int pRoot = Find(p);
    int qRoot = Find(q);

    if (pRoot == qRoot) {
        vaild = false;
        return;
    }
    mark[p] = true;
    mark[q] = true;   // p和q参与到最后的顶点数量的统计
    edges++;   // 在合并之前,将边的数量递增
    id[qRoot] = pRoot;
}
点赞