算法专题 | 并查集

记录连通块的某种和(累加祖宗的值)

如计算每个连通块的v[i]的和
见AcWing1252.搭配购买 AcWing837.连通块中点的个数

并查集维护与祖宗距离详解

在这里插入图片描述
说明
p[x]的值在计算距离之前必须先要find(p[x])一遍
因为d[x]要变成从x到根节点的距离,所以在执行d[x] += d[p[x]]之前,要先将d[p[x]]变成p[x]到根节点的距离

find递归语句写在最前面,在做这个操作: 按距离根节点从近到远的顺序,把这条x到根节点路径上的点的距离都更新一遍

并查集 1

一些细节问题:并查集一般用一维坐标比较方便,所以如果是二维点阵n * m 判断是否有环,可以先转换为一维坐标(x,y) -> x * n + y

int get(x,y) {
return x *n + y;
}

如:AcWing 1250. 格子游戏

一.并查集—合并+查找
并查集两个操作: 近乎0(1)时间复杂度内快速维护这两个操作

  1. 将两个集合合并
  2. 询问两个元素是在一个集合中
  • 维护每个集合中元素的个数 见连通块中的点的个数
  • 维护每个集合中元素与根节点的关系 见食物链

并查集的最本质是find函数,记这个就行了

二.基本原理
每个集合用一棵树来表示。树根的编号就是整棵树的编号。每个节点储存它的父节点,p[x]表示x的父节点
在这里插入图片描述
问题1. 如何判断树根:if(p[x] == x)
问题2. 如何求x的集合标号: while(p[x] != x) x = p[x];
问题3. 如何合并两个集合: p[find(x)] = find(y)

优化-路径压缩
我找一次后,所有路径上的点指向跟节点

第一次求要三次,第二次就只需要一次

模版题 AcWing 836.合并集合

版本1 简练

#include <iostream>
#include <cstdio>
#include <vector>

using namespace std;

const int N = 100010;

int n, m;
int p[N];

int find(int x)
{ // 返回x的祖宗节点 + 路径压缩
    if (p[x] != x)
        p[x] = find(p[x]); // 写find(p[x])这样才会递归下去
    return p[x];
}

int main() {
    string c; // 用string, 不会遇到M,Q1,Q2这种询问的麻烦
    int a, b;
    cin >> n >> m;
    
    for (int i = 1; i <= n; i ++) {
        p[i] = i;
    }
    
    for (int i = 0; i < m; i ++) {
        cin >> c >> a >> b;
        int pa = find(a), pb = find(b); // 祖宗点
        if (c == "M") p[pa] = pb;  // union操作 合并 
        // p[pb] = pa;也可以,影响不大,因为只要a和b的祖宗结点相同即可
        else {
            if (pa == pb)
                cout << "Yes" << endl;
            else cout << "No" << endl;
        }
    }
    return 0;
}

版本2 用了UnionFind类 vector初始化遇到0比较麻烦

#include <iostream>
#include <cstdio>
#include <vector>

using namespace std;

class UnionFind {
public:
    vector<int> father;

    UnionFind(int num) {
        for (int i = 0; i <= num; i ++) {
            father.push_back(i);
        }
    }

    int Find(int n) {
        if (father[n] == n) return n;
        father[n] = Find(father[n]);
        return father[n];
    }

    void Union(int a, int b) {
        int fa = Find(a);
        int fb = Find(b);
        father[fb] = fa;
    }
};


int main() {
    int n, m;
    char c;
    int a, b;
    cin >> n >> m;
    UnionFind UF(n);
    for (int i = 0; i < m; i ++) {
        cin >> c >> a >> b;
        if (c == 'M') UF.Union(a, b);
        else {
            if (UF.Find(a) == UF.Find(b)) {
                cout << "Yes" << endl;
            } else {
                cout << "No" << endl;
            }
        }
    }
    return 0;
}

与祖宗的关系

并查集维护与祖宗距离详解

在这里插入图片描述

*238.银河英雄传说(维护与祖宗的距离)

在这里插入图片描述

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

using namespace std;

const int N = 30010;

int m;
int p[N], size[N], d[N];

