首页 > 其他分享 >LeetCode 15 - 并查集

LeetCode 15 - 并查集

时间:2022-10-05 23:11:06浏览次数:57  
标签:结点 15 parent get int 查集 ++ grid LeetCode

所谓并查集,是指支持集合的合并和查询操作的数据结构。

  • 合并:将两个集合合并为一个。
  • 查询:查询某个元素属于哪个集合,通常是返回集合内的一个代表元素(例如二叉树的所有结点可视为属于同一个集合,代表元素为根结点)。查询操作是为了判断两个元素是否在同一个集合之中

但是一般来说,并查集特指其中最常见的一种实现:不交集森林。不交集森林把 每一个集合以一棵树表示,每一个结点即是一个元素,结点保存着到它父结点的引用,数的根结点保存一个指向自身的引用。

添加操作

添加操作 MakeSet(x) 添加一个元素 x,这个元素自身就是一棵树,所以将它标记为根结点。

void MakeSet(x) {
    x.parent = x;
}

查询操作

查询操作 Find(x) 从结点 x 开始,找到它所属树的根结点。

TreeNode Find(x) {
    if (x.parent == x) return x;
    else return Find(x.parent);
}
  • 路径压缩优化

在树很不平衡时,上述查询操作可能花费 O(n) 的时间。常见优化是:在向上查询时,把路径上的每个结点都直接连接到根结点上,这样以后查询时就能直接找到根结点。

TreeNode Find(x) {
    if(x.parent == x) return x;
    x.parent = Find(x.parent);
    return x.parent;
}

合并操作

合并操作 Union(x,y) 把两个指定元素所在集合合并为一个。首先找出两个结点对应的两个根结点:

  • 如果这两个根结点是同一个,则说明这两个元素本来已经在同一个集合中;
  • 否则,使其中一个根结点成为另一个根结点的父结点。
void Union(x, y) {
    xRoot = Find(x);
    yRoot = Find(y);
    if(xRoot != yRoot)
        xRoot.parent = yRoot;
}
  • 按秩合并优化

上面的合并操作可能会使树变得很不平衡,增加树的深度。为了 控制树的深度,在合并时,比较两棵树的大小,让较小的树合并到较大的树上

秩(rank) 这个指标常用来衡量树的大小,秩的定义如下:

  • 只有一个结点的树,秩为 0。
  • 两棵树合并时:
    • 如果两棵树的秩相同,合并后树的秩为原来的秩加一。
    • 如果两棵树的秩不同,合并后树的秩为原来较大的秩。

在合并时考虑两棵树秩的大小,让较大者成为根结点,这称为按秩合并。

void MakeSet(TreeNode x) {
  x.parent = x;
  x.rank = 0;
}

void Union(TreeNode x, TreeNode y) {
  TreeNode xRoot = Find(x);
  TreeNode yRoot = Find(y);
  TreeNode large = null, small = null;
  if(xRoot != yRoot) {
    if(xRoot.rank < yRoot.rank) {
      large = yRoot;
      small = xRoot;
    } else {
      large = xRoot;
      small = yRoot;
    }
    small.parent = large; // 设置 parent
    if(large.rank == small.rank) // 设置 rank
      large.rank += 1;
  }
}

200 岛屿数量

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

方法一:DFS

遍历每一个网格:

  • 如果当前位置为陆地(grid[i][j]=='1'),则从该位置开始进行 DFS,将搜索过程中遇到的陆地全部标记为 '2' 。搜索完毕后将结果加一。
  • 如果当前位置不是陆地(grid[i][j]!='1'),则跳过。
