56.【必备】并查集-下

本文的网课内容学习自B站左程云老师的算法详解课程,旨在对其中的知识进行整理和分享~

网课链接:算法讲解057【必备】并查集-下_哔哩哔哩_bilibili

一.移除最多的同行同列石头

题目:移除最多的同行或同列石头

算法原理

  • 算法原理概述
    • 该算法利用并查集(Union - Find)数据结构来解决问题。核心思路是将同行或同列的石头看作是相关联的元素,通过并查集的合并操作将它们归为同一集合,最后计算出可以移除的石头数量。
  • 具体步骤
    • 初始化并查集相关数据结构(build方法)
      • build方法中,首先清空用于记录每行和每列第一次遇到石头编号的HashMaprowFirstcolFirst)。
      • 然后将并查集的每个元素(这里对应每块石头的编号)的父节点初始化为自身,即father[i]=i,并且初始化集合数量sets为石头的总数n
    • 查找操作(find方法)
      • find方法中,如果当前元素i的父节点不是它自身,就递归地查找其父节点的父节点,直到找到根节点。然后将当前元素的父节点更新为根节点,最后返回根节点。这一步实现了路径压缩,提高后续查找效率。
    • 合并操作(union方法)
      • union方法中,首先通过find方法找到两个元素xy的根节点fxfy
      • 如果这两个根节点不相同,就将fx的父节点设置为fy,表示将x所在的集合合并到y所在的集合中,并且集合数量sets减1。
    • 处理石头关系并计算可移除石头数量(removeStones方法)
      • removeStones方法中,首先获取石头的总数n,然后调用build方法进行初始化。
      • 接着遍历每块石头,对于每块石头,获取其行row和列col
      • 如果row不在rowFirst中(表示第一次遇到这一行的石头),就将这一行对应的石头编号i记录到rowFirst中。否则,就调用union方法将当前石头编号i与这一行第一次遇到的石头编号(rowFirst.get(row))进行合并。
      • 同样地,对于列,如果col不在colFirst中,就将这一列对应的石头编号i记录到colFirst中;否则,调用union方法将当前石头编号i与这一列第一次遇到的石头编号(colFirst.get(col))进行合并。
      • 最后,可移除的石头数量等于石头总数n减去最终的集合数量sets。因为在一个集合中的石头,除了代表这个集合的一块石头(根节点对应的石头),其他石头都可以被移除。

代码实现

import java.util.HashMap;

// 移除最多的同行或同列石头
// n 块石头放置在二维平面中的一些整数坐标点上。每个坐标点上最多只能有一块石头
// 如果一块石头的 同行或者同列 上有其他石头存在,那么就可以移除这块石头
// 给你一个长度为 n 的数组 stones ,其中 stones[i] = [xi, yi] 表示第 i 块石头的位置
// 返回 可以移除的石子 的最大数量。
// 测试链接 : https://leetcode.cn/problems/most-stones-removed-with-same-row-or-column/
public class Code01_MostStonesRemovedWithSameRowOrColumn {

    // key : 某行
    // value : 第一次遇到的石头编号
    public static HashMap<Integer, Integer> rowFirst = new HashMap<Integer, Integer>();

    public static HashMap<Integer, Integer> colFirst = new HashMap<Integer, Integer>();

    public static int MAXN = 1001;

    public static int[] father = new int[MAXN];

    public static int sets;

    public static void build(int n) {
        rowFirst.clear();
        colFirst.clear();
        for (int i = 0; i < n; i++) {
            father[i] = i;
        }
        sets = n;
    }

    public static int find(int i) {
        if (i != father[i]) {
            father[i] = find(father[i]);
        }
        return father[i];
    }

    public static void union(int x, int y) {
        int fx = find(x);
        int fy = find(y);
        if (fx != fy) {
            father[fx] = fy;
            sets--;
        }
    }

