在并查集(Union-Find)中学习了并查集算法的原理以及几种算法实现。
下面通过leetcode的算法题来具体使用并查集(Union-Find)算法。主要是两种方法来实现并查集:
- 使用模板,定义UF类,直接套用模板即可。
- 不定义UF类,在函数内部实现初始化,调用find,union函数。
文章目录
Union-Find模板
1. 使用路径压缩的加权quick-union算法
class UF{
int N;
int count;
int[] id;
int[] sz;
UF(int N){
this.N = N;
count = N;
id = new int[N];
sz = new int[N];
for(int i = 0; i < N; i++) {
id[i] = i;
sz[i] = 1;
}
}
public int getCount() {
return count;
}
public void union(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if(pRoot != qRoot) {
if(sz[pRoot] < sz[qRoot]) {
id[pRoot] = id[qRoot];
sz[qRoot] += sz[pRoot];
}else {
id[qRoot] = id[pRoot];
sz[pRoot] += sz[qRoot];
}
count--;
}
}
private int find(int p) {
if(p == id[p]) return p;
id[p] = find(id[p]);
return id[p];
}
}
如测试数据量较大,适合用上述模板,若测试数据量较小,使用如下简单版本的并查集:
2. quick-union算法
public class UF {
private int[] id; //分量id(以触点作为索引)
private int count; //分量数量
public UF(int N) {
//初始化分量id数组
count = N;
id = new int[N];
for(int i = 0; i < N; i++) {
id[i] = i;
}
}
public int count() {
return count;
}
public boolean connected(int p, int q) {
return find(p) == find(q);
}
public int find(int p) {
//找出分量的名称
while(p != id[p]) p = id[p];
return p;
}
public void union(int p, int q) {
//将p和q的根节点统一
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot) return;
id[pRoot] = qRoot;
// id[find(p)] = find(q);
count--;
}
}
1319. 连通网络的操作次数
1. 题目描述
leetcode题目链接:1319. 连通网络的操作次数
2. 思路分析
利用并查集求出多余的缆线数量和连通分量的个数。
-
三个连通块要想联通至少得需要两条边(也就是两条线)那么不难看出最终结果就是连通块数量-1
-
注意一个前提也就是线要够(n个节点至少需要n-1条线)
3. 代码实现
使用路径压缩的加权quick-union算法
class Solution {
public int makeConnected(int n, int[][] connections) {
if (connections.length < n - 1) return -1; // n 个节点相互连通至少需要n-1条线
UF uf = new UF(n);
for (int[] connect : connections) {
uf.union(connect[0], connect[1]); // 合并
}
return uf.getCount() - 1;
}
class UF { // 路径压缩的加权quick-union算法模板
int N;
int count;
int[] id;
int[] sz;
private UF (int n) {
N = n;
count = n;
id = new int[N];
sz = new int[N];
for (int i = 0; i < N; i++) {
id[i] = i;
sz[i] = 1;
}
}
public int getCount () {
return count;
}
public void union (int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot != qRoot) {
if (sz[pRoot] < sz[qRoot]) {
id[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
} else {
id[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
}
count--;
}
}
private int find (int p) {
if (p == id[p]) {
return p;
}
id[p] = find(id[p]);
return id[p];
}
}
}
quick-union算法
class Solution {
public int makeConnected(int n, int[][] connections) {
if (connections.length < n - 1) return -1;
UF uf = new UF(n);
for (int[] connect : connections) {
uf.union(connect[0], connect[1]);
}
return uf.count() - 1;
}
class UF { // 算法模板
private int[] id; //分量id(以触点作为索引)
private int count; //分量数量
public UF(int N) {
//初始化分量id数组
count = N;
id = new int[N];
for(int i = 0; i < N; i++) {
id[i] = i;
}
}
public int count() {
return count;
}
public boolean connected(int p, int q) {
return find(p) == find(q);
}
public int find(int p) {
//找出分量的名称
while(p != id[p]) p = id[p];
return p;
}
public void union(int p, int q) {
//将p和q的根节点统一
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot) return;
id[pRoot] = qRoot;
// id[find(p)] = find(q);
count--;
}
}
}
这个就比较耗时,因此使用更简单的并查集写法,不创建并查集的类,直接在函数中实现:
class Solution {
int[] id;
public int makeConnected(int n, int[][] connections) {
if (connections.length < n - 1) return -1;
// 初始化
id = new int[n];
for (int i = 0; i < n; i++) {
id[i] = i;
}
for (int[] connect : connections) {
union(connect[0], connect[1]);
}
// 这里也可以定义count函数,就不用再写for循环,同上
int count = 0;
for (int i = 0; i < n; i++) {
if (id[i] == i) {
count++;
}
}
return count - 1;
}
// find、union函数直接从类中复制即可
private int find(int p) {
//找出分量的名称
while(p != id[p]) p = id[p];
return p;
}
private void union(int p, int q) {
//将p和q的根节点统一
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot) return;
id[pRoot] = qRoot;
}
}
union函数,getCount函数,初始化都在函数内部实现。
class Solution {
public int makeConnected(int n, int[][] connections) {
int[] id = new int[n];
for (int i = 0; i < n; i++) {
id[i] = i;
}
int count = n, res = 0;
for (int[] e : connections) {
int p1 = find(id, e[0]);
int p2 = find(id, e[1]);
if (p1 == p2) {
res++;
} else {
count--;
id[p1] = p2;
}
}
return res + 1 >= count ? count - 1 : -1;
}
private int find(int[] id, int p) {
if (p == id[p]) return p;
id[p] = find(id, id[p]);
return id[p];
}
}
547. 省份数量
1. 题目描述
leetcode题目链接:547. 省份数量
2. 思路分析
使用并查集算法,如果两者之间为1,则合并在一块,最后返回count。
3. 代码实现
模板实现
class Solution {
public int findCircleNum(int[][] isConnected) {
int m = isConnected.length, n = isConnected[0].length;
if (m == 0 || n == 0) return 0;
UF uf = new UF(m);
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (isConnected[i][j] == 1) {
uf.union(i, j);
}
}
}
return uf.getCount();
}
class UF { // 路径压缩的加权quick-union算法模板
int N;
int count;
int[] id;
int[] sz;
private UF (int n) {
N = n;
count = n;
id = new int[N];
sz = new int[N];
for (int i = 0; i < N; i++) {
id[i] = i;
sz[i] = 1;
}
}
public int getCount () {
return count;
}
public void union (int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot != qRoot) {
if (sz[pRoot] < sz[qRoot]) {
id[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
} else {
id[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
}
count--;
}
}
private int find (int p) {
if (p == id[p]) {
return p;
}
id[p] = find(id[p]);
return id[p];
}
}
}
不用模板,函数内部实现
class Solution {
int[] id;
int count = 0;
public int findCircleNum(int[][] isConnected) {
int m = isConnected.length, n = isConnected[0].length;
if (m == 0 || n == 0) return 0;
count = m;
id = new int[m];
for (int i = 0; i < m; i++) id[i] = i; // 初始化
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (isConnected[i][j] == 1) union(i, j);
}
}
return count;
}
// find、union函数直接从类中复制即可
private int find(int p) {
if (p == id[p]) return p;
id[p] = find(id[p]);
return id[p];
}
private void union(int p, int q) {
int pid = find(p);
int qid = find(q);
if (pid == qid) return;
id[pid] = qid;
count--;
}
}
959. 由斜杠划分区域
1. 题目描述
leetcode题目链接:959. 由斜杠划分区域
2. 思路分析
连接时需要考虑以下5种情况:
- 字符为
空格“ ”
:此时需连接区域[1, 2, 3, 4] - 字符为
斜杠"/"
:此时需连接区域[0, 3], [1, 2] - 字符为
反斜杠"\"
:此时需连接区域[0, 1],[2, 3] - 考虑方格右端(如图中黄色区域所示)[1, 1 + 4 + 3]
- 考虑方格下端(如图中青色区域所示)[2, 4 * n]
3. 代码实现
模板实现
class Solution {
public int regionsBySlashes(String[] grid) {
int n = grid.length;
int size = 4 * n * n;
UF uf = new UF(size);
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
int k = 4 * i * n + 4 * j;
if (grid[i].charAt(j) == ' ') {
uf.union(k, k + 1);
uf.union(k + 1, k + 2);
uf.union(k + 2, k + 3);
} else if (grid[i].charAt(j) == '/') {
uf.union(k, k + 3);
uf.union(k + 1, k + 2);
} else {
uf.union(k, k + 1);
uf.union(k + 2, k + 3);
}
if (j < n - 1) {
uf.union(k + 1, k + 4 + 3);
}
if (i < n - 1) {
uf.union(k + 2, k + n * 4);
}
}
}
return uf.getCount();
}
class UF { // 路径压缩的加权quick-union算法模板
int N;
int count;
int[] id;
int[] sz;
private UF (int n) {
N = n;
count = n;
id = new int[N];
sz = new int[N];
for (int i = 0; i < N; i++) {
id[i] = i;
sz[i] = 1;
}
}
public int getCount () {
return count;
}
public void union (int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot != qRoot) {
if (sz[pRoot] < sz[qRoot]) {
id[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
} else {
id[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
}
count--;
}
}
private int find (int p) {
if (p == id[p]) {
return p;
}
id[p] = find(id[p]);
return id[p];
}
}
}
1579. 保证图可完全遍历
1. 题目描述
leetcode题目链接:1579. 保证图可完全遍历
2. 思路分析
- 贪心思路,优先处理公共边,将公共边信息保存在并查集中,并计算多余的公共边(处于同一个连通分量);
- 然后对Alice和Bob分别处理(注意此时应在各自的并查集中执行操作),因为图可完全遍历等价于图中只存在一个连通分量,此时对其各自操作,边的顺序并不影响结果,累加多余的边(处于同一个连通分量)。
- 最后判断能否完全遍历即可。
需要注意的是,题目中的节点不是从0开始的,为了符合并查集的定义初始化,先将节点编号改为从0开始。
3. 代码实现
class Solution {
public int maxNumEdgesToRemove(int n, int[][] edges) {
// 先将节点编号改为从0开始,或者大小设为n+1
for (int[] edge : edges) {
edge[1]--;
edge[2]--;
}
UF ufa = new UF(n);
UF ufb = new UF(n);
int res = 0;
// 优先处理公共边
for (int[] edge : edges) {
if (edge[0] == 3) {
if (!ufa.union(edge[1], edge[2])) {
res++;
} else {
ufb.union(edge[1], edge[2]);
}
}
}
// 分别处理单独边
for (int[] edge : edges) {
if (edge[0] == 1) {
if (!ufa.union(edge[1], edge[2])) {
res++;
}
}
if (edge[0] == 2) {
if (!ufb.union(edge[1], edge[2])) {
res++;
}
}
}
return ufa.getCount() == 1 && ufb.getCount() == 1 ? res : -1;
}
class UF {
int count; //连通分量个数
int[] id;
int[] sz;
public UF(int n) {
count = n;
id = new int[n];
sz = new int[n];
for (int i = 0; i < n; i++) {
id[i] = i;
sz[i] = 1;
}
}
public int getCount() {
return count;
}
public int find(int p) {
if (p != id[p]) {
id[p] = find(id[p]);
}
return id[p];
}
public boolean union(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return false;
}
if (sz[pRoot] > sz[qRoot]) {
id[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
} else {
id[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
}
count--;
return true;
}
}
}
803. 打砖块
1. 题目描述
leetcode题目链接:803. 打砖块
2. 思路分析
此题考察的是逆向思维。
这题只需求每次打击位置后掉落的砖块数目即可,因此可将矩阵中第一行(最顶端位置的砖块)汇聚到根节点,逆向求解是,先使用一个临时副本矩阵,对此进行操作,将打击掉后的砖块状态存入并查集中,然后逆向求解每次将砖块补齐后,前后根节点连接子树的大小差值。
可以参考:打砖块:官方视频讲解
3. 代码实现
class Solution {
public int[] hitBricks(int[][] grid, int[][] hits) {
int m = grid.length, n = grid[0].length;
if (grid == null || m == 0 || n == 0) {
return new int[]{};
}
int[][] copy = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
copy[i][j] = grid[i][j];
}
}
// 1. 击碎
for (int[] hit : hits) {
copy[hit[0]][hit[1]] = 0;
}
// 2. 建图连接
int size = m * n;
UF uf = new UF(size + 1);
//顶层初始化
for (int i = 0; i < n; i++) {
if (copy[0][i] == 1) {
uf.union(size, i);
}
}
for (int i = 1; i < m; i++) {
for (int j = 0; j < n; j++) {
if (copy[i][j] == 1) {
int con = i * n + j;
//看上边
if (copy[i - 1][j] == 1) {
uf.union(con, con - n);
}
//看左边
if (j > 0 && copy[i][j - 1] == 1) {
uf.union(con, con - 1);
}
}
}
}
// 3. .逆序补回砖块
int[] dirs = {-1, 0, 1, 0, -1};
int len = hits.length;
int[] res = new int[len];
for (int i = len - 1; i >= 0; i--) {
int x = hits[i][0], y = hits[i][1];
int loc = x * n + y;
if (grid[x][y] == 0) {
continue;
}
int origin = uf.getSize(size);
if (x == 0) {
uf.union(size, loc);
}
//观察四个方向,将连接的网格合并
for (int k = 0; k < 4; k++) {
int newx = x + dirs[k], newy = y + dirs[k + 1];
if (newx < 0 || newx >= m || newy < 0 || newy >= n) {
continue;
}
int newloc = newx * n + newy;
if (copy[newx][newy] == 1) {
uf.union(loc, newloc);
}
}
int current = uf.getSize(size);
res[i] = Math.max(0, current - origin - 1);
//补上砖块
copy[x][y] = 1;
}
return res;
}
class UF {
int[] id;
int[] sz;
UF (int n) {
id = new int[n];
sz = new int[n];
for (int i = 0; i < n; i++) {
id[i] = i;
sz[i] = 1;
}
}
public int getSize(int p) {
int root = find(p);
return sz[root];
}
public int find(int p) {
if (p != id[p]) {
id[p] = find(id[p]);
}
return id[p];
}
public void union(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) return;
id[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
}
}
}
参考: