并查集的实现与练习

本文详细介绍了并查集数据结构及其优化方法,包括QuickUnion、路径压缩和按秩合并。并查集常用于判断两个元素是否属于同一集合,以及合并两个集合。文中通过实例展示了如何使用并查集解决如省份数量、冗余连接、连通网络的操作次数、等式方程的可满足性、最长连续序列、按公因数计算最大组件大小和岛屿数量等问题。并查集的优势在于其高效地处理特定问题,如判断元素间的连通性。
摘要由CSDN通过智能技术生成

并查集

img

使用条件:当关于线与线之间,两个表达式,两个数字等等具有了性质(共同关系)的传递性可以考虑使用并查集。

并查集主要提供了以下 2个操作,分别是「并查集」这个名字中的前两个字:

  • 并:把两个集合「合并」成一个集合;
  • 查:查询元素属于哪一个集合,可以顺便回答两个元素是否在一个集合中。
返回值函数名函数作用
构造函数无返回值UnionFind(int x)初始化并查集
voidunion(int x, int y)在x和y之间添加一条连接
intfind(int x)返回x所在的连通分量的标识
boolisConneted(int x, int y)返回是否x和y在一个连通分量之中
intgetCount()返回连通分量的数量

并查集之所以高效是因为在这个数据结构中只能回答1.判断两个数字所处的集合是否是同一个集合 2.操作两个数字所在的集合合并为同一个集合。相比求出全部的路径来说,其实他回答的问题很少,但是就是因为他只能回答这几个特定的问题,并且有一些场景下需要仅仅需要回答这几个问题,所以可以高效地在某些场景下解决一些问题。

一种实现+两种优化

Quick Union

class UnionFind {
private:
	int* parent;
	int count;
public:
	UnionFind(int n):
	count(n),
	parent(new int[n]) {
		for (int i = 0; i < n; i ++) {
			parent[i] = i;
		}
	}

	~UnionFind() {
		delete[] parent;
	}

	int find(int x) {// 找父亲节点
		while (x != parent[x]) {
			x = parent[x];
		}
		return parent[x];
	}

	bool isConneted(int x, int y) {
		return find(x) == find(y);
	}

	void unionElem(int x, int y) {
		int xRoot = find(x);
		int yRoot = find(y);

		if (xRoot == yRoot) 
			return ;
		parent[xRoot] = yRoot;// 连接根节点
	}
};

优化一:小树接大树之sz数组优化

class UnionFind {
private:
	int* parent;
    int* sz;// 记录以i为根节点的树的节点数
	int count;
public:
	UnionFind(int n):
	count(n),
	parent(new int[n]),
    sz(new int[n]) {
		for (int i = 0; i < n; i ++) {
			parent[i] = i;
            sz[i] = 1;
		}
	}

	~UnionFind() {
		delete[] parent;
        delete[] sz;
	}

	int find(int x) {
		while (x != parent[x]) {
			x = parent[x];
		}
		return parent[x];
	}

	bool isConneted(int x, int y) {
		return find(x) == find(y);
	}

	void unionElem(int x, int y) {
		int xRoot = find(x);
		int yRoot = find(y);

		if (xRoot == yRoot) 
			return ;
	
		if (sz[xRoot] <= sz[yRoot]) {// 这里让节点数少的树连接上节点数多的树
            parent[xRoot] = yRoot;
        } else {
            parent[yRoot] = xRoot;
        }
	}
};

优化二:小树接大树之rank数组优化

class UnionFind {
private:
	int* parent;
	int* rank;// 记录以i为根节点的树的层数
	int count;
public:
	UnionFind(int n):
	count(n),
	parent(new int[n]),
	rank(new int[n]) {
		for (int i = 0; i < n; i ++) {
			parent[i] = i;
			rank[i] = 1;
		}
	}

	~UnionFind() {
		delete[] parent;
		delete[] rank;
	}

	int find(int x) {
		while (x != parent[x]) {
			x = parent[x];
		}
		return parent[x];
	}

	bool isConneted(int x, int y) {
		return find(x) == find(y);
	}

	void unionElem(int x, int y) {
		int xRoot = find(x);
		int yRoot = find(y);

		if (xRoot == yRoot) 
			return ;
	
		if (rank[xRoot] < rank[yRoot]) {// 比较树的层数
			parent[xRoot] = parent[yRoot];
		} else if (rank[yRoot] < rank[xRoot]){
			parent[yRoot] = parent[xRoot];
		} else {
			parent[xRoot] = parent[yRoot];
			rank[yRoot] += 1;
		}
	}
};

优化三:路径压缩优化

class UnionFind {
private:
	int* parent;
	int* rank;
	int count;
public:
	UnionFind(int n):
	count(n),
	parent(new int[n]),
	rank(new int[n]) {
		for (int i = 0; i < n; i ++) {
			parent[i] = i;
			rank[i] = 1;
		}
	}

	~UnionFind() {
		delete[] parent;
		delete[] rank;
	}

	int find(int x) {
		while (x != parent[x]) {
			parent[x] = parent[parent[x]];// 每一次都让途径的节点都只想根节点
			x = parent[x];
		}
		return x;
        
        // 将路径上所有的节点都指向根节点
        //if (x != parent[x]) {
        //    parent[x] = find(parent[x]);
        //}
        //return parent[x];
	}

	bool isConneted(int x, int y) {
		return find(x) == find(y);
	}

	void unionElem(int x, int y) {
		int xRoot = find(x);
		int yRoot = find(y);

		if (xRoot == yRoot) 
			return ;
	
		if (rank[xRoot] < rank[yRoot]) {
			parent[xRoot] = parent[yRoot];
		} else if (rank[yRoot] < rank[xRoot]){
			parent[yRoot] = parent[xRoot];
		} else {
			parent[xRoot] = parent[yRoot];
			rank[yRoot] += 1;
		}
	}
};

并查集之加边练习

省份数量

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ffaXzXt0-1629965553112)(D:\github\gitee\leet-book-solutoin\并查集\并查集.assets\1629711058256.png)]

总体的思路就是判断每一个城市是否被连接,并且在判断某一个城市的时候需要将所有和该城市连接的城市都标记为已被连接,然后继续判断下一个城市,如此就可以所有的省份了。

所以关键就在于让所有连接的城市都标记,这样最后只要求出标记了多少次就可以了。

标记的方法很像标记连通块,所以又3中方法可以标记。

(并查集)

第一种方式就可以使用并查集。并查集很适合本题,因为如果使用(DFS或者BFS)都不能像求(Floof Fill)那样轻易的就可以标记了,本题需要转一个弯(连接这个城市连接的城市),但是并查集本身就是将同一种性质的连通块放入一个集合中,所以就可以利用(union函数和find函数)来解决这个问题,最后只要返回根节点的个数即可。

class Solution {
public:
    int find(int x, vector<int>& parent) {
        if (x != parent[x]) {
            parent[x] = find(parent[x], parent);
        }
        return parent[x];
    }

    int findCircleNum(vector<vector<int>>& isConnected) {
        int len = isConnected.size();
        vector<int> un(len, 0);
        for (int i = 0; i < len; i ++) {
            un[i] = i;
        }

        for (int i = 0; i < len; i ++) {
            for (int j = i + 1; j < len; j ++) {// 只用遍历[i+1, len-1]即可
                if (isConnected[i][j] == 1) {
                    int iRoot = find(i, un);
                    int jRoot = find(j, un);
                    un[iRoot] = jRoot;// 省份相连
                }
            }
        }
        
        int ans = 0;
        for (int i = 0; i < len; i ++) {
            if (un[i] == i) // 统计根节点的数量,而只有(i,i)才有可能成为根节点
                ans ++;
        }
        return ans;
    }
};
(深度优先搜索)

求出连通块的经典做法就是使用(DFS),将有相同性质有相连的位置全部标记。本题的同一性质是isConnected[i][j] == 1,根据这个性质就可以使得所有城市连接起来。

class Solution {
public:
    void dfs(vector<vector<int>>& isConnected, vector<bool>& vis, int k) {
        vis[k] = true;
        int len = isConnected.size();
        for (int i = 0; i < len; i ++) {
            if (isConnected[k][i] == 1 && !vis[i]) {
                dfs(isConnected, vis, i);
            }
        }
    }

    int findCircleNum(vector<vector<int>>& isConnected) {
        int len = isConnected.size();
        vector<bool> vis(len, false);// 将len个城市初始化为false(没有被连接)
        int ans = 0;
        for (int i = 0; i < len; i ++) {
            if (!vis[i]) {
                dfs(isConnected, vis, i);
                ans ++;
            }
        }
        return ans;
    }
};
(广度优先搜索)

当然还可以使用(BFS),使用queue将所有满足isConnected[i][j] == 1的位置标记为同一块空间,这样最后也可以求出连通块的数量。

class Solution {
public:
    int findCircleNum(vector<vector<int>>& isConnected) {
        int len = isConnected.size();
        queue<int> q;
        vector<bool> vis(len, false);
        int ans = 0;

        for (int i = 0; i < len; i ++) {
            if (!vis[i]) {// 判断第i个城市是否已经被连接过了
                ans ++;// 连接城市+1
                // 将与第i个城市的所有城市都标记一遍
                q.push(i);
                while (!q.empty()) {
                    int size = q.size();
                    while (size --) {
                        int top = q.front();
                        q.pop();
                        vis[top] = true;
                        for (int i = 0; i < len; i ++) {
                            if (isConnected[top][i] == 1 && !vis[i]) {
                                q.push(i);
                            }
                        }
                    }
                }
            }
        }

        return ans;
    }
};

冗余连接

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r6ox2BTD-1629965553112)(D:\github\gitee\leet-book-solutoin\并查集\并查集.assets\1629721767520.png)]

(并查集)

本题是考察并查集的典型案例。本题要求找出最后一个重复的边,所以就可以从前往后遍历,让所有的边的两端都相连接,如果在遍历的过程中遇到了根节点相同的两个点就说明两条边已经相连过了,就可以返回这条边。

问:本题是如何想到使用并查集的?

答:因为不能出现重复的边,所以就需要将点之间两两连接起来,并标记已经连接过了。等到遇到一个即将形成环的边就放回这条边,而可以形成环的边的两个点一定是分别在一条有公共点的两条边上,这样才能形成环,所以关键是如何判断是否在有公共点的边上,这就可以想到使用并查集,以为并查集就可以快速的判断是否两个点在同一个集合中,也就是是否有公共点,也可以快速地合并两个点,所以本题可以使用并查集。

class Solution {
public:
    int find(int x, vector<int>& parent) {
        if (x != parent[x]) {
            parent[x] = find(parent[x], parent);
        }
        return parent[x];
    }
    void unionElem(int x, int y, vector<int>& parent) {
        int xRoot = find(x, parent);
        int yRoot = find(y, parent);
        if (xRoot != yRoot) {
            parent[xRoot] = yRoot;
        }
    }