    public static int removeStones(int[][] stones) {
        int n = stones.length;
        build(n);
        for (int i = 0; i < n; i++) {
            int row = stones[i][0];
            int col = stones[i][1];
            if (!rowFirst.containsKey(row)) {
                rowFirst.put(row, i);
            } else {
                union(i, rowFirst.get(row));
            }
            if (!colFirst.containsKey(col)) {
                colFirst.put(col, i);
            } else {
                union(i, colFirst.get(col));
            }
        }
        return n - sets;
    }

}

二.找出知晓秘密的所有专家

题目:找出知晓秘密的所有专家

算法原理

  • 算法原理概述
    • 这个算法基于并查集来处理专家之间秘密的传播问题。通过对会议按照时间进行排序,逐步处理每个时刻的会议,在会议中合并专家所属的集合,并根据秘密传播规则更新集合的秘密属性,最后找出知晓秘密的专家。
  • 具体步骤
    • 初始化并查集和秘密属性(build方法)
      • build方法中,对于所有专家(从0到n - 1),将每个专家的父节点初始化为自身(father[i]=i),并且将每个专家所在集合的秘密属性初始化为falsesecret[i]=false)。
      • 然后将专家first的父节点设置为专家0(father[first]=0),表示专家first与专家0在初始时的联系,并且将专家0的秘密属性设置为truesecret[0]=true),因为专家0最初知晓秘密。
    • 查找操作(find方法)
      • find方法中,如果当前专家i的父节点不是它自身,就递归地查找其父节点的父节点,直到找到根节点。然后将当前专家的父节点更新为根节点,最后返回根节点。这一步实现了路径压缩,提高后续查找效率。
    • 合并操作(union方法)
      • union方法中,首先通过find方法找到两个专家xy的根节点fxfy
      • 如果这两个根节点不相同,就将fx的父节点设置为fy,表示将x所在的集合合并到y所在的集合中。同时,更新合并后集合(以fy为代表元素)的秘密属性,将其设置为原来两个集合秘密属性的逻辑或(secret[fy]|=secret[fx]),因为只要其中一个集合有秘密,合并后的集合就有秘密。
    • 处理会议过程
      • findAllPeople方法中,首先调用build方法进行初始化,然后对会议数组meetings按照会议时间进行排序(Arrays.sort(meetings, (a, b) -> a[2]-b[2]);)。
      • 接着遍历排序后的会议数组。对于每个时刻的会议(通过lr指针来界定同一时刻的会议范围),先对这个时刻的所有会议执行合并操作(union(meetings[i][0], meetings[i][1])),将参会的专家合并到同一个集合中。
      • 然后,对于这个时刻的每个会议中的专家,如果该专家所属集合(通过find方法找到根节点)的秘密属性为false,就将该专家的父节点重新设置为自身,这相当于将这个专家从可能知晓秘密的集合中分离出来,因为这个时刻的会议没有让他们知晓秘密。
      • 最后,遍历所有专家,将所属集合秘密属性为true(通过find方法找到根节点对应的秘密属性)的专家加入结果列表ans,并返回这个列表。

代码实现

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

// 找出知晓秘密的所有专家
// 给你一个整数 n ,表示有 n 个专家从 0 到 n - 1 编号
// 另外给你一个下标从 0 开始的二维整数数组 meetings
// 其中 meetings[i] = [xi, yi, timei] 表示专家 xi 和专家 yi 在时间 timei 要开一场会
// 一个专家可以同时参加 多场会议 。最后,给你一个整数 firstPerson
// 专家 0 有一个 秘密 ,最初,他在时间 0 将这个秘密分享给了专家 firstPerson
// 接着,这个秘密会在每次有知晓这个秘密的专家参加会议时进行传播
// 更正式的表达是,每次会议,如果专家 xi 在时间 timei 时知晓这个秘密
// 那么他将会与专家 yi 分享这个秘密,反之亦然。秘密共享是 瞬时发生 的
// 也就是说,在同一时间,一个专家不光可以接收到秘密,还能在其他会议上与其他专家分享
// 在所有会议都结束之后,返回所有知晓这个秘密的专家列表
// 你可以按 任何顺序 返回答案
// 链接测试 : https://leetcode.cn/problems/find-all-people-with-secret/
public class Code02_FindAllPeopleWithSecret {

