并查集
我们往往说弗洛伊德算法很巧妙,几行代码就可以解决APSP(All Pairs Shortest Paths,多源最短路径)问题;其实并查集也很巧妙,检测无向图是否有环,核心代码只有一行:
int find(int index) {
return parent[index] == index ? index : find(parent[index]);
}
即使进行rank优化来压缩路径,并查集的代码量依然很少。不过并查集类型的题目往往可以用dfs/bfs解决,故算法题目中很少直接使用。而有一些题目,可以用dfs解决,但用并查集更符合人的直觉,比如省份类型、岛屿类型的问题,又比如朋友圈类型的问题。
朋友圈
547. 省份数量 原本就是朋友圈问题,大约是为了规避腾讯的法务部,改成了省份数量,算法思路没有变化。利用并查集的思想最好解决这个问题,遍历完所有数据后,父亲的总数(所谓父亲,就是只有孩子没有parent的节点)就是朋友圈的总数。更直观来说,就像家庭图谱,但凡两个人追根溯源能找到一个老祖宗的,两个人就算亲戚,而老祖宗不是一个,那么就形成了不同的两个亲戚圈。
代码实现:
1.按rank进行并查集的构造和初始化
int[] parent;
int[] rank;
void initialize(int n) {
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
int find(int index) {
return parent[index] == index ? index : find(parent[index]);
}
void union(int x, int y) {
int root_x = find(x);
int root_y = find(y);
if (root_x == root_y) return;
if (rank[root_x] > rank[root_y]) {
parent[root_y] = root_x;
} else if (rank[root_x] < rank[root_y]) {
parent[root_x] = root_y;
} else {
parent[root_x] = root_y;
rank[root_y]++;
}
}
2.迭代所有数据
for (int i = 0; i < n; i++) {
for (int j = i+1; j < n; j++) {
if(isConnected[i][j]==1){
union(i,j);
}
}
}
3.统计所有无父亲节点(父亲节点是自己)的节点
for (int i = 0; i < n; i++) {
if(parent[i]==i)++provinces;
}
课程表
图论的问题普遍比较棘手,倒并不是题目难,而是无从下手。比如课程表类问题,属于拓扑排序的问题,也算是图论里重要的一块。这类问题与动态规划相反,想出解法很容易,代码实现非常难,甚至无从下手。
以 210. 课程表 II 为例。
现在你总共有 n 门课需要选,记为0到n-1。
在选修某些课程之前需要一些先修课程。例如,想要学习课程 0 ,你需要先完成课程1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。
可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
拓扑排序通常可以用dfs/bfs实现,在运算中构建有向图并判断是否有环,同时借助栈记录其中一个顺序。下面以dfs为例子:
class Solution {
// 存储有向图
List<List<Integer>> edges;
// 标记每个节点的状态:0=未搜索,1=搜索中,2=已完成
int[] visited;
// 用数组来模拟栈,下标 n-1 为栈底,0 为栈顶
int[] result;
// 判断有向图中是否有环
boolean valid = true;
// 栈下标
int index;
public int[] findOrder(int numCourses, int[][] prerequisites) {
edges = new ArrayList<List<Integer>>();
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
visited = new int[numCourses];
result = new int[numCourses];
index = numCourses - 1;
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
}
// 每次挑选一个「未搜索」的节点,开始进行深度优先搜索
for (int i = 0; i < numCourses && valid; ++i) {
if (visited[i] == 0) {
dfs(i);
}
}
if (!valid) {
return new int[0];
}
// 如果没有环,那么就有拓扑排序
return result;
}
public void dfs(int u) {
// 将节点标记为「搜索中」
visited[u] = 1;
// 搜索其相邻节点
// 只要发现有环,立刻停止搜索
for (int v: edges.get(u)) {
// 如果「未搜索」那么搜索相邻节点
if (visited[v] == 0) {
dfs(v);
if (!valid) {
return;
}
}
// 如果「搜索中」说明找到了环
else if (visited[v] == 1) {
valid = false;
return;
}
}
// 将节点标记为「已完成」
visited[u] = 2;
// 将节点入栈
result[index--] = u;
}
}
Is this article useful to you? How about buy me a coffee ☕ ?