    vector<int> findRedundantConnection(vector<vector<int>>& edges) {
        int len = edges.size();
        vector<int> parent(len + 1, 0);
        for (int i = 1; i <= len; i ++) {
            parent[i] = i;
        }
        for (int i = 0; i < len; i ++) {
            if (find(edges[i][0], parent) != find(edges[i][1], parent)) {
                unionElem(edges[i][0], edges[i][1], parent);
            } else {
                return edges[i];
            }
        }
        return {};
    }
};

连通网络的操作次数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VbiNoB67-1629965553114)(D:\github\gitee\leet-book-solutoin\并查集\并查集.assets\1629766631041.png)]

(并查集)

还是使用边连接的问题,所以可以使用并查集。

首先需要判断是否网线足够使用,也就是判断n > len + 1,若不满足该条件就说明不能将所用的电脑全部连接起来。

然后只要计算出连通分量的个数即可。而需要操作的最少次数就是连通分量的个数 - 1(因为已经连接的连通分量之间就不用再进行修改连接了)。

class Solution {
public:
    int find(int x, vector<int>& parent) {
        if (x != parent[x]) {
            parent[x] = find(parent[x], parent);
        }
        return parent[x];
    }

    void unionElem(int x, int y, vector<int>& parent) {
        parent[find(x, parent)] = find(y, parent);
    }

    bool isConnected(int x, int y, vector<int>& parent) {
        return find(x, parent) == find(y, parent);
    }

    int makeConnected(int n, vector<vector<int>>& connections) {
        int len = connections.size();
        if (len + 1 < n) return -1;
        
        vector<int> un(n, 0);
        for (int i = 0; i < n; i ++) {
            un[i] = i;
        }
        
        for (int i = 0; i < len; i ++) {
            if (!isConnected(connections[i][0], connections[i][1], un)) {
                unionElem(connections[i][0], connections[i][1], un);
            }
        }
        int ans = 0;
        for (int i = 0; i < n; i ++) {
            if (un[i] == i) ans ++;
        }
        return ans - 1;
    }
};

并查集之与数字建立联系

等式方程的可满足性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XvZOxth6-1629965553115)(D:\github\gitee\leet-book-solutoin\并查集\并查集.assets\1629770922501.png)]

(并查集)

两个等式之间是有等号的传递性的,换言之,就是等号两边的未知数是可以放在同一个集合当中的(集合中的所有未知数都相等),而不等号两边的未知数都是一个个独立的集合。由此可知,可以划分连通分量来达到划分集合的目的。如果发现连通分量之间有交集就说明发生了冲突,就直接返回false,只有当所有的联通分量都不互相冲突的时候,才可以返回true

class Solution {
public:
    int find(int x, vector<int>& parent) {
        if (x != parent[x]) {
            parent[x] = find(parent[x], parent);
        }
        return parent[x];
    }

    void unionElem(int x, int y, vector<int>& parent) {
        parent[find(x, parent)] = find(y, parent);
    }

    bool isConnected(int x, int y, vector<int>& parent) {
        return find(x, parent) == find(y, parent);
    }

    bool equationsPossible(vector<string>& equations) {
        vector<int> un(26, 0);
        for (int i = 0; i < 26; i ++) {
            un[i] = i;
        }        
        for (auto equal : equations) {
            if (equal[1] == '=') {
                unionElem(equal[0] - 'a', equal[3] - 'a', un);
            }
        }
        for (auto equal : equations) {
            if (equal[1] == '!' && isConnected(equal[0] - 'a', equal[3] - 'a', un)) {
                return false;
            }
        }
        return true;
    }
};

最长连续序列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XRH5LrfX-1629965553116)(D:\github\gitee\leet-book-solutoin\并查集\并查集.assets\1629815685444.png)]

(暴力+哈希)

最暴力的思路是:枚举每一个数,将这个数当做是连续序列的开头,然后就是遍历整个数组判断数字中是否存在这个数的县一个位置,如果存在就继续查找下一个,如果找不到就计算以这个数开头的最长连续序列的长度。这个可以使用哈希表将顺序遍历数组的部分O(n)换成哈希表直接查找O(1)。

这样做的一个问题是会重复的计算相同的数字,如果nums = {1, 2, 3, 4, 5},那么从1开始需要遍历一遍数组,从2开始又要遍历一个数组,…,所以这样的做法使得后面的数组重复了前面已经计算过的长度,而理想的状态是直接找到1(连续序列的最小)直接遍历一遍数组,然后后面的连续的数字就可以直接跳过。

class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        unordered_set<int> se(nums.begin(), nums.end());
        int ans = 0;
        for (int num : nums) {
            int next = 1;
            while (se.count(num + next)) next ++;
            ans = max(ans, next);
        }
        return ans;
    }
};
  • 时间复杂度O(n2)
  • 空间复杂度O(n)
(哈希表)

根据上面所说的直接从连续序列的第一个数开始枚举的优化,可以使用哈希表来做到。怎么找到每一段连续序列的第一个数字呢?

连续序列的第一个数可以说在这个数之前的没有一个数可以接上当前这个数,换言之就是假设当前数为num则不存在num-1在数组中,那么num一定是第一个数。所以可以通过map.count(num - 1)来判断是否num是连续序列的第一个数字。