int find(int x)
{
    if (p[x] != x)
    {
        int root = find(p[x]); // 递归到底
        d[x] += d[p[x]]; // 从底往回,更新距离
        p[x] = root; // 压缩路径
    }
    return p[x];
}

int main()
{
    scanf("%d", &m);

    for (int i = 1; i < N; i ++ )
    {
        p[i] = i;
        size[i] = 1;
    }

    while (m -- )
    {
        char op[2]; // 用scanf读单个字符的巧妙写法
        int a, b;
        scanf("%s%d%d", op, &a, &b);
        if (op[0] == 'M')
        {
            int pa = find(a), pb = find(b);
            d[pa] = size[pb];
            size[pb] += size[pa];
            p[pa] = pb;
        }
        else
        {
            int pa = find(a), pb = find(b); 
            // 必须find一次,才能把d[a],d[b]更新到最新
            if (pa != pb) puts("-1");
            else printf("%d\n", max(0, abs(d[a] - d[b]) - 1));
        }
    }

    return 0;
}

AcWing 837.连通块中点的个数(累加祖宗上的值)

// 三种操作都包含了——特别是维护每个集合的个数
#include <iostream>
#include <cstdio>
#include <vector>

using namespace std;

const int N = 100010;

int n, m;
int p[N], size[N]; 
// 只记录根结点的size

