图论相关(二)
本文主要记录两个图论算法:有向图的环检测、拓扑排序算法(分别用DFS和BFS)
1、有向图的环检测
207. 课程表 - 力扣(LeetCode) (leetcode-cn.com)
什么时候无法修完所有课程?当存在循环依赖的时候
看到依赖问题,首先想到的就是把问题转化成「有向图」这种数据结构,只要图中存在环,那就说明存在循环依赖
可以根据题目输入的 prerequisites
数组去构造有向图
如果发现这幅有向图中存在环,那就说明课程之间存在循环依赖,肯定没办法全部上完;反之,如果没有环,那么肯定能上完全部课程
即:先构造一个有向图,然后去判断该有向图是否存在环
DFS版本:
class Solution {
// 表示节点是否访问过
boolean[] visited;
// 当前的路径
boolean[] path;
boolean hasCycle = false;
public boolean canFinish(int numCourses, int[][] prerequisites) {
visited = new boolean[numCourses];
path = new boolean[numCourses];
// 建图
LinkedList<Integer>[] graph = new LinkedList[numCourses];
// 初始化操作
for (int i = 0; i < numCourses; i++) {
graph[i] = new LinkedList<>();
}
for (int[] e : prerequisites){
int from = e[1];
int to = e[0];
graph[from].add(to);
}
// 对图中的每一个节点都做DFS,因为图有可能是不连通的
for (int i = 0; i < numCourses && !hasCycle; i++) {
graphTravel(graph, i);
}
return !hasCycle;
}
void graphTravel(LinkedList<Integer>[] graph, int s){
// 这两个if的顺序不能互换,如果互换了 hasCycle还没有赋值,就被返回了
if (path[s]){
hasCycle = true;
return;
}
if (visited[s] || hasCycle)
return;
// 访问的不用撤销
visited[s] = true;
// 在for循环外面做选择(回溯则是在for循环内做选择)
path[s] = true;
for (int v : graph[s]) {
graphTravel(graph, v);
}
// 撤销选择
path[s] = false;
}
}
PS:类比贪吃蛇游戏,visited
记录蛇经过过的格子,而 onPath
仅仅记录蛇身。onPath
用于判断是否成环,类比当贪吃蛇自己咬到自己(成环)的场景。
BFS版本:
BFS 算法借助 indegree
数组记录每个节点的「入度,思路如下:
1、构建邻接表,和之前一样,边的方向表示「被依赖」关系。
2、构建一个 indegree
数组记录每个节点的入度,即 indegree[i]
记录节点 i
的入度。
3、对 BFS 队列进行初始化,将入度为 0 的节点首先装入队列。
4、开始执行 BFS 循环,不断弹出队列中的节点,减少相邻节点的入度,并将入度变为 0 的节点加入队列。
5、如果最终所有节点都被遍历过(count
等于节点数),则说明不存在环,反之则说明存在环。
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<Integer>[] graph = new LinkedList[numCourses];
// 初始化
for (int i = 0; i < numCourses; i++) {
graph[i] = new LinkedList<>();
}
// 记录每个节点的入度
int[] degree = new int[numCourses];
// 建立邻接表
for (int[] e : prerequisites){
int from = e[1];
int to = e[0];
graph[from].add(to);
// 顺便计算每个节点的入度
degree[to]++;
}
Queue<Integer> queue = new LinkedList<>();
// 将入度为0的节点入队
for (int i = 0; i < numCourses; i++) {
if (degree[i] == 0)
queue.add(i);
}
int count = 0;
while (!queue.isEmpty()){
int cur = queue.poll();
count++;
for (int i = 0; i < graph[cur].size(); i++) {
// 获取与cur相邻的节点,并将它们的入度减一,因为cur出队了
int index = graph[cur].get(i);
degree[index]--;
if (degree[index] == 0)
queue.add(index);
}
}
// 如果没有环的话,count == numCourses为true,如果有环的话,图最后剩下的肯定是入度不为0的节点,它们没有进入队列,那也就没有被遍历到
return count == numCourses;
}
}
2、拓扑排序算法
210. 课程表 II - 力扣(LeetCode) (leetcode-cn.com)
这道题就是上道题的进阶版,不是仅仅让你判断是否可以完成所有课程,而是进一步让你返回一个合理的上课顺序,保证开始修每个课程时,前置的课程都已经修完
只要图中无环,那么我们就调用 traverse
函数对图进行 DFS 遍历,记录后序遍历结果,最后把后序遍历结果反转,作为最终的答案。
二叉树的后序遍历是什么时候?遍历完左右子树之后才会执行后序遍历位置的代码。换句话说,当左右子树的节点都被装到结果列表里面了,根节点才会被装进去。
后序遍历的这一特点很重要,之所以拓扑排序的基础是后序遍历,是因为一个任务必须等到它依赖的所有任务都完成之后才能开始开始执行。
DFS版本:
class Solution {
boolean[] visited;
boolean[] path;
boolean hasCycle = false;
List<Integer> postorder;
void graphTravel(LinkedList<Integer>[] graph, int s){
if (path[s]){
hasCycle = true;
return;
}
if (visited[s] || hasCycle)
return;
visited[s] = true;
path[s] = true;
for (int v : graph[s]) {
graphTravel(graph, v);
}
path[s] = false;
// 记录后序遍历
postorder.add(s);
}
public int[] findOrder(int numCourses, int[][] prerequisites) {
postorder = new ArrayList<>();
visited = new boolean[numCourses];
path = new boolean[numCourses];
LinkedList<Integer>[] graph = new LinkedList[numCourses];
for (int i = 0; i < numCourses; i++) {
graph[i] = new LinkedList<>();
}
for (int[] e : prerequisites){
int from = e[1];
int to = e[0];
graph[from].add(to);
}
for (int i = 0; i < numCourses && !hasCycle; i++) {
graphTravel(graph, i);
}
if (hasCycle)
return new int[]{};
Collections.reverse(postorder);
int[] res = new int[numCourses];
for (int i = 0; i < res.length; i++) {
res[i] = postorder.get(i);
}
return res;
}
}
BSF版本:
class Solution {
public int[] findOrder(int numCourses, int[][] prerequisites) {
List<Integer>[] graph = new LinkedList[numCourses];
// 初始化
for (int i = 0; i < numCourses; i++) {
graph[i] = new LinkedList<>();
}
// 记录每个节点的入度
int[] degree = new int[numCourses];
// 建立邻接表
for (int[] e : prerequisites){
int from = e[1];
int to = e[0];
graph[from].add(to);
// 顺便计算每个节点的入度
degree[to]++;
}
Queue<Integer> queue = new LinkedList<>();
// 将入度为0的节点入队
for (int i = 0; i < numCourses; i++) {
if (degree[i] == 0)
queue.add(i);
}
int count = 0;
List<Integer> path = new ArrayList<>();
while (!queue.isEmpty()){
int cur = queue.poll();
count++;
path.add(cur);
for (int i = 0; i < graph[cur].size(); i++) {
// 获取与cur相邻的节点,并将它们的入度减一,因为cur出队了
int index = graph[cur].get(i);
degree[index]--;
if (degree[index] == 0)
queue.add(index);
}
}
if (path.size() == numCourses){
int[] res = new int[numCourses];
for (int i = 0; i < path.size(); i++) {
res[i] = path.get(i);
}
return res;
}
return new int[]{};
}
}
图的遍历都需要 `visited` 数组防止走回头路,这里的 BFS 算法其实是通过 `indegree` 数组实现的 `visited` 数组的作用,只有入度为 0 的节点才能入队,从而保证不会出现死循环