并查集与搜索的联系

在 LeetCode 上练习时了解到,并查集 这一数据结构比较常用于解决图论方面的问题,其应用场景与搜索有很多重叠。例如,两者都可用于求无向图中连通分量connected component or just component)的个数。下图1中就有三个连通分量,分别以不同颜色标出:

Components

LeetCode 例题:547. 省份数量

并查集

鉴于 STL 没有实现并查集(已知 Boost 有),我们需要自己动手实现:以 vector<int> 为本体,辅以 get_root()merge_set() 两个函数,即可完成。注意,此实现舍弃了性能的完整性2,追求的目标是代码量小、功能够用,适用于做题和笔试场景。

值得注意的有两点:

  • 路径压缩path compression)的实现方式
  • 如何清点当前并查集中集合的数量
 1class Solution {
 2    int get_root(vector<int> &unionset, int idx) {
 3        if (idx == unionset[idx]) {
 4            return idx;
 5        } else {
 6            return unionset[idx] = get_root(unionset, unionset[idx]); // KEY!!! This is how parents are updated
 7                                                                      // from intermediate nodes to root (path compression)
 8        }
 9    }
10    void merge_set(vector<int> &unionset, int a, int b) {
11        unionset[get_root(unionset, a)] = get_root(unionset, b);
12    }
13public:
14    int findCircleNum(vector<vector<int>>& M) {
15        const int N = M.size();
16        vector<int> unionset(N);
17        for (int i = 0; i < N; ++i) {
18            unionset[i] = i;
19        }
20        for (int i = 1; i < N; ++i) {
21            for (int j = 0; j < i; ++j) {
22                if (M[i][j]) {
23                    merge_set(unionset, i, j);
24                }
25            }
26        }
27
28        /////////////////////////////////////////////// this part
29        // vector<int> has(N, 0);
30        // for (int i = 0; i < unionset.size(); ++i) {
31        //     has[get_root(unionset, i)] = 1;
32        // }
33        // int sum = 0;
34        // for (int i = 0; i < has.size(); ++i) {
35        //     sum += has[i];
36        // }
37        // return sum;
38        /////////////////////////////////////////////// can be improved as follows
39        /////////////////////////////////////////////// (check # of roots in the union set):
40        int num = 0; 
41        for (int i = 0; i < N; ++i) {
42            if (unionset[i] == i) {
43                num++;
44            }
45        }
46        return num;
47        ///////////////////////////////////////////////
48    }
49};

深搜

 1class Solution {
 2public:
 3    int findCircleNum(vector<vector<int>>& M) {
 4        const int N = M.size();
 5        vector<bool> visited(N, false);
 6        int count = 0;
 7        stack<int> s;
 8        for (int i = 0; i < N; i++) {
 9            if (visited[i]) {
10                continue;
11            }
12            visited[i] = true;
13            s.push(i);
14            while (!s.empty()) {
15                int pivot = s.top();
16                s.pop();
17                for (int j = 0; j < N; ++j) {
18                    if (M[pivot][j] && !visited[j]) {
19                        visited[j] = true;
20                        s.push(j);
21                    }
22                }
23            }
24            count++;
25        }
26        return count;
27    }
28};

深搜之数据结构选配:

  • 记录节点已被遍历的 visited 数组:vector<bool> visited(N, false)
  • 维护深搜遍历顺序的栈:stack<int> s

广搜

 1class Solution {
 2public:
 3    int findCircleNum(vector<vector<int>>& M) {
 4        const int N = M.size();
 5        vector<bool> visited(N, false);
 6        queue<int> q;
 7        int count = 0;
 8        for (int i = 0; i < N; ++i) {
 9            if (visited[i]) {
10                continue;
11            }
12            visited[i] = true;
13            q.push(i);
14            while (!q.empty()) {
15                int n = q.front();
16                q.pop();
17                for (int j = 0; j < N; ++j) {
18                    if (!visited[j] && M[n][j]) {
19                        visited[j] = true;
20                        q.push(j);
21                    }
22                }
23            }
24            count++;
25        }
26        return count;
27    }
28};

广搜之数据结构选配:

  • 记录节点已被遍历的 visited 数组:vector<bool> visited(N, false)
  • 维护广搜遍历顺序的队列:queue<int> q

注意:当有用到 visited 数组时,要考虑清楚 visited 置位的时机。一个节点“be visited”,应该被定义为“被发现”(已经经由其他节点探知),还是“被处理”(刚从栈顶/队头弹出,当前正在处理)?此例题中,“be visited”被定义为了“被发现”。


  1. 此图来自维基百科 ,有后期处理。 ↩︎

  2. 相较于 Boost 的实现,此实现缺少了 union by rank——其总的思想是在合并两个集合时考虑二者的高矮,将矮集合并入高集合而不是相反,使得合并的结果更扁平。然而在实际实现中,rank 并不等于集合的高度,而是集合高度的上界 。可参考 Boost 实现的源码(文件一文件二 )。 ↩︎


从一段 Android 源码说开去
自定义 Std::sort 对比函数时的陷阱