    public static int MAXN = 100001;

    public static int[] father = new int[MAXN];

    // 集合的标签信息 : 设置集合的一些属性
    // 属性在哪?secret[代表元素] 代表集合的属性
    public static boolean[] secret = new boolean[MAXN];

    public static void build(int n, int first) {
        for (int i = 0; i < n; i++) {
            father[i] = i;
            secret[i] = false;
        }
        father[first] = 0;
        secret[0] = true;
    }

    public static int find(int i) {
        if (i != father[i]) {
            father[i] = find(father[i]);
        }
        return father[i];
    }

    public static void union(int x, int y) {
        int fx = find(x);
        int fy = find(y);
        if (fx != fy) {
            father[fx] = fy;
            secret[fy] |= secret[fx];
        }
    }

    // 会议排序 : m * log m
    // 处理过程 : O(m)
    // 收集答案 : O(n)
    public static List<Integer> findAllPeople(int n, int[][] meetings, int first) {
        build(n, first);
        // {0 : 专家   1 : 专家编号   2 : 时刻}
        Arrays.sort(meetings, (a, b) -> a[2] - b[2]);
        int m = meetings.length;
        for (int l = 0, r; l < m;) {
            r = l;
            while (r + 1 < m && meetings[l][2] == meetings[r + 1][2]) {
                r++;
            }
            // l....r这些会议,一定是一个时刻
            for (int i = l; i <= r; i++) {
                union(meetings[i][0], meetings[i][1]);
            }
            // 有小的撤销行为,但这不是可撤销并查集
            // 只是每一批没有知道秘密的专家重新建立集合而已
            for (int i = l, a, b; i <= r; i++) {
                a = meetings[i][0];
                b = meetings[i][1];
                if (!secret[find(a)]) {
                    father[a] = a;
                }
                if (!secret[find(b)]) {
                    father[b] = b;
                }
            }
            l = r + 1;
        }
        List<Integer> ans = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            if (secret[find(i)]) {
                ans.add(i);
            }
        }
        return ans;
    }

}

 三.好路径的数目

题目:好路径的数目