class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        unordered_set<int> se(nums.begin(), nums.end());
        int ans = 0;
        for (int num : nums) {
            if (se.count(num - 1)) continue;  // 如果不是第一个连续的数字就直接跳过
            int next = 1;
            while (se.count(num + next)) next ++;  // 一直找到下一个数字
            ans = max(ans, next);
        }
        return ans;
    }
};
  • 时间复杂度O(n)
  • 空间复杂度O(n)
(排序)

使用排序算法是最直接也是最容易想到的,但是时间复杂度为O(nlogn)(有一点高)。

排完序之后,可以直接计算连续的序列,其中注意要跳过重复的数字。并且在每一个连续序列的断开的地方需要计算上一个连续序列的长度即可。

class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        if (nums.empty()) return 0;

        sort(nums.begin(), nums.end());
        int len = nums.size();
        int ans = 0;
        int cnt = 1;
        for (int i = 1; i < len; i ++) {
            if (nums[i] == nums[i - 1]) continue;  // 出现重复的数字
            else if (nums[i] == nums[i - 1] + 1) {  // 出现连续的数字
                cnt ++;
            } else {
                ans = max(ans, cnt);// 结算去最大值
                cnt = 1;
            }
        }
        return max(ans, cnt);
    }
};
  • 时间复杂度O(nlogn)
  • 空间复杂度O(1)
(并查集)

还有一个比较奇妙的想法就是并查集。

并查集是将具有相同性质的数字联系起来,而本题是要求连续的数字的序列长度。连续数字就是一种性质,可以将数组中的每一个numnum + 1num - 1都找到并连接起来,这样最后只要是连续的数值就都会在一棵树上,最后的答案就是所有树中的节点数最多的树大小。

class Solution {
public:
    unordered_map<int, int> mp, sz;
    int find(int x) {
        if (x != mp[x]) {
            mp[x] = find(mp[x]);
        }
        return mp[x];
    }

    void merge(int x, int y) {
        int xRoot = find(x);
        int yRoot = find(y);
        if (xRoot == yRoot) return ;
        if (sz[xRoot] < sz[yRoot]) {
            mp[xRoot] = yRoot;
            sz[yRoot] += sz[xRoot];
        } else {
            mp[yRoot] = xRoot;
            sz[xRoot] += sz[yRoot];
        }
    }

    int longestConsecutive(vector<int>& nums) {
        for (int num : nums) {
            if (!mp.count(num)) {  // 这样可以避免出现多个数的时候会重复判断
                mp[num] = num;
                sz[num] = 1;
            } else continue;

            if (mp.count(num + 1)) merge(num, num + 1);
            if (mp.count(num - 1)) merge(num, num - 1);
        }
        int ans = 0;
        for (auto e : sz) {
            ans = max(ans, e.second);
        }
        return ans;
    }
};
  • 时间复杂度O(nlogn)
  • 空间复杂度O(n)

按公因数计算最大组件大小

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mdFWPTGE-1629965553116)(D:\github\gitee\leet-book-solutoin\并查集\并查集.assets\1629861982297.png)]

(并查集)

本题很明显是要建一个集合,并且集合中的数字的相同性质是拥有相同的公因数。这样就找到了数字之间的联系可以将数字连接起来形成性质的传递。

所以关键就是如何使用公因数这个性质来形成集合。

可以将每一个数字都联系到公因数上,最后统计数组nums有相同的祖先的数量,其中去一个最大值即可。

其中注意两个问题:

1.在分解质因数的时候,只用枚举sqrt(nums[i])个数字即可。因为在枚举前面sqrt(nums[i])个数的时候,nums[i] / j就是nums[i]对应的质因数。

2.为什么不能直接使用UnionFind对象中的get_tree_size()接口直接获取数组nums中的以nums[i]为祖先的节点个数呢?

这是因为UnionFind对象在初始化的时候,就初始化了max_element个数字。 nums[i]为祖先的树中很可能会包含不在nums[i]中节点,而在1 ~ maxelement中的数字。最终得到的答案中的连通分量中节点的数量因为有不是nums中的数,导致答案很大。

3.不可以使用unordered_map<int, int>哈希表来代替vector<int>数组。因为需要前面的质因数的节点。如果使用unordered_map<int, int>只初始化nums数组中的值,在枚举质因数的时候质因数指向的节点就是0,这不是理想的结果。理想的结果是节点指向节点本身才对。

class UnionFind {// 并查集
private:
	vector<int> parent;
	vector<int> sz;
	int count;
public:
	UnionFind(int n):// 初始化并查集
	count(n),
	parent(n),
	sz(n) {
		for (int i = 0; i < n; i ++) {
			parent[i] = i;
			sz[i] = 1;
		}
	}

	int find(int x) {  // 找到x的根节点
		if (parent[x] != x) {
			parent[x] = find(parent[x]);
		}
		return parent[x];
	}

	bool isConneted(int x, int y) {  // 判断两个数字是否在同一个集合当中
		return find(x) == find(y);
	}

	void unionElem(int x, int y) {  // 合并两个集合
		int xRoot = find(x);
		int yRoot = find(y);

		if (xRoot == yRoot) 
			return ;
		
		if (sz[xRoot] < sz[yRoot]) {
			parent[xRoot] = yRoot;
			sz[yRoot] += sz[xRoot];
		} else {
			parent[yRoot] = xRoot;
			sz[xRoot] += sz[yRoot];
		}
		count --;
	}

	int get_part_size() {  // 连通分块的数量
		return count;
	}