int find(int x) {  // 返回x的祖宗节点 + 路径压缩
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main() {
    string c; // 用string, 不会遇到M,Q1,Q2这种询问的麻烦
    int a, b;
    cin >> n >> m;

    // 初始化
    for (int i = 1; i <= n; i ++) {
        p[i] = i;
        size[i] = 1;
    }

    // 分三种情况
    for (int i = 0; i < m; i ++) {
        cin >> c;
        if (c == "C") {
            cin >> a >> b;
            if (find(a) != find(b)) {   // 如果已经连通了,就不用再加结点数了,因为我们只看根的size
                size[find(b)] += size[find(a)];
            }

            p[find(a)] = find(b);  // union操作 合并
        } else if (c == "Q1") {
            cin >> a >> b;
            if (find(a) == find(b)) puts("Yes");
            else puts("No");
        } else {
            cin >> a;
            cout << size[find(a)] << endl;
        }
    }
    return 0;
}

*AcWing1252.搭配购买(累加祖宗上的值)

在这里插入图片描述

/**
 *    @Author: Wilson79
 *    @Datetime: 2019年12月20日 星期五 15:21:04
 *    @Filename: 1252.搭配购买(并查集).cpp
 */

#include <iostream>

using namespace std;

const int N = 10005;

int n, m, vol;
int p[N], v[N], w[N], f[N];
int a, b;

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

int main() {
    cin >> n >> m >> vol;
    for (int i = 1; i <= n; i ++) {
        p[i] = i;
    }

    for (int i = 1; i <= n; i ++) {
        cin >> v[i] >> w[i];
    }

    // 计算每个连通块的v和w 最终一定连通块的祖宗一定满足p[x] == x
    for (int i = 1; i <= m; i ++) {
        cin >> a >> b;
        int pa = find(a), pb = find(b);

        if (pa != pb) {
            v[pb] += v[pa]; // 累计每个祖宗结点的值
            w[pb] += w[pa];
            p[pa] = pb;
        }
    }

    // 01背包模版
    for (int i = 1; i <= n; i ++) {
        if(p[i] == i)
            for (int j = vol; j >= v[i]; j --) {
                f[j] = max(f[j - v[i]] + w[i], f[j]);
            }
    }

    cout << f[vol] << endl;

    return 0;
}

AcWing 1250.格子游戏(二维点阵)

在这里插入图片描述

// 2019年12月20日 星期五 15:35:09
#include <iostream>

using namespace std;

const int N = 210;

int p[N * N];
int n, m;
int x, y, a, b;
char c;


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

int main() {
    cin >> n >> m;

    for (int i = 0; i < n * n; i ++) {
        p[i] = i;
    }

    int i;
    for (i = 1; i <= m; i ++) {
        cin >> x >> y >> c;
        x--, y--;
        a = x * n + y;
        if (c == 'R') {
            b = x * n + y + 1;
        } else {
            b = (x + 1) * n + y;
        }

        if (find(a) != find(b)) {
            p[find(a)] = find(b);
        } else {
            cout << i << endl;
            break;
        }
    }

    if (i > m) cout << "draw" << endl;

    return 0;
}

AcWing 240.食物链(记录与根节点的关系)

在这里插入图片描述

/*
	0->1->2->0  0是根节点

	
	距离d:记录当前节点到根节点的距离
	余1:可以吃根节点
	余2:可以被根节点吃 x
	余0:与根节点是同类 y

	x吃y:x - y mod 3 为1
	x被y吃:x - y mod 3 为2
*/
#include <iostream>

using namespace std;

const int N = 50010;

int n, m;
int p[N], d[N];

int find(int x)
{
    if (p[x] != x)
    {
        int t = find(p[x]);  // 递归语句必须写在前面,这样可以保证你先到达了根节点,然后倒过来一步步执行后面的语句
        // 因为p[a] = b, p[b] = c;你合并了三棵树,这是你再取a树中的点,find(x)它的d[p[x]]需要先更新,因为d[p[x]]还存的是x到a的距离
        d[x] += d[p[x]];
        p[x] = t;
    }
    return p[x];
}

// int find(int x) {
// 	if (p[x] != x) {
// 		d[x] += d[p[x]];  // 这样写距离会变短,比如你Union了三棵树 d[p[x]]一直没被更新  这个问题我想了很久才搞明白,每次碰到递归就头疼
// 		p[x] = find(p[x]);
// 	}
// 	return p[x];
// }

int main() {
    cin >> n >> m;

    for (int i = 1; i <= n; i ++) {
    	p[i] = i;
    } 

    int res = 0;
    while (m --) {
    	int t, x, y;
    	cin >> t >> x >> y;

    	if (x > n || y > n) res ++;
    	else {
    		int px = find(x), py = find(y);
    		if (t == 1) {  // 判断是否同类
    			if (px == py && (d[x] - d[y]) % 3= 0) res ++;
    			else if (px != py) {
    				p[px] = py;
    				d[px] = d[y] - d[x];
    			}
    		} else { // t == 2 给出谁吃谁
    			if  (px == py && (d[x] - d[y] - 1) % 3= 0) res ++;
    			else if (px != py) {           // 不是一个集合,因此需合并
    				p[px] = py;
    				d[px] = d[y] - d[x] + 1;   // (d[x] + ? - d[y]) mod 3 == 1
    			}
    		}
    	}	
    } 

    cout << res << endl;

    return 0;
}

LeetCode 765.情侣牵手(并查集)

在这里插入图片描述

/**
 *    @Author: Wilson79
 *    @Datetime: 2019年12月22日 星期日 00:23:24
 *    @Filename: 765.情侣牵手(并查集).cpp
 */

/*  ====算法分析====
    并查集 
    1.首先初始状态把这N对情侣分别构成一个连通块
    2.考虑k对情侣相互错开的情况,他们形成一个环,可以知道需k-1次交换使排列正确
    3.这样相互错开的情况,分别构成连通块,最后用N - 连通块个数即为答案
    例如:0,1|2,3|... |2N-2,2N-1
    |0 3| ... |7 2|...|6 1| 看相对顺序,可以发现这三对构成一个环,只需2次交换
    同理还有其他类型的环构成连通块
*/

const int N = 80;
int p[N];

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

class Solution {
public:
    int minSwapsCouples(vector<int>& row) {
        int n = row.size();

        // 初始化p[x]数组
        for (int i = 0; i < n; i += 2) {
            p[i + 1] = i;
            p[i] = i;   
        }

        // |0 3| ... |7 2|...|6 1| 合并0,3   7,2  6,1 最终这两个人到同一连通块
        for (int i = 0; i < n; i += 2) {
            p[find(row[i + 1])] = find(row[i]);  
        }

        int res = n / 2;
        for (int i = 0; i <= n; i ++) {
            if (p[i] == i) res --;
        }

        return res;
    }
};

1319.连通网络的操作次数

在这里插入图片描述

class Solution {
public:
    static const int N = 1e5 + 7;
    int p[N];
    int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    
    int makeConnected(int n, vector<vector<int>>& connections) {
        int Size = connections.size();

        for (int i = 0; i < n; i++) {
            p[i] = i;
        }

        int cnt = 0;
        for (int i = 0; i < Size; i ++) {
            int a = connections[i][0], b = connections[i][1];
            int pa = find(a), pb = find(b);
            if (pa == pb) cnt ++; // 多余的线
            else p[pa] = pb;
        }

        // 算出连通块的个数,用多余的线去连
        int connect = 0;
        for (int i = 0; i < n; i ++) {
            if (p[i] == i) connect ++;
        }

        if (connect - cnt > 1) return -1;
        
        return connect - 1;
    }
};

并查集 2

并查集是面试笔试和竞赛中非常常用的一个数据结构算法,因为它代码比较精简,但思维含量高

并查集两个操作: 近乎0(1)时间复杂度内快速维护这两个操作

  • 将两个集合合并
  • 询问两个元素是在一个集合中

一:并union 查 find
判断两个元素是不是在同一个集合就看他们的老大是不是同一个。
1,2; 3,4;两个集合 2和4合并需要选出一个新的老大。
在这里插入图片描述
4的原老大是3,现在他最大的老大是1,相当于他原来是个小公司被大公司合并了。所以这个小公司的所有人都要服从于最后的大老板
所以判断一个元素是不是老大就看它的箭头是不是指向它自己。

二:find和union
find:一直顺着箭头找,直到找到最大的boss
union:先找两个元素的老大,再选一个作为最终的老大

三:路径压缩
第一次一步一步找到了最终boss,那么此时建立一个直接到最终boss的箭头,那之后要做查询时就不需要再通过boss的boss的boss一步步找到最终boss了,相当于你直接和最终boss建立了联系。 这就是路径压缩
在这里插入图片描述

并查集模板


class UnionFind {
public:
    vector<int> father;
    UnionFind(int num) {
        for (int i = 0; i < num; i ++) {
            father.push_back(i);
        }
    }

    int Find(int n) {
        if(father[n] == n) return n;
        father[n] = Find(father[n]);
        return father[n];
    }

    bool Union(int a, int b) {
        int fa = Find(a);
        int fb = Find(b);
        father[fb] = fa;
        return fa == fb;   // 判断两个点是否连通
    }
};

示例代码

class UnionFind {
public:
    vector<int> father;
    // num表示元素个数
    // 这里的UnionFind定义在这里其实是为了方便初始化并查集
    // 如UnionFind UF(n);
    UnionFind(int num) {
        for (int i = 0; i < num; i ++) {
            father.push_back(i); // 箭头指向自己
        }
    }

    // 4->3->1->1 finish
    int Find(int n) {
        // 非递归
        while(father[n] != n) {
            n = father[n];
        }
        return n;
    }

    int Find(int n) {
        // 递归
        if (father[n] == n) return n;
        return Find(father[n]);
    }

    // 不仅返回了4的最终boss,还让4和1建立了一个联系,方便下次直接找1
    int Find(int n) {
        // 递归 + 路径压缩
        if(father[n] == n) return n;
        father[n] = Find(father[n]);
        return father[n];  // 这里改为Find(father[n])也是可以的,但是会多占用很多空间
    }

    void Union(int a, int b) {
        int fa = Find(a); // 2的原老大是1
        int fb = Find(b); // 4的原老大是3
        father[fb] = fa;   // 让3指向1
    }
};

LeetCode 547.FriendCircles


//  版本一 UnionFind初始化
class UnionFind {
public:
    vector<int> father;
    UnionFind(int num) {
        for (int i = 0; i < num; i ++) {
            father.push_back(i);
        }
    }

    int Find(int n) {
        // 递归 + 路径压缩
        if(father[n] == n) return n;
        father[n] = Find(father[n]);
        return father[n];
    }

    void Union(int a, int b) {
        int fa = Find(a); // 2的原老大是1
        int fb = Find(b); // 4的原老大是3
        father[fb] = fa;   // 让3指向1
    }
};


class Solution {
public:
    int findCircleNum(vector<vector<int>> &M) {
        int n = M.size();
        UnionFind UF(n); 
        for (int i = 0; i < n; i ++) {
            for (int j = 0; j < n; j ++) {
                if (M[i][j] == 1) UF.Union(i, j);
            }
        }

        int res = 0;
        for (int i = 0; i < n; i ++) {
            if (UF.Find(i) == i) {
                res ++;
            }
        }

        return res;
    }
};

LeetCode 200.NumberOfIslands

// 并查集
// 向四个方向进行Union操作,注意两个参数的先后,后面的服从前面的
class UnionFind {
public:
    vector<int> father;
    UnionFind(int num) {
        for (int i = 0; i < num; i ++) {
            father.push_back(i);
        }
    }

    int Find(int n) {
        if (father[n] == n) return n;
        father[n] = Find(father[n]);
        return father[n];
    }

    void Union(int a, int b) {
        int fa = Find(a);
        int fb = Find(b);
        father[fb] = fa;
    }
};

int encode(int i, int j, int m) {
    return i * m + j;
}

class Solution {
public:
    int numIslands(vector<vector<char>> &grid) {
        if (!grid.size() || !grid[0].size()) return 0;
        int n = grid.size(), m = grid[0].size();

        UnionFind UF(n * m);

        // 合并
        int dx[] = {1, 0, -1, 0}, dy[] = {0, -1, 0, 1};
        for (int i = 0; i < n; i ++) {
            for (int j = 0; j < m; j ++) {
                if (grid[i][j] == '1') {
                    for (int d = 0; d < 4; d ++) {
                        int x = dx[d] + i, y = dy[d] + j;
                        if (x >= 0 && x < n && y >= 0 && y < m && grid[x][y] == '1') {
                            UF.Union(encode(i, j, m), encode(x, y, m));
                        }
                    }
                }
            }
        }

        // 查找
        int res = 0;
        for (int i = 0; i < n; i ++) {
            for (int j = 0; j < m; j ++) {
                if (grid[i][j] == '1' && UF.Find(encode(i, j, m))== encode(i, j, m)) {
                    res ++;
                }
            }
        }
        return res;
    }
};
// dfs算法
void dfs(vector<vector<char>> &grid, int a, int b) {

    //因为要修改原来的grid,所以这里必须用引用
    int dx[] = {1, -1, 0, 0}, dy[] = {0, 0, 1, -1};
    int m = grid.size(), n = grid[0].size();

    grid[a][b] = '0';
    for (int i = 0; i < 4; i++) {
        int x = a + dx[i], y = b + dy[i];
        if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == '1') {
            dfs(grid, x, y);
        }
    }
    return;
}