int numOfIslands(char[][] grid) {
    int rows = grid.length, cols = grid[0].length;
    int result = 0;
    for(int i = 0; i < rows; i++) {
        for(int j = 0; j < cols; j++) {
            if(grid[i][j] == '1') {
                dfs(grid, i, j);
                result++;
            }
        }
    }
    return result;
}
void dfs(char[][] grid, int r, int c) {
    // base case:下标越界、当前位置为海水或已访问过
    if(r < 0 || r >= grid.length || c < 0 
             || c >= grid[0].length || grid[r][c] != '1')
        return;
    // 执行任务:将当前陆地标记为 '2'
    grid[r][c] = '2';
    // 访问相邻位置
    dfs(grid, r-1, c);
    dfs(grid, r+1, c);
    dfs(grid, r, c-1);
    dfs(grid, r, c+1);
}

方法二:BFS

同 DFS 的整体思路一样,也是遍历每一个网格,找到一块陆地就将其放入队列,然后开始 BFS:

  • 首先将结果加一;
  • 然后将队列中的元素依次出队,检查相邻的四个位置:
    • 如果当前位置下标越界或者是海水,或者已访问过,则跳过;
    • 如果是陆地则放入队列,并将其修改为 '2'
  • 搜索结束返回结果。
int numOfIslands(char[][] grid) {
    int rows = grid.length, cols = grid[0].length;
    int result = 0;
    // 遍历每一个位置,每遇到一块陆地就开始 BFS
    for(int i = 0; i < rows; i++) {
        for(int j = 0; j < cols; j++) {
            // 遇到陆地开始 BFS
            if(grid[i][j] == '1') {
                result++;
                Queue<int[]> queue = new LinkedList<>();
                queue.offer(new int[]{i, j});
                grid[i][j] = '2'; // 标记为已访问过
                // 开始 BFS
                while(!queue.isEmpty()) {
                    int[] cell = queue.poll();
                    int x = cell[0], y = cell[1];
                    int[] dx = {-1, 1, 0, 0};
                    int[] dy = {0, 0, -1, 1};
                    for(int k = 0; k < 4; k++) {
                        int newX = x + dx[k];
                        int newY = y + dy[k];
                        // 下标越界
                        if(newX < 0 || newY == rows || newY < 0 || newY == cols)
                            continue;
                        // 已访问过或者是海水
                        if(grid[newX][newY] != '1')
                            continue;
                        // 遇到陆地,则加入队列继续搜索
                        queue.offer(new int[]{newX, newY});
                        grid[newX][newY] = '2';
                    } // for k
                } // while(!queue.isEmpty())
            } // if(grid[i][j] == '1')
        } // for j
    } // for i
    return result;
}

方法三:并查集

扫描每一个网格,如果当前位置是陆地,则 将它与相邻四个位置的陆地在并查集中进行合并。最终岛屿数量就是 并查集中连通分量的数目

先定义并查集的类,在其中定义构造函数、查询、合并等操作。

class UnionFind {
    int count; // 集合数目,初始时每块陆地自成一个集合
    int[] parent;
    int[] rank; // 每个集合(树)的秩
    
    // 根据 grid 构造并查集
    public UnionFind(char[][] grid) {
        count = 0;
        int rows = grid.length, cols = grid[0].length;
        parent = new int[rows * cols];
        rank = new int[rows * cols];
        // 遍历每个网格
        for(int i = 0; i < rows; i++) {
            for(int j = 0; j < cols; j++) {
                // 每一块陆地作为一个结点
                if(grid[i][j] == '1') {
                    parent[i * cols + j] = i * cols + j;
                    count++;
                }
                rank[i * cols + j] = 0;
            }
        }
    }
    
    public int find(int i) {
        if(i != parent[i])
            parent[i] = find(parent[i]);
        return parent[i];
    }
    
    public void union(int x, int y) {
        int xRoot = find(x);
        int yRoot = find(y);
        // 按秩合并优化
        if(xRoot != yRoot) {
            if(rank[xRoot] > rank[yRoot])
                parent[yRoot] = xRoot;
            else if(rank[xRoot] < rank[yRoot])
                parent[xRoot] = yRoot;
            else {
                parent[yRoot] = xRoot;
                rank[xRoot] += 1;
            }
            // 合并后数量减一
            count--;
        }
    }
    