	int get_tree_size(int i) {  // 第i个树中的节点的个数
		return sz[i];
	}
};

class Solution {
public:
    int largestComponentSize(vector<int>& nums) {
        int len = nums.size();
        int size = *max_element(nums.begin(), nums.end());
        UnionFind un(size + 1);
        
        for (int i = 0; i < len; i ++) {
            for (int j = 2; j * j <= nums[i]; j ++) {  // 分解质因数
                if (nums[i] % j == 0) {
                    un.unionElem(nums[i], j);
                    un.unionElem(nums[i], nums[i] / j);
                }
            }
        }

        int ans = 0;
        vector<int> root(size + 1, 0);
        for (int i = 0; i < len; i ++) {
            ans = max(ans, ++root[un.find(nums[i])]);  // 统计祖先(该祖先在数组中)下的节点数
        }
        return ans;
    }
};

并查集之连通块问题

岛屿数量

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R5esDAMX-1629965553117)(D:\github\gitee\leet-book-solutoin\并查集\并查集.assets\1629892246964.png)]

本题是求出连通块的个数,很常规可以使用DFS或者BFS或者并查集解决,这样重点讲一下并查集的思想。

(深搜)

找到一个连通块的一个部分,然后将整个连通块中的1都替换成0,这样就可以了递归找出所有的连通块的个数了。

class Solution {
public:
    int xd[4] = {-1, 0, 1, 0};
    int yd[4] = {0, 1, 0, -1};
    void dfs(vector<vector<char>>& grid, int x, int y) {
        grid[x][y] = '0';
        int m = grid.size(), n = grid[0].size();
        for (int i = 0; i < 4; i ++) {
            int xi = x + xd[i], yi = y + yd[i];
            if (xi < 0 || xi >= m || yi < 0 || yi >= n) continue;
            if (grid[xi][yi] == '0') continue;
            dfs(grid, xi, yi);
        }
    }

    int numIslands(vector<vector<char>>& grid) {
        int m = grid.size(), n = grid[0].size();
        int ans = 0;

        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;
    }
};
(广搜)

广搜的使用queue作为辅助数据结构可以到达一样的效果。

class Solution {
public:
    using PII = pair<int, int>;
    int numIslands(vector<vector<char>>& grid) {
        int xd[4] = {-1, 0, 1, 0}, yd[4] = {0, 1, 0, -1};
        int m = grid.size(), n = grid[0].size();
        queue<PII> q;
        
        int ans = 0;
        for (int i = 0; i < m; i ++) {
            for (int j = 0; j < n; j ++) {
                if (grid[i][j] != '1') continue;
                ans ++;
                q.push({i, j});
                while (!q.empty()) {
                    auto top = q.front();
                    q.pop();
                    int x = top.first, y = top.second;
                    grid[x][y] = '0';
                    for (int k = 0; k < 4; k ++) {
                        int xi = x + xd[k], yi = y + yd[k];
                        if (xi < 0 || xi >= m || yi < 0 || yi >= n) continue;
                        if (grid[xi][yi] != '1') continue;
                        grid[xi][yi] = '0';
                        q.push({xi, yi});
                    }
                }
            }
        }
        return ans;
    }
};
(并查集)

本题还可以使用并查集。就是将数组中的所有数字一开始都初始化为一个集合,也就是一开始有m * n个集合,然后将grid[i][j] == 1的位置都合并到一个集合当中,随即也要将并查集中的集合个数相应地减少1,知道最后将所有的并查集中集合都合并完成。但是最后不可以直接返回并查集中的集合个数,因为其中所有的0都没有被合并,所以现在并查集中的集合是合并之后的1和所有的0的个数。这就要求我们需要在遍历数组的时候,将所有0的个数都记录下来,最后答案就是un.get_part_size() - ocean

class UnionFind {  // 并查集
private:
	vector<int> parent;  // 父亲节点
	vector<int> sz;  // 第i个树中节点的个数
	int count;  // 连通分量的数量
public:
	UnionFind(int n):// 初始化并查集
	count(n),
	parent(n),
	sz(n) {
		for (int i = 0; i < n; i ++) {
			parent[i] = i;
			sz[i] = 1;
		}
	}

	int find(int x) {  // 找到x的根节点
		if (parent[x] != x) {
			parent[x] = find(parent[x]);
		}
		return parent[x];
	}

	bool isConneted(int x, int y) {  // 判断两个数字是否在同一个集合当中
		return find(x) == find(y);
	}

	void unionElem(int x, int y) {  // 合并两个集合
		int xRoot = find(x);
		int yRoot = find(y);

		if (xRoot == yRoot) 
			return ;
		
		if (sz[xRoot] < sz[yRoot]) {
			parent[xRoot] = yRoot;
			sz[yRoot] += sz[xRoot];
		} else {
			parent[yRoot] = xRoot;
			sz[xRoot] += sz[yRoot];
		}
		count --;
	}

	int get_part_size() {  // 连通分块的数量
		return count;
	}

	int get_tree_size(int i) {  // 第i个树中的节点的个数
		return sz[i];
	}
};
class Solution {
public:
    int getIndex(int x, int y, int col) {
        return x * col + y;
    }