class Solution {
public:
    int numIslands(vector<vector<char>> &grid) {
        int ans = 0;
        if (!grid.size() || !grid[0].size()) return 0;

        int m = grid.size(), n = grid[0].size();
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == '1') {
                    dfs(grid, i, j);
                    ans++;
                }
            }
        }
        return ans;
    }
};

LeetCode 684.Redundant Connection

// 判断两个顶点是否连通,看他们的祖先是不是相同
// 685进阶题
class UnionFind
{
public:
    vector<int> father;
    UnionFind(int num)
    {
        for (int i = 0; i < num; i++)
        {
            father.push_back(i);
        }
    }

    int Find(int n)
    {
        if (father[n] == n)
            return n;
        father[n] = Find(father[n]);
        return father[n];
    }

    bool Union(int a, int b)
    {
        int fa = Find(a);
        int fb = Find(b);
        father[fb] = fa;
        return fa == fb; // 判断两个点是否连通
    }
};


class Solution
{
public:
    vector<int> findRedundantConnection(vector<vector<int>> &edges)
    {
        int n = edges.size();

        UnionFind UF(n);
        for (int i = 0; i < n; i++)
        {
            int x = edges[i][0], y = edges[i][1];
            // 初始化是0,1..n-1,所有下标要先减1,不然会越界
            if (UF.Union(x - 1, y - 1))
            {
                return {x, y};
            }
        }

        return {1, 1};
    }
};
发布了182 篇原创文章 · 获赞 71 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/qq_43827595/article/details/103460593
今日推荐