    public int getCount() {
        return count;
    }
}

然后利用并查集统计连通分量数目。

int numOfIslands(char[][] grid) {
    int rows = grid.length, cols = grid[0].length;
    int result = 0;
    // 构造并查集对象
    UnionFind uf = new UnionFind(grid);
    // 遍历整个 grid
    for(int i = 0; i < rows; i++) {
        for(int j = 0; j < cols; j++) {
            // 发现陆地,则将该陆地与相邻陆地合并
            if(grid[i][j] == '1') {
                grid[i][j] = '0';
                int[] dx = {-1, 1, 0, 0};
                int[] dy = {0, 0, -1, 1};
                // 查看相邻区块是否为陆地,是则合并
                for(int k = 0; k < 4; k++) {
                    int newX = i + dx[k];
                    int newY = j + dy[k];
                    // 当前位置下标越界或不为陆地
                    if(newX < 0 || newX >= rows || newY < 0 
                       		|| newY >= cols || grid[newX][newY] != '1')
                        continue;
                    // 将该相邻位置与当期位置对应的集合合并
                    uf.union(i * cols + j, newX * cols + newY);
                } // for k
            } // for if
        } // for j
    } // for i
    return uf.getCount();
}

547 省份数量

n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。

省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。

给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。

返回矩阵中 省份 的数量。

本题实际上是求图中的连通分量数。

方法一:DFS

遍历所有城市,对于每个城市:

  • 如果该城市尚未被访问过,则从该城市开始 DFS:
    • 通过矩阵 isConnected 得到与该城市直接相连的城市有哪些,这些城市与当前城市同属一个连通分量;
    • 然后对这些城市继续 DFS,直到同一个连通分量的所有城市都被访问到,即可得到一个省份。
  • 遍历完所有城市后,即可得到连通分量总数。
int numOfIsProvinces(int[][] isConnected) {
    int cityNum = isConnected.length;
    boolean[] visited = new boolean[cityNum];
    int result = 0;
    for(int i = 0; i < cityNum; i++) {
        if(!visited[i]) {
            visited[i] = true;
            // 从当前城市开始找到一个连通分量
            dfs(isConnected, visited, i);
            result++;
        }
    }
    return result;
}

void dfs(int[][] isConnected, boolean[] visited, int city) {
    // 遍历每个城市
    for(int i = 0; i < isConnected.length; i++) {
        // 找到相邻城市,从它开始继续搜索
     	if(isConnected[city][i] == 1 && !visited[i]){
            visited[i] = true;
            dfs(isConnected, visited, i);
        }
    }
}

方法二:BFS

和 DFS 的整体思路一样,也是遍历每个城市,从当前城市搜索出整个连通分量,在搜索过程中将遇到的相连的城市标记为已访问。不同之处在于搜索这个连通分量的方式

int numOfProvinces(int[][] isConnected) {
    int cityNum = isConnected.length;
    boolean[] visited = new boolean[cityNum];
    Queue<Integer> queue = new LinkedList<>();
    // 遍历每一个城市,搜索一个连通分量
    int result = 0;
    for(int i = 0; i < cityNum; i++) {
        if(!visited[i]) {
            queue.offer(i);
            while(!queue.isEmpty()) {
             	int curCity = queue.poll();
                visited[curCity] = true; // 出队后标记为已访问
                // 让相邻城市入队,继续下一轮搜索
                for(int j = 0; j < cityNum; j++) {
                    if(isConnected[curCity][j] == 1 && !visited[j]) {
                        queue.offer(j);
                    }
                }
            } // while
            result++;
        }
    }
    return result;
}

方法三:并查集

计算连通分量数的另一个方法是使用并查集:

  • 初始时,每个城市都自成一个连通分量。
  • 遍历每个城市与其他每个城市的连接关系(isConnected 矩阵),如果连个城市之间直接相连,则它们属于同一个连通分量,对它们进行合并
  • 遍历整个矩阵之后,计算 连通分量数,即为省份总数。