    int numIslands(vector<vector<char>>& grid) {
        int yd[4] = {0, 1, 0, -1};
        int xd[4] = {-1, 0, 1, 0};
        int m = grid.size(), n = grid[0].size();
        UnionFind un(m * n);
        int ocean = 0;
        for (int i = 0; i < m; i ++) {
            for (int j = 0; j < n; j ++) {
                if (grid[i][j] != '1') {
                    ocean ++;
                    continue;
                }
                for (int k = 0; k < 4; k ++) {
                    int xi = i + xd[k], yi = j + yd[k];
                    if (xi < 0 || xi >= m || yi < 0 || yi >= n) continue;
                    if (grid[xi][yi] != '1') continue;
                    un.unionElem(getIndex(xi, yi, n), getIndex(i, j, n));
                }
            }
        }

        return un.get_part_size() - ocean;
    }
};

总结:并查集的核心思想还是将有相同性质的数字合并起来形成一个集合。只不过在求解连通块问题的时候,往往不仅需要处理连通块,还需要处理非连通块的部分,所以需要注意这一块。

被包围的区域

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fBhvBPwc-1629965553117)(D:\github\gitee\leet-book-solutoin\并查集\并查集.assets\1629873182913.png)]

本题其实就是在求出连通块的数量的基础上,转了一个弯。和边界相连的O不能被替换,而其他的O要被全部替换成X。所以总体的思想就是首先为了防止误将边界上的O替换,所以将和边界相连的O都标记一下,然后替换数组中没有被标记的O,这样就是将中间不和边界相连的O全部替换掉了。

(递归爆搜)

第一种思路就是,将和边界相连的O使用dfs求出连通块并在在vis数组中标记一遍,然后再使用dfs将中间没有被标记的部分的O都替换成X。

class Solution {
public:
    int xd[4] = {-1, 0, 1, 0};
    int yd[4] = {0, 1, 0, -1};

    void dfs1(vector<vector<char>>& board, vector<vector<bool>>& vis, int x, int y) {
        vis[x][y] = true;
        int m = board.size(), n = board[0].size();
        for (int i = 0; i < 4; i ++) {
            int xi = x + xd[i], yi = y + yd[i];
            if (xi < 0 || xi >= m || yi < 0 || yi >= n) continue;
            if (board[xi][yi] == 'X' || vis[xi][yi]) continue;
            dfs1(board, vis, xi, yi);
        }
    }

    void dfs2(vector<vector<char>>& board, int x, int y) {
        board[x][y] = 'X';
        int m = board.size(), n = board[0].size();
        for (int i = 0; i < 4; i ++) {
            int xi = x + xd[i], yi = y + yd[i];
            if (xi < 0 || xi >= m || yi < 0 || yi >= n) continue;
            if (board[xi][yi] == 'X') continue;
            dfs2(board, xi, yi);
        }
    }

    void solve(vector<vector<char>>& board) {
        int m = board.size(), n = board[0].size();
        vector<vector<bool>> vis(m, vector<bool>(n, false));
        for (int i = 0; i < m; i ++) {
            for (int j = 0; j < n; j ++) {
                if ((i == 0 || j == 0 || i == m - 1 || j == n - 1)
                    && (board[i][j] == 'O')) 
                    dfs1(board, vis, i, j);
            }
        }
        for (int i = 1; i < m - 1; i ++) {
            for (int j = 1; j < n - 1; j ++) {
                if (board[i][j] == 'O' && !vis[i][j]) {
                    dfs2(board, i, j);
                }
            }
        }
    }
};
(深搜简化版)

其实可以不用使用两个dfs来分别解决。其实只要第一个dfs标记即可。其余剩下的都是没有标记的,可以不用使用dfs将连通块O的替换X,可以直接遍历一遍二维数组遇到O替换成X。而且**vis数组也可以省略掉,直接在原数组上用#替换O来标记即可。**

class Solution {
public:
    int xd[4] = {-1, 0, 1, 0};
    int yd[4] = {0, 1, 0, -1};
    void dfs(vector<vector<char>>& board, int x, int y) {
        board[x][y] = '#';
        int m = board.size(), n = board[0].size();
        for (int i = 0; i < 4; i ++) {
            int xi = x + xd[i], yi = y + yd[i];
            if (xi < 0 || xi >= m || yi < 0 || yi >= n) continue;
            if (board[xi][yi] != 'O') continue;
            dfs(board, xi, yi);
        }
    }

    void solve(vector<vector<char>>& board) {
        int m = board.size(), n = board[0].size();
        for (int i = 0; i < m; i ++) {
            for (int j = 0; j < n; j ++) {
                if (i != 0 && i != m - 1 && j != 0 && j != n - 1) continue;
                if (board[i][j] != 'O') continue;
                dfs(board, i, j);
            }
        }
        for (int i = 0; i < m; i ++) {
            for (int j = 0; j < n; j ++) {
                if (board[i][j] == '#') {
                    board[i][j] = 'O';
                } else if (board[i][j] == 'O') {
                    board[i][j] = 'X';
                }
            }
        }
    }
};
(广搜简化版)

使用queue来广度搜索也是可以的,这里就直接写了一个简化版的。

