普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月19日首页

并查集+排序,Java双百详细题解

作者 acoreo
2021年11月28日 14:07

image.png

题意分析

一共有n个专家,最开始只有0号专家知道秘密,在时刻0他会把秘密分享给另一个专家。之后每次开会,知道秘密的专家都会把秘密分享出去。目标是求出最后时刻,一共有多少专家知道秘密。

解题思路

每次分享秘密,可以看作把两个专家合并到一个集合中,因此采用并查集求解。
特别的,本题的目标是求出祖先为0的节点有哪些。

最开始,每个专家的祖先节点记为自己,由于秘密传播是通过会议进行的,时间靠后的会议不可能传播到时间靠前的会议,因此需要先对meetings数组按照会议时间排序。
排序完成后,遍历所有时刻。同一时刻可能存在多场会议,由于秘密共享是瞬时发生的,且同一时刻的会议是乱序的,不存在先后,所以对每一时刻的处理分为两步:

  1. 第一轮遍历:首先判断两位专家中是否有人知道秘密,若有人知道秘密,则将两位专家的祖先节点都置为0。完成该操作后,无论两位专家是否有人知道秘密,都将两个专家合并,因为同一时刻的其他会议中,可能有其他知道秘密的专家将秘密分享给这两位中的任何一个,若存在此情况,则当前时刻过后,这两位专家也知道了秘密。
  2. 第二轮遍历:处理两种情况,
    • 场景一:第一轮遍历中,先遍历到某场会议,此时两位专家都不知道秘密,但在后面的遍历中,其中一位专家知道了秘密,由于上一步做了合并集合处理,此时将两位专家的祖先节点均置为0即可。
    • 场景二:第一轮遍历中,先遍历到某场会议,此时两位专家都不知道秘密,在后面的遍历中,这两位专家均没有被分享秘密,这时需要将两位专家从合并的集合中分离出来,如果不分离出来,在后面某时刻,如果这两位专家其中一个知道了秘密,那么会认为这两位专家都知道了秘密,但事实上,由于该时刻已过去,秘密无法分享给另一位专家。示例2即为此情况

解题技巧

由于需要按照时间对meetings数组进行升序排序,且之后要按照时间顺序进行遍历。所以考虑使用TreeMap对会议进行存储,key为时刻,value为当前时刻的会议列表。

详细代码

class Solution {
    
    // 并查集数组,记录每个元素的祖先节点
    public int[] p;
    
    // 查找每个元素的祖先,(路径压缩,并查集模板)
    public int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
        
    public List<Integer> findAllPeople(int n, int[][] meetings, int firstPerson) {
        p = new int[n+1];
        // 祖先数组初始化,将每个元素的祖先标记为自己
        for (int i = 1; i <= n; ++ i) p[i] = i;
        // 合并0号专家与firstPerson
        p[firstPerson] = 0;
        Map<Integer, List<int[]>> map = new TreeMap<>();
        // 构造以时刻为key,会议列表为value的Map,TreeMap将自动按照key升序排序
        for (int[] m : meetings) {
            // m[2]为会议时刻,每个时刻对应多场会议
            List<int[]> list = map.getOrDefault(m[2], new ArrayList<>());
            list.add(m);
            map.put(m[2], list);
        }
        // 对于每个时刻,遍历两次
        for (int x : map.keySet()) {
            // 第一轮遍历,合并集合
            for (int[] l : map.get(x)) {
                int a = l[0], b = l[1];                
                if (p[find(a)] == 0 || p[find(b)] == 0) { p[find(a)] = 0; p[find(b)] = 0; }
                p[find(b)] = p[find(a)];
            }
            // 第二轮遍历,分场景讨论
            for (int[] l : map.get(x)) {
                int a = l[0], b = l[1];
                // 场景一:两位专家在前面的会议均不知道秘密,后面遍历中其中一位专家知道了秘密,瞬时共享,两人都将知道秘密
                if (p[find(a)] == 0 || p[find(b)] == 0) { p[find(a)] = 0; p[find(b)] = 0; }
                // 场景二:两位专家在该时刻始终都不知道秘密,将合并的集合分离开,防止后面时刻有一个专家知道秘密,将秘密分享给另一个专家
                else { p[a] = a; p[b] = b; }
            }
        }       
        List<Integer> ans = new ArrayList<>();
        // 祖先为0的元素即为知道秘密的专家
        for (int i = 0; i < n; ++ i) {
            if (p[find(i)] == 0) ans.add(i);
        }        
        return ans;
    }
}
❌
❌