int[] parent; // 并查集

int numOfProvinces(int[][] isConnected) {
  int cityNum = isConnected.length;
  // 初始化并查集
  parent = new int[cityNum];
  for (int i = 0; i < cityNum; i++) {
    parent[i] = i;
  }
  // 遍历每个城市,相连城市进行合并
  for (int i = 0; i < cityNum; i++) {
    for (int j = i + 1; j < cityNum; j++) {
      if (isConnected[i][j] == 1)
        // 每次合并都会让连通分量数减一
        union(i, j);
    }
  }
  // 统计连通分量数
  int result = 0;
  for (int i = 0; i < cityNum; i++)
    // 根结点数量 == 连通分量数
    if (parent[i] == i) result++;
  return result;
}

int find(int index) {
  if (parent[index] != index) parent[index] = find(parent[index]);
  return parent[index];
}

void union(int x, int y) {
  parent[find(x)] = find(y);
}

·

684 冗余连接

给定往一棵 n 个节点 (节点值 1~n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1 到 n 中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组 edges ,edges[i] = [ai, bi] 表示图中在 aibi 之间存在一条边。

请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案,则返回数组 edges 中最后出现的边。

方法:并查集

树是一个连通且无环的无向图,在树中加入一条新边就会出现环,这条边就是本题需要寻找的边。

可以通过并查集寻找这条边。初始时,每个结点都属于不同的连通分量。遍历每条边,判断这条边连接的两个顶点是否属于相同的连通分量。

  • 如果属于不同的连通分量,说明在遍历到这条边之前,这两个结点是不连通的,进一步说明不是这条边导致了环的出现。合并这两个顶点分属的连通分量。
  • 如果属于相同的连通分量,说明在遍历到这条边之前,这两个结点已经连通,进一步说明这条边是导致环出现的那条边。
int[] parent;

int[] findRedundantConnection(int[][] edges) {
  int n = edges.length;
  parent = new int[n + 1];
  // 构建无交集森林
  for (int i = 1; i <= n; i++) {
    parent[i] = i;
  }
  // 遍历每条边
  for (int i = 0; i < n; i++) {
    int[] edge = edges[i];
    int node1 = edge[0], node2 = edge[1];
    // 属于不同的连通分量
    if (find(node1) != find(node2)) union(node1, node2);
    else // 属于相同的连通分量,则当前边就是造成环的边
    return edge;
  }
  return new int[0];
}

// 并查集——查询操作,返回根结点
int find(int index) {
  // 路径压缩优化
  if (parent[index] != index) parent[index] = find(parent[index]);
  return parent[index];
}
// 并查集——合并操作,将 index1 的根结点设置为 index2 的根结点
void union(int index1, int index2) {
  parent[find(index1)] = find(index2);
}

399 除法求值

给你一个变量对数组 equations 和一个实数值数组 values作为已知条件,其中 equations[i] = [Ai, Bi]values[i] 共同表示等式 Ai / Bi = values[i] 。每个 AiBi 是一个表示单个变量的字符串。

另有一些以数组 queries 表示的问题,其中 queries[j] = [Cj, Dj] 表示第 j 个问题,请你根据已知条件找出 Cj / Dj = ? 的结果作为答案。

返回 所有问题的答案 。如果存在某个无法确定的答案,则用 -1.0 替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串,也需要用 -1.0 替代这个答案。

方法一:并查集

构建有向图

题目给出的 equationsvalues 可以表示成一个图,equations 中出现的变量就是图的结点,分子分母的关系可以表示成一个有向关系,values 对应的值就是有向边的权值。

路径压缩

在查询一个结点的根结点时,将路径上的所有点的父结点都设为这个根结点,这就是路径压缩。路径压缩可以将一个集合中的所有结点的值都转换成根结点的倍数,从而可以计算出比值。在路径压缩中需要不断更新边上的权值(因为压缩后结点直接指向了根结点,有向边发生变动),过程如下:

class Solution {
   
	  // 并查集内部类
    private class UnionFind {
        private int[] parent;
        private double[] weight;
        public UnionFind(int n) {
            this.parent = new int[n];
            this.weight = new double[n];
            for(int i = 0; i < n; i++) {
                parent[i] = i;
                weight[i] = 1.0d;
            }
        }
        public void union(int x, int y, double value) {
            int rootX = find(x);
            int rootY = find(y);
            if(rootX == rootY)
                return;
            parent[rootX] = rootY;
            // 推导关系见下图
            weight[rootX] = weight[y] * value / weight[x];
        }
        public int find(int x) {
            if(x != parent[x]) {
                int temp_parent = parent[x];
                parent[x] = find(temp_parent);
                weight[x] *= weight[temp_parent];
            }
            return parent[x];
        }
        public double isConnected(int x, int y) {
            int rootX = find(x);
            int rootY = find(y);
            if(rootX == rootY)
                return weight[x] / weight[y];
            else
                return -1.0d;
        }
    }

    double[] calcEquation(List<List<String>> equations, double[] values,
                     List<List<String>> queries) {
        int n = equations.size();
        UnionFind uf = new UnionFind(2 * n);
        // 第一步:预处理,将变量的值与 id 进行映射,使得并查集的底层使用数组实现,方便编码
        Map<String, Integer> hashMap = new HashMap<>();
        int id = 0;
        for(int i = 0; i < n; i++) {
            List<String> equation = equations.get(i);
            String var1 = equation.get(0);
            String var2 = equation.get(1);
            if(!hashMap.containsKey(var1)) {
                hashMap.put(var1, id);
                id++;
            }
            if(!hashMap.containsKey(var2)) {
                hashMap.put(var2, id);
                id++;
            }
            uf.union(hashMap.get(var1), hashMap.get(var2), values[i]);
        }
        // 第二步:做查询
        int m = queries.size();
        double[] result = new double[m];
        for(int i = 0; i < m; i++) {
            String var1 = queries.get(i).get(0);
            String var2 = queries.get(i).get(1);
            Integer id1 = hashMap.get(var1);
            Integer id2 = hashMap.get(var2);
            if(id1 == null || id2 == null)
                result[i] = -1.0d;
            else
                result[i] = uf.isConnected(id1, id2);
        }
        return result;
    }
}

方法二:BFS

用同样的方式建图,然后对于每个查询,就可以从起点出发,通过 BFS 不断更新起点与当前点之间的路径长度,直到搜索到终点为止。

class Pair {
    int index;
    double value;
    Pair(int index, double value) {
        this.index = index;
        this.value = value;
    }
}

class Solution {
    double calcEquation(List<List<String>> equations, double[] values, 
                   List<List<String>> queries) {
        int id = 0;
        // 将字符串映射为 id
        Map<String, Integer> map = new HashMap<>();
        int n = equations.size();
        for(int i = 0; i < n; i++) {
            String node1 = equations.get(i).get(0);
            String node2 = equations.get(i).get(1);
            if(!map.containsKey(node1))
                map.put(node1, id++);
            if(!map.containsKey(node2))
                map.put(node2, id++);
        }
        // 建图:用 List<Pair> 类型的数组来存储每个点相连的所有点及其比值
        List<Pair>[] edges = new List[id];
        for(int i = 0; i < id; i++)
            edges[i] = new ArrayList<Pair>();
        for(int i = 0; i < n; i++) {
            String node1 = equations.get(i).get(0);
            String node2 = equations.get(i).get(1);
            Integer id1 = map.get(node1);
            Integer id2 = map.get(node2);
            edges[id1].add(new Pair(id2, values[i]));
            edges[id2].add(new Pair(id1, 1.0/values[i]));
        }
        // BFS,为每个查询求得答案
        int m = queries.size();
        double[] result = new double[m];
        for(int i = 0; i < m; i++) {
            String node1 = queries.get(i).get(0);
            String node2 = queries.get(i).get(1);
            double ans = -1.0;
            if(map.containsKey(node1) && map.containsKey(node2)) {
                int id1 = map.get(node1);
                int id2 = map.get(node2);
                if(id1 == id2) {
                    ans = 1.0;
                } else {
                    Queue<Integer> queue = new LinkedList<>();
                    queue.offer(id1); // 从第一个点开始搜索
                    double[] ratios = new double[id];
                    Arrays.fill(ratios, -1.0);
                    ratios[id1] = 1.0;

                    while(!queue.isEmpty() && ratios[id2] < 0) {
                        int x = queue.poll();
                        // 访问相邻结点,直到搜索到 id2
                        for(Pair pair : edges[x]) {
                            int y = pair.index;
                            double val = pair.value;
                            if(ratios[y] < 0) {
                                rratios[y] = ratios[x] * val;
                                queue.offer(y);
                            }
                        }
                    }
                    ans = ratios[id2]; // 找到一个答案
                }
            }
            result[i] = ans;
        }
        return result;
    }
}

标签:结点,15,parent,get,int,查集,++,grid,LeetCode
From: https://www.cnblogs.com/lzh1995/p/16756726.html

相关文章

  • day15
    二叉树的层序遍历模板//利用一个指针来记录每一层的节点的数量(很关键)classSolution{publicList<Integer>rightSideView(TreeNoderoot){List<Intege......
  • 15.7 os.path模块的常用方法_课堂案例 重要方法wark(path) 方法
     importos.pathprint('1.',os.path.abspath('demo13.py'))#获取文件或目录绝对路径print('2.',os.path.exists('demo13.py'),os.pa......
  • 【教奶奶学SQL】(task6)秋招秘籍A(leetcode刷题)
    学习总结(1)敲黑板:练习第六题再看看,还欠最后1题困难题。(2)​​​猴子题解电子书​​文章目录​​学习总结​​​​练习一:各部门工资最高的员工(leetcode184难度:中等)​​​​......
  • 学习记录15集合
    集合什么是集合?同时存储多个元素,需要怎么做?以前学习过数组,可数组的使用是有弊端的——数组的长度是固定的集合与数组一样,都可以被看作是一个容器。在没有添加元素的情......
  • 【C语言_15】自定义函数和math库函数详解篇!
    一.函数的概念1.什么是函数?函数就是一段封装好的,可以重复使用的代码,它使得我们的程序更加模块化,不需要编写大量重复的代码。函数可以提前保存起来,并给它起一个独一无二的名......
  • 15_AAC编码实战
    本文将分别通过命令行、编程2种方式进行AAC编码实战,使用的编码库是libfdk_aac。要求fdk-aac对输入的PCM数据是有参数要求的,如果参数不对,就会出现以下错误:[libfdk_aac......
  • luogu P3644 [APIO2015] 八邻旁之桥
    Link题解首先忽略掉同侧的询问。对于\(K=1\),它其实就是问一个点到其它点的距离之和最小值,直接找到中位数然后计算即可。对于一条路线,我们可以发现,如果建的桥里这两个......
  • LeetCode 12 - 贪心
    455.分发饼干对每个孩子i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干j,都有一个尺寸s[j] 。如果s[j] >=g[i],我们可以将这个饼干j......
  • LeetCode 09 - 滑动窗口
    3.无重复字符的最长子串方法:滑动窗口使用左右两指针表示一个窗口,窗口中没有重复字符。每次向右移动right指针以扩大窗口范围,在该过程中:如果最新添加的字符在窗口中......
  • LeetCode 10 - 双指针
    11.盛最多水的容器方法:双指针用两个指针指向「容器」的左右边界,每次移动数字较小那一侧的指针,直到两指针相遇。这样移动指针的正确性是可以保证的。publicintmaxAre......