class Solution {
public:
    using PII = pair<int, int>;
    void solve(vector<vector<char>>& board) {
        int xd[4] = {-1, 0, 1, 0};
        int yd[4] = {0, 1, 0, -1};

        int m = board.size(), n = board[0].size();
        for (int i = 0; i < m; i ++) {
            for (int j = 0; j < n; j ++) {
                if (i != 0 && j != 0 && i != m - 1 && j != n - 1) continue;
                if (board[i][j] != 'O') continue; 
                queue<PII> q;
                q.push({i, j});
                while (!q.empty()) {
                    auto top = q.front();
                    q.pop();
                    int x = top.first, y = top.second;
                    board[x][y] = '#';
                    for (int k = 0; k < 4; k ++) {
                        int xi = x + xd[k], yi = y + yd[k];
                        if (xi < 0 || xi >= m || yi < 0 || yi >= n) continue;
                        if (board[xi][yi] != 'O') continue;
                        board[xi][yi] = '#';
                        q.push({xi, yi}); 
                    }
                }
            }
        }

        for (int i = 0; i < m; i ++) {
            for (int j = 0; j < n; j ++) {
                if (board[i][j] == '#') {
                    board[i][j] = 'O';
                } else {
                    board[i][j] = 'X';
                }
            }
        }

    }
};
(并查集)

并查集的思路也比较新奇。就是设置一个虚拟头结点,然后将连接边界的O都和dummy放在一个集合当中,将其余的O放在一个集合当中,最后在遍历数组的时候,如果和dummy有相同的祖先节点的位置都是边界上的O,所以都不动,将其余和dummy有不同祖先节点的位置上的O都换成X即可。

class UnionFind {  // 并查集
private:
	vector<int> parent;  // 父亲节点
	vector<int> sz;  // 第i个树中节点的个数
	int count;  // 连通分量的数量
public:
	UnionFind(int n):// 初始化并查集
	count(n + 1),
	parent(n + 1),
	sz(n + 1) {
		for (int i = 0; i <= n; i ++) {
			parent[i] = i;
			sz[i] = 1;
		}
	}

	int find(int x) {  // 找到x的根节点
		if (parent[x] != x) {
			parent[x] = find(parent[x]);
		}
		return parent[x];
	}

	bool isConneted(int x, int y) {  // 判断两个数字是否在同一个集合当中
		return find(x) == find(y);
	}

	void unionElem(int x, int y) {  // 合并两个集合
		int xRoot = find(x);
		int yRoot = find(y);

		if (xRoot == yRoot) 
			return ;
		
		if (sz[xRoot] < sz[yRoot]) {
			parent[xRoot] = yRoot;
			sz[yRoot] += sz[xRoot];
		} else {
			parent[yRoot] = xRoot;
			sz[xRoot] += sz[yRoot];
		}
		count --;
	}

	int get_part_size() {  // 连通分块的数量
		return count;
	}

	int get_tree_size(int i) {  // 第i个树中的节点的个数
		return sz[i];
	}
};
class Solution {
public:
    int getIndex(int x, int y, int col) {
        return x * col + y;
    }

    void solve(vector<vector<char>>& board) {
        int xd[4] = {-1, 0, 1, 0};
        int yd[4] = {0, 1, 0, -1};
        int m = board.size(), n = board[0].size();
        UnionFind un(m * n + 1); // 多开一个节点的位置,设置为虚拟头结点
        int dummy = m * n;

        for (int i = 0; i < m; i ++) {
            for (int j = 0; j < n; j ++) {
                if (board[i][j] != 'O') continue;
                if (i == 0 || i == m - 1 || j == 0 || j == n - 1) {
                    un.unionElem(getIndex(i, j, n), dummy);
                } else {
                    for (int k = 0; k < 4; k ++) {
                        int xi = i + xd[k], yi = j + yd[k];
                        if (xi < 0 || xi >= m || yi < 0 || yi >= n) continue;
                        if (board[xi][yi] != 'O') continue;
                        un.unionElem(getIndex(xi, yi, n), getIndex(i, j, n));
                    }
                }
            }
        }

        for (int i = 0; i < m; i ++) {
            for (int j = 0; j < n; j ++) {
                // 只有和虚拟头结点相连的位置才设置为'O',其余的位置都设置为'X'
                if (board[i][j] == 'O') {
                    if (!un.isConneted(getIndex(i, j, n), dummy)) {
                        board[i][j] = 'X';
                    }
                }
            }
        }
    }
};

由斜杠划分区域

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uzBUgW08-1629965553118)(D:\github\gitee\leet-book-solutoin\并查集\并查集.assets\1629898315676.png)]

(数组转换 + 深搜连通块)

可以将一个斜杠上的位置替换成由1构成,这样就可以转化成为求出连通块数量的问题了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G8RU5zaG-1629965553118)(D:\github\gitee\leet-book-solutoin\并查集\并查集.assets\1629900397293.png)]

那为什么将放个分成3X3的放个呢?

首先1X1不可以使用,所以只能考虑2X2,3X3,4X4…然后看2X2,发现因为求出连通块的时候,仅仅只有上下左右这4个方向可以和其他的位置相连接,但是如果转化成2X2的方格的话就不能达到这个目标,所以2X2不可以。接下来再试一试3X3,发现由斜杠划分的区域已经可以形成连通块了,所以3X3就可以了。当然3X3可以,4X4也就可以,但是为了计算方便还是分得越少越好。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ner0NX7y-1629965553119)(D:\github\gitee\leet-book-solutoin\并查集\并查集.assets\1629900378411.png)]

class Solution {
public:
    int xd[4] = {-1, 0, 1, 0};
    int yd[4] = {0, 1, 0, -1};
    void dfs(vector<vector<int>>& bigGrid, int x, int y) {
        bigGrid[x][y] = 1;
        int m = bigGrid.size(), n = bigGrid[0].size();
        for (int i = 0; i < 4; i ++) {
            int xi = x + xd[i], yi = y + yd[i];
            if (xi < 0 || xi >= m || yi < 0 || yi >= n) continue;
            if (bigGrid[xi][yi] == 1) continue;
            dfs(bigGrid, xi, yi);
        }
    }

