1.并查集
赤裸裸的并查集,做这道题就是冲着并查集去的。
按秩求并+路径压缩,维护一个count表示不相交集合森林里树的棵数
class Solution { public: int *pre; int findCircleNum(vector<vector<int>>&M) { int N = (int) M.size(); pre = new int[N+1](); int count=N; for (int i = 0; i < N; i++) { for (int j = 0; j < i; j++) { int x = find(i+1); int y = find(j+1); if (1 == M[i][j]&&x!=y) { if (pre[y] < pre[x]) pre[x] = y; else { if (pre[x] == pre[y]) pre[x]--; pre[y] = x; } --count; } } } return count; } int find(int x) { if (0 >= pre[x]) return x; else return pre[x] = find(pre[x]); } };
时间复杂度:遍历矩阵(嵌套for循环)+并查集最坏复杂度O(n),整体复杂度为O(n3)
空间复杂度:pre数组,复杂度O(n)
2.DFS
这道题是并查集的典型,但最优解却不是并查集。
重新读题,注意给出的输入用例
看到这种矩阵联想到图论算法是很自然的。
结合题意,朋友圈的个数,体先在这个矩阵中,也就是聚集在一起的1的块数,单独的1算一块。
套用离散数学中的概念也就是,求无向图中连通分量的个数。
从任意一个未被访问的节点出发开始搜索,将访问过的节点标记为visited。
每完成一次DFS,即聚集的1的块增加了一。
class Solution { public: int findCircleNum(vector<vector<int>> &M) { int N = M.size(); vector<int> visit(N, 0); int count = 0; for (int i = 0; i < N; ++i) { if (!visit[i]) { dfs(M, visit, i); count++; } } return count; } void dfs(vector<vector<int>> &M, vector<int> &visit, int n) { visit[n] = 1; for (int j = 0; j < M.size(); ++j) { if (M[n][j] && !visit[j]) { dfs(M, visit, j); } } } };
时间复杂度:遍历矩阵O(n2)
空间复杂度:visit数组O(n)
3.BFS
和DFS思路相同,只是把遍历图的方法换成BFS,代码略。
4.总结
根据这道题可以抽象出一类问题,即求无向图的连通分量数。
求解这类问题常用的方法就是并查集与DFS。
从复杂度分析上来看,DFS的效率要更高,但在实际应用中并不一定。
理论上,深度优先搜索比union-find快,因为它能保证所需的时间是常数而union-find算法不行;但在实际应用中,这点差异微不足道。union-find算法其实更快,因为它不需要完整地构造并表示一幅图。更重要的是,union-find算法是一种动态算法(我们在任何时候都能用接近常数的时间检查两个顶点是否连通,甚至是在添加一条边的时候),但深度优先搜索则必须要对图进行预处理。因此,我们在完成只需要判断连通性或是需要完成有大量连通性查询和插入操作混合等类似的任务时,更倾向使用union-find算法,而深度优先搜索则更适合实现图的抽象数据类型,因为它能更有效地利用已有的数据结构。
引自《算法 第4版》p350
5.此外
在此基础上思考无向图的着色问题
以及求有向图的强连通分量数问题(Kosaraju的算法和Tarjan算法)
挖坑待填。