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

483 阅读3分钟

Offer 驾到,掘友接招!我正在参与2022春招系列活动-刷题打卡任务,点击查看活动详情

题目描述: 给你一个整数 n ,表示有 n 个专家从 0n - 1 编号。另外给你一个下标从 0 开始的二维整数数组 meetings ,其中 meetings[i] = [xi, yi, timei] 表示专家 xi 和专家 yi 在时间 timei 要开一场会。一个专家可以同时参加 多场会议 。最后,给你一个整数 firstPerson

专家 0 有一个 秘密 ,最初,他在时间 0 将这个秘密分享给了专家 firstPerson 。接着,这个秘密会在每次有知晓这个秘密的专家参加会议时进行传播。更正式的表达是,每次会议,如果专家 xi 在时间 timei 时知晓这个秘密,那么他将会与专家 yi 分享这个秘密,反之亦然。

秘密共享是 瞬时发生 的。也就是说,在同一时间,一个专家不光可以接收到秘密,还能在其他会议上与其他专家分享。

在所有会议都结束之后,返回所有知晓这个秘密的专家列表。你可以按 任何顺序 返回答案。

方法一:广度优先搜索

分析:本题中对专家之间传递秘密的描述,符合图的特征,在题面中特意强调秘密传递是瞬间完成的,并且一个专家可以参加多个会议,这是为了保证知道秘密的专家会把秘密瞬间传播到和他之间存在路径的所有专家。会议表示边,对应的图中的连通块中如果有一个专家知道秘密,那么这个连通块中的所有专家都会知道,因此,我们可以使用广度优先搜索求解联通块,从知道秘密的专家开始搜索。

需要注意的是,必须把每个时间点的会议单独考虑,因此我们在每一个时间点,都独立建图,然后BFS。

这里每个时间点分离采用按照自定义排序,按时间从小到大的顺序排序列表。也可以使用map保存每个时间对应的会议,然后对每个时间点的会议各自处理。

class Solution {
public:
    vector<int> findAllPeople(int n, vector<vector<int>>& meetings, int firstPerson) {
        const int m = meetings.size();
        vector<int> seen(n, 0);
        seen[0] = seen[firstPerson] = 1;
        sort(meetings.begin(), meetings.end(), [&](vector<int>& lhs, vector<int>& rhs) {
            return lhs[2] < rhs[2];
        });
        unordered_set<int> vertices;
        unordered_map<int, vector<int>> edges;
        for (int i = 0; i < m; ) {
            int j = i;
            while (j + 1 < m && meetings[j + 1][2] == meetings[i][2])
                ++j;
                
            vertices.clear();
            edges.clear();
            for (int k = i; k <= j; ++k) {
                int x = meetings[k][0], y = meetings[k][1];
                vertices.insert(x);
                vertices.insert(y);
                edges[x].push_back(y);
                edges[y].push_back(x);
            }

            queue<int> q;
            for (int x: vertices) {
                if (seen[x])
                    q.push(x);
            }
            while (!q.empty()) {
                auto x = q.front();
                q.pop();
                for (auto y: edges[x]) {
                    if (!seen[y]) {
                        seen[y] = 1;
                        q.push(y);
                    }
                }
            }
            i = j + 1;
        }
        vector<int> ans;
        for (int i = 0; i < n; ++i)
            if (seen[i])
                ans.push_back(i);
        return ans;
    }
};

时间复杂度: 排序为O(nlogn)O(n\log n),所有BFS过程加起来为O(n)O(n),因此总的时间复杂度为O(nlogn)O(n\log n),其中nnmeetings的数量。这里不使用排序,使用map(不管是map还是unordered_map,unordered_map之后也要再加一步排序操作),时间复杂度也是一样的,因为总是要先按照时间处理会议,所以必须要有一个排序操作。

方法二:并查集

因为本题是有关并查集的问题,所以也可以用并查集求解。但是使用并查集的难点在于,每个时间点的连通关系必须分开处理。比如如果在时间t1,1和2是联通的,但是1和2都没有和知道秘密的结点连通,那么到了时间t2>t1之后,必须将1和2的连通关系去掉。

常用的并查集有下列两个操作

  1. find(u),查找u结点的祖先
  2. merge(u, v),将uv合并到一个连通块里

为了满足的需要,可以再添加一个isolate(u)操作,该操作将结点u的祖先结点设置为自己,也就是把u结点和他所在的连通快的联系断开,注意这里断开时,必须保证结点已经进行过路径压缩了,否则,如果有结点通过u指向祖先,那么这个结点也会被断开。

代码如下

class UnionFindSet {
public:
    UnionFindSet(int n_): n(n_), setCount(n_), parent(n_), size(n_, 1) {
        iota(parent.begin(), parent.end(), 0);
    }
    int find(int u) {
        if (u != parent[u])
            parent[u] = find(parent[u]);
        return parent[u];
    }
    bool merge(int u, int v) {
        int pu = find(u);
        int pv = find(v);
        if (pu == pv) return false;
        if (size[pu] < size[pv]) swap(pu, pv);
        parent[pv] = pu;
        size[pu] += size[pv];
        --setCount;
        return true;
    }
    void isolate(int u) {
        if (u != parent[u]) {
            parent[u] = u;
            size[u] = 1;
            ++setCount;
        }
    }
    bool isConnected(int u, int v) {
        return find(u) == find(v);
    }
private:
    vector<int> parent;
    vector<int> size;
    int setCount;
    int n;
};
class Solution {
public:
    vector<int> findAllPeople(int n, vector<vector<int>>& meetings, int firstPerson) {
        const int m = meetings.size();
        sort(meetings.begin(), meetings.end(), [&](auto& lhs, auto& rhs) {
            return lhs[2] < rhs[2];
        });

        UnionFindSet ufs = UnionFindSet(n);
        ufs.merge(0, firstPerson);
        for (int i = 0; i < m; ) {
            int j = i;
            while (j + 1 < m && meetings[j + 1][2] == meetings[i][2])
                ++j;
            
            unordered_set<int> vertices;
            for (int k = i; k <= j; ++k) {
                int x = meetings[k][0], y = meetings[k][1];
                ufs.merge(x, y);
                vertices.insert(x);
                vertices.insert(y);
            }
            for (auto x: vertices) {
                if (!ufs.isConnected(0, x))
                    ufs.isolate(x);
            }
            i = j + 1;
        }
        vector<int> ans;
        for (int i = 0; i < n; ++i)
            if (ufs.isConnected(0, i))
                ans.push_back(i);
        return ans;
    }
};

时间复杂度: 并查集每次merge的时间复杂度为O(logn),操作次数为O(\log n),操作次数为O(n),所以总的时间复杂度为O(nlogn)O(n\log n),其中nnmeetings的数量。