    int regionsBySlashes(vector<string>& grid) {
        int m = grid.size(), n = grid[0].size();
        vector<vector<int>> bigGrid(3 * m, vector<int>(3 * n, 0));
        for (int i = 0; i < m; i ++) {
            for (int j = 0; j < n; j ++) {
                if (grid[i][j] == '\\') {
                    bigGrid[3*i][3*j] = bigGrid[3*i + 1][3*j + 1] 
                    = bigGrid[3*i + 2][3*j + 2] = 1;
                } else if (grid[i][j] == '/') {
                    bigGrid[3*i][3*j + 2] = bigGrid[3*i + 1][3*j + 1] =
                     bigGrid[3*i + 2][3*j] = 1;
                }
            }
        }

        int ans = 0;
        for (int i = 0; i < 3 * m; i ++) {
            for (int j = 0; j < 3 * n; j ++) {
                if (bigGrid[i][j] == 0) {
                    dfs(bigGrid, i, j);
                    ans ++;
                }
            }
        } 
        return ans;
    }
};

注意:既然已经使用3X3的方格转换后了,所以该题目就变成了求出岛屿数量那题了,因此除了使用深搜还可以使用广搜和并查集。(后面两种就不写代码了,有兴趣可以参考岛屿数量那一题)

(转化思想 + 并查集)

还有一种转换的思想不是将一个方格切成更小的方格,而是按对角线分开,当然这里只是假想成是对角线分割,实际在计算机中是不存在这种对角线分割的三角形存储空间的。

转换成这种形状的存储空间之后,有一个好处就是:可以将\/都表示出来。如图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m2QQWs7z-1629965553119)(D:\github\gitee\leet-book-solutoin\并查集\并查集.assets\1629940134317.png)]

已经解决了单元格表示的问题,那接下来就要看是否这种分割方式可以还原出原来图形的连通块

这里有一个本题最关键的一个思想就是:如果现在有两个方格,那么上面方格的2号区域和下面方格的0号区域无论在什么情况下(两个方格无论是\/<空格>)都可以合并成一个集合。同理,一个方格如果有右边区域的话,那么该方格的1号区域和右边方格的3号区域也是无论在什么情况下都可以合并成一个集合的。其余的上合并和左合并也是一样

根据上面这样合并方法就可以使得分割方格之间合并形成连通块,并且可以还原出原来的连通块。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RMYJHm3R-1629965553120)(D:\github\gitee\leet-book-solutoin\并查集\并查集.assets\1629940935253.png)]

class UnionFind {  // 并查集
private:
	vector<int> parent;  // 父亲节点
	vector<int> sz;  // 第i个树中节点的个数
	int count;  // 连通分量的数量
public:
	UnionFind(int n):// 初始化并查集
	count(n),
	parent(n),
	sz(n) {
		for (int i = 0; i < n; i ++) {
			parent[i] = i;
			sz[i] = 1;
		}
	}

	int find(int x) {  // 找到x的根节点
		if (parent[x] != x) {
			parent[x] = find(parent[x]);
		}
		return parent[x];
	}

	bool isConneted(int x, int y) {  // 判断两个数字是否在同一个集合当中
		return find(x) == find(y);
	}

	void unionElem(int x, int y) {  // 合并两个集合
		int xRoot = find(x);
		int yRoot = find(y);

		if (xRoot == yRoot) 
			return ;
		
		if (sz[xRoot] < sz[yRoot]) {
			parent[xRoot] = yRoot;
			sz[yRoot] += sz[xRoot];
		} else {
			parent[yRoot] = xRoot;
			sz[xRoot] += sz[yRoot];
		}
		count --;
	}

	int get_part_size() {  // 连通分块的数量
		return count;
	}

	int get_tree_size(int i) {  // 第i个树中的节点的个数
		return sz[i];
	}
};
class Solution {
public:
    int regionsBySlashes(vector<string>& grid) {
        int m = grid.size(), n = grid[0].size();
        UnionFind un(4 * m * n);
        for (int i = 0; i < m; i ++) {
            for (int j = 0; j < n; j ++) {
                int head = 4 * (i * n + j);
                if (grid[i][j] == '/') {  // 左上和右下合并
                    un.unionElem(head, head + 3);
                    un.unionElem(head + 1, head + 2);
                } else if (grid[i][j] == '\\') {  // 右上和左下合并 
                    un.unionElem(head, head + 1);
                    un.unionElem(head + 2, head + 3);
                } else {  // 四个部分全部合并
                    un.unionElem(head, head + 1);  
                    un.unionElem(head + 2, head + 3);
                    un.unionElem(head + 1, head + 2);
                }
                
                // 方格之间相互合并
                if (i > 0) {  // 如果上面的方格的话,可以向上合并
                    un.unionElem(head, head + 2 - 4 * n);
                }
                if (i < m - 1) {  // 如果有下面的方格的话,可以向下合并
                    un.unionElem(head + 2, head + 4 * n);
                }
                if (j > 0) {  // 如果有左边方格的话,可以向左合并
                    un.unionElem(head + 3, head + 1 - 4);
                }
                if (j < n - 1) {  // 如果有右方格的话,可以向右合并
                    un.unionElem(head + 1, head + 3 + 4);
                }
            }
        }
        return un.get_part_size();  // 返回连通块的数量
    }
};
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hyzhang_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值