算法原理

  • 算法原理概述
    • 该算法主要基于并查集来计算树中好路径的数目。通过对边按照端点节点值的最大值进行排序,逐步合并节点所在的集合,在合并过程中根据节点值的关系来确定代表节点,并计算满足好路径条件的路径数量。
  • 具体步骤
    • 初始化并查集相关数据结构(build方法)
      • build方法中,对于树中的每个节点(从0到n - 1),将其父节点初始化为自身(father[i]=i),并且将每个节点所在集合中最大值的次数初始化为1(maxcnt[i]=1)。
    • 查找操作(find方法)
      • find方法中,如果当前节点i的父节点不是它自身,就递归地查找其父节点的父节点,直到找到根节点。然后将当前节点的父节点更新为根节点,最后返回根节点。这一步实现了路径压缩,提高后续查找效率。
    • 合并操作(union方法)
      • union方法中,首先通过find方法找到两个节点xy的根节点fxfy,这里fxfy分别是xy所在集团的代表节点,同时也是所在集团的最大值下标。
      • 如果vals[fx]>vals[fy],则将y所在集团的父节点设置为x所在集团的代表节点(father[fy]=fx)。
      • 如果vals[fx]<vals[fy],则将x所在集团的父节点设置为y所在集团的代表节点(father[fx]=fy)。
      • 如果vals[fx]=vals[fy],这意味着两个集团的最大值相同。此时计算好路径的数量为两个集团中最大值的次数的乘积(path = maxcnt[fx]*maxcnt[fy]),然后将y所在集团的父节点设置为x所在集团的代表节点(father[fy]=fx),并更新x所在集团中最大值的次数(maxcnt[fx]+=maxcnt[fy])。最后返回计算出的好路径数量path
    • 计算好路径数量(numberOfGoodPaths方法)
      • numberOfGoodPaths方法中,首先获取节点的数量n,然后调用build方法进行初始化。
      • 将结果ans初始化为n,这是因为每个节点自身可视为一条好路径。
      • 对边数组edges按照边两端节点值的最大值进行排序(Arrays.sort(edges, (e1, e2) ->(Math.max(vals[e1[0]], vals[e1[1]]) - Math.max(vals[e2[0]], vals[e2[1]])))。
      • 然后遍历排序后的边数组,对于每条边edge,调用union方法合并边两端的节点所在的集合,并将union方法返回的好路径数量加到结果ans中。最后返回ans,即为好路径的总数。

代码实现

import java.util.Arrays;

// 好路径的数目
// 给你一棵 n 个节点的树(连通无向无环的图)
// 节点编号从0到n-1,且恰好有n-1条边
// 给你一个长度为 n 下标从 0 开始的整数数组 vals
// 分别表示每个节点的值。同时给你一个二维整数数组 edges
// 其中 edges[i] = [ai, bi] 表示节点 ai 和 bi 之间有一条 无向 边
// 好路径需要满足以下条件:开始和结束节点的值相同、 路径中所有值都小于等于开始的值
// 请你返回不同好路径的数目
// 注意,一条路径和它反向的路径算作 同一 路径
// 比方说, 0 -> 1 与 1 -> 0 视为同一条路径。单个节点也视为一条合法路径
// 测试链接 : https://leetcode.cn/problems/number-of-good-paths/
public class Code03_NumberOfGoodPaths {

    public static int MAXN = 30001;

    // 需要保证集合中,代表节点的值,一定是整个集合的最大值
    public static int[] father = new int[MAXN];

    // 集合中最大值的次数,也就是 集合中代表节点的值有几个
    public static int[] maxcnt = new int[MAXN];

    public static void build(int n) {
        for (int i = 0; i < n; i++) {
            father[i] = i;
            maxcnt[i] = 1;
        }
    }

    // 这个并查集的优化只来自扁平化
    public static int find(int i) {
        if (i != father[i]) {
            father[i] = find(father[i]);
        }
        return father[i];
    }

    // 核心!
    // 注意以下的写法!
    // 谁的值大,谁做代表节点
    // 同时注意 maxcnt 的更新
    public static int union(int x, int y, int[] vals) {
        // fx : x所在集团的代表节点,同时也是x所在集团的最大值下标
        int fx = find(x);
        // fy : y所在集团的代表节点,同时也是y所在集团的最大值下标
        int fy = find(y);
        int path = 0;
        if (vals[fx] > vals[fy]) {
            father[fy] = fx;
        } else if (vals[fx] < vals[fy]) {
            father[fx] = fy;
        } else {
            // 两个集团最大值一样!
            path = maxcnt[fx] * maxcnt[fy];
            father[fy] = fx;
            maxcnt[fx] += maxcnt[fy];
        }
        return path;
    }

    public static int numberOfGoodPaths(int[] vals, int[][] edges) {
        int n = vals.length;
        build(n);
        int ans = n;
        // 课上重点讲这个核心排序!
        // 处理边的时候,依次从小节点往大节点处理
        Arrays.sort(edges, (e1, e2) -> (Math.max(vals[e1[0]], vals[e1[1]]) - Math.max(vals[e2[0]], vals[e2[1]])));
        for (int[] edge : edges) {
            ans += union(edge[0], edge[1], vals);
        }
        return ans;
    }

    // 课上讲解的例子1和例子2
    public static void main(String[] args) {
        // 课上例子1
        //              0  1  2  3  4  5  6  7
        int[] vals1 = { 2, 1, 1, 2, 2, 1, 1, 2 };
        int[][] edges1 = {
                { 0, 1 },
                { 0, 2 },
                { 1, 3 },
                { 2, 4 },
                { 2, 5 },
                { 5, 6 },
                { 6, 7 } };
        System.out.println(numberOfGoodPaths(vals1, edges1));

        // 课上例子2
        //              0  1  2  3  4  5  6  7  8  9 10 11 12
        int[] vals2 = { 1, 2, 2, 3, 1, 2, 2, 1, 1, 3, 3, 3, 3 };
        int[][] edges2 = {
                { 0, 1 },
                { 0, 2 },
                { 0, 3 },
                { 1, 4 },
                { 4, 7 },
                { 4, 8 },
                { 3, 5 },
                { 3, 6 },
                { 6, 9 },
                { 6, 10 },
                { 6, 11 },
                { 9, 12 } };
        System.out.println(numberOfGoodPaths(vals2, edges2));
    }

}

四.尽量减少恶意软件的传播

题目:尽量减少恶意软件的传播 II

算法原理

  • 算法原理概述
    • 该算法主要利用并查集结构来处理恶意软件传播的优化问题。通过对网络中的普通节点进行并查集合并,确定与病毒节点相邻的普通节点集合的感染源头,进而统计每个病毒源头节点被移除时能够拯救的节点数量,最终找出能使感染节点数量最小化的病毒源头节点。
  • 具体步骤
    • 初始化相关操作(build方法)

      • 目的:初始化算法所需的各种数据结构,为后续处理做准备。
      • 步骤
        • 对于从0到(n - 1)的每个节点(i):
          • 将表示节点是否感染病毒的(virus[i])设为(false),表示每个源头节点删除时能拯救节点数量的(cnts[i])设为0,集合感染源头标记(infect[i])设为 - 1(表示未发现源头),集合大小(size[i])设为1,在并查集中将节点(i)的父节点(father[i])设为(i)自身。
          • 然后遍历初始被感染的节点数组(initial),将其中节点对应的(virus)标记设为(true)。
    • 并查集查找操作(find方法)

      • 目的:找到节点在并查集中的根节点,同时进行路径压缩以提高后续操作效率。
      • 步骤:如果当前节点(i)的父节点不是它自身,就递归地查找其父节点的父节点,直到找到根节点。找到根节点后,将当前节点(i)的父节点更新为根节点,最后返回根节点。
    • 并查集合并操作(union方法)

      • 目的:将两个节点所在的集合合并为一个集合。
      • 步骤
        • 首先通过(find)方法分别找到节点(x)和(y)在并查集中的根节点(fx)和(fy)。
        • 如果(fx)和(fy)不相等,将(fx)的父节点设置为(fy),并且更新(y)所在集合(以(fy)为代表节点)的大小,即(size[fy]+ = size[fx])。
    • 算法核心处理过程(minMalwareSpread方法)

      • 普通节点合并
        • 目的:将网络中的非病毒节点根据连接关系进行合并,构建并查集结构。
        • 步骤:获取网络节点数量(n)后调用(build)方法初始化。然后通过两层嵌套的(for)循环遍历邻接矩阵(graph)。对于两个非病毒节点((graph[i][j]=1)且(virus[i]=false)且(virus[j]=false)),调用(union)方法将它们合并到同一个集合中。
      • 确定集合感染源头
        • 目的:确定每个普通节点集合可能的感染源头。
        • 步骤:对于每个初始被感染的病毒节点(sick),再遍历所有节点(neighbor)((sick\neq neighbor)且(virus[neighbor]=false)且(graph[sick][neighbor]=1))。找到(neighbor)所在集合的代表节点(fn)(通过(find)方法)。如果(infect[fn]= - 1),表示该集合尚未确定感染源头,将(infect[fn])设为(sick);如果(infect[fn]\neq - 2)且(infect[fn]\neq sick),表示该集合有不同的感染源头,将(infect[fn])设为 - 2,表示该集合有多个感染源头,无法拯救。
      • 统计拯救数据
        • 目的:统计每个病毒源头节点被移除时能够拯救的节点数量。
        • 步骤:遍历所有节点(i),如果(i)是其所在集合的代表节点((i == find(i)))且(infect[i]\geq0)(表示该集合有唯一感染源头),将(infect[i])对应的(cnts)值加上该集合的大小(size[i])。
      • 确定最终结果
        • 目的:找出能使感染节点数量最小化的病毒源头节点。
        • 步骤:先对初始被感染的节点数组(initial)进行排序。将结果(ans)初始化为(initial),将最大拯救节点数量(max)初始化为(cnts[ans])。然后再次遍历(initial),如果(cnts[i]>max),更新(ans)为(i),更新(max)为(cnts[i])。最后返回(ans)。

代码实现

import java.util.Arrays;

// 尽量减少恶意软件的传播 II
// 给定一个由 n 个节点组成的网络,用 n x n 个邻接矩阵 graph 表示
// 在节点网络中,只有当 graph[i][j] = 1 时,节点 i 能够直接连接到另一个节点 j。
// 一些节点 initial 最初被恶意软件感染。只要两个节点直接连接,
// 且其中至少一个节点受到恶意软件的感染,那么两个节点都将被恶意软件感染。
// 这种恶意软件的传播将继续,直到没有更多的节点可以被这种方式感染。
// 假设 M(initial) 是在恶意软件停止传播之后,整个网络中感染恶意软件的最终节点数。
// 我们可以从 initial 中删除一个节点,
// 并完全移除该节点以及从该节点到任何其他节点的任何连接。
// 请返回移除后能够使 M(initial) 最小化的节点。
// 如果有多个节点满足条件,返回索引 最小的节点 。
// initial 中每个整数都不同
// 测试链接 : https://leetcode.cn/problems/minimize-malware-spread-ii/
public class Code04_MinimizeMalwareSpreadII {

    // 如果测试数据变大,就改变这个值
    public static int MAXN = 301;

    // [3,6,103]
    // virus[3] = true;
    // virus[103] = true;
    // 方便查询
    public static boolean[] virus = new boolean[MAXN];

    // 每个源头点删掉的话,能拯救多少点的数据
    public static int[] cnts = new int[MAXN];

    // 集合的标签 : 集合的感染点是什么点
    // a : 代表点,整个集合源头是 infect[a]
    // infect[a] == -1,目前这个集合没有发现源头
    // infect[a] >= 0,目前这个集合源头是 infect[a]
    // infect[a] == -2,目前这个集合源头不止一个,已经无法拯救了!
    public static int[] infect = new int[MAXN];

    // 并查集固有信息
    public static int[] father = new int[MAXN];

    // 集合的标签 : 集合的大小是多少
    public static int[] size = new int[MAXN];

    // 集合一定只放普通点,源头点根本不参与集合,也不是元素!

    public static void build(int n, int[] initial) {
        for (int i = 0; i < n; i++) {
            virus[i] = false;
            cnts[i] = 0;
            infect[i] = -1;
            size[i] = 1;
            father[i] = i;
        }
        for (int i : initial) {
            virus[i] = true;
        }
    }

    public static int find(int i) {
        if (i != father[i]) {
            father[i] = find(father[i]);
        }
        return father[i];
    }

    public static void union(int x, int y) {
        int fx = find(x);
        int fy = find(y);
        if (fx != fy) {
            father[fx] = fy;
            size[fy] += size[fx];
        }
    }

    public static int minMalwareSpread(int[][] graph, int[] initial) {
        int n = graph.length;
        build(n, initial);
        // 不是病毒的点,普通点合并!
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (graph[i][j] == 1 && !virus[i] && !virus[j]) {
                    union(i, j);
                }
            }
        }
        // 病毒周围的普通点(集合 )去设置源头!
        for (int sick : initial) {
            for (int neighbor = 0; neighbor < n; neighbor++) {
                if (sick != neighbor && !virus[neighbor] && graph[sick][neighbor] == 1) {
                    int fn = find(neighbor);
                    if (infect[fn] == -1) {
                        infect[fn] = sick;
                    } else if (infect[fn] != -2 && infect[fn] != sick) {
                        infect[fn] = -2;
                    }
                }
            }
        }
        // 统计拯救数据
        for (int i = 0; i < n; i++) {
            // 不是代表点,不看
            if (i == find(i) && infect[i] >= 0) {
                cnts[infect[i]] += size[i];
            }
        }
        Arrays.sort(initial);
        int ans = initial[0];
        int max = cnts[ans];
        for (int i : initial) {
            if (cnts[i] > max) {
                ans = i;
                max = cnts[i];
            }
        }
        return ans;
    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值