拓扑排序
实现思路
拓扑排序思路如下:
- 找到所有入度为0的节点,加入队列
- 删除队列中的一个节点,并将其指向的所有节点的入度值减1
- 若有入度值为0的城市,加入队列
- 重复上述操作,直至队列为空。此时没有入度值为0的节点,要么遍历完成,要么图中有环。
拓扑排序和图的关系
直观地说就是,让你把⼀幅图「拉平」,⽽且这个「拉平」的图⾥⾯,所有箭头⽅向都是⼀致的,⽐如上图所有箭头都是朝右的。
很显然,如果⼀幅有向图中存在环,是⽆法进⾏拓扑排序的,因为肯定做不到所有箭头⽅向⼀致;反过来,如果⼀幅图是「有向⽆环图」,那么⼀定可以进⾏拓扑排序。
旅行问题
实现思路:
- 首先初始化优先队列、入度数组、标记数组(用来判断某个城市是否来过)、后置城市数组,先将入度为0的城市开销、编号添加到队列,并将对应标记置为true。
- 每次取优先队列的最小值,如果当前花费还够,则减去对应开销,并将可去城市数cnt加一。然后将对应的后置数组里的所有城市入度减一。遍历所有的城市,如果没有访问过,并且入度为0,则加入到队列。
// 牛客 NC522 旅行Ⅰ
public int Travel (int N, int V, int[] A, Point[] list) {
// o1[0]:该城市的编号 o1[1]:去该城市的花费
PriorityQueue<int[]> queue = new PriorityQueue<>(
(o1,o2) -> o1[1] == o2[1] ? o1[0] - o2[0] : o1[1] - o2[1]);
// 标记数组
boolean[] visited = new boolean[N];
// 入度数组
int[] Indeg=new int[N];
// 后置城市数组
int[][] limit=new int[N][N];
// 给后置城市数组和入度数组赋值
for (Point p : list) {
limit[p.x-1][p.y-1] = 1;
Indeg[p.y-1]++;
}
// 如果入度为0,则表示可以访问,加入到队列,并且访问标记置为true
for (int i = 0; i < N; i++) {
if (Indeg[i] == 0) {
visited[i] = true;
queue.offer(new int[]{i,A[i]});
}
}
// 记录最多可去的城市
int count = 0;
while (!queue.isEmpty()) {
int[] city = queue.poll();
int id = city[0];
int cost = city[1];
if (V < cost) {
break;
}
V -= cost;
count++;
//如果存在后置城市,将对应后置城市入度减一
for (int i = 0; i < N; i++) {
if (limit[id][i] == 1) {
Indeg[i]--;
}
}
//如果入度为0,并且没有访问过,则加入到队列,并且访问标记置为true
for (int i = 0; i < N; i++) {
if (!visited[i] && Indeg[i] == 0) {
visited[i] = true;
queue.offer(new int[]{i,A[i]});
}
}
}
return count;
}
拓扑排序在图中的应用
207 课程表
题目描述
题目链接:https://leetcode-cn.com/problems/course-schedule/
题目解析
题⽬应该不难理解,什么时候⽆法修完所有课程?当存在循环依赖的时候。
具体来说,我们⾸先可以把课程看成「有向图」中的节点,节点编号分别是 0, 1, …, numCourses-
1,把课程之间的依赖关系看做节点之间的有向边。⽐如说必须修完课程 1 才能去修课程 3,那么就有⼀条有向边从节点 1 指向 3。
所以我们可以根据题⽬输⼊的 prerequisites 数组⽣成⼀幅类似这样的图:
如果发现这幅有向图中存在环,那就说明课程之间存在循环依赖,肯定没办法全部上完;反之,如果没有环,那么肯定能上完全部课程。那么题目就转成判断是否有环问题了。
我们可以用邻接表存储上述结构代码如下:
List<Integer>[] buildGraph(int numCourses, int[][] prerequisites) {
List<Integer>[] graph = new LinkedList[numCourses];
for (int i = 0; i < graph.length; i++) {
graph[i] = new LinkedList<>();
}
for (int[] edge : prerequisites) {
int from = edge[0];
int to = edge[1];
// 修完课程 from 才能修课程 to
// 在图中添加⼀条从 from 指向 to 的有向边
graph[from].add(to);
}
return graph;
}
然后遍历是否有环,代码如下:
// 记录⼀次 traverse 递归经过的节点
boolean[] onPath;
// 记录遍历过的节点,防⽌⾛回头路
boolean[] visited;
// 记录图中是否有环
boolean hasCycle = false;
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<Integer>[] graph = buildGraph(numCourses, prerequisites);
visited = new boolean[numCourses];
onPath = new boolean[numCourses];
for (int i = 0; i < numCourses; i++) {
traverse(graph,i);
}
return !hasCycle;
}
// 从节点 s 开始 BFS 遍历,将遍历过的节点标记为 true
void traverse(List<Integer>[] graph, int s) {
if (onPath[s]) { // 有环
hasCycle = true;
}
if (visited[s] || hasCycle) {
return;
}
onPath[s] = true;
visited[s] = true;
for (Integer t : graph[s]) {
traverse(graph,t);
}
onPath[s] = false;
}
总结
这道题的核⼼就是判断⼀幅有向图中是否存在环。
不过如果出题⼈继续恶⼼你,让你不仅要判断是否存在环,还要返回这个环具体有哪些节点,怎么办?
你可能说,onPath ⾥⾯为 true 的索引,不就是组成环的节点编号吗?
不是的,假设下图中绿⾊的节点是递归的路径,它们在 onPath 中的值都是 true,但显然成环的节点只是其中的⼀部分:
那么这就需要用到拓扑排序了
201 课程表 ll
题目描述
题目链接:https://leetcode-cn.com/problems/course-schedule-ii/
题目解析
这道题就是上道题的进阶版,不是仅仅让你判断是否可以完成所有课程,⽽是 进⼀步让你返回⼀个合理的上课顺序
,保证开始修每个课程时,前置的课程都已经修完。
其实也不难看出来,如果把课程抽象成节点,课程之间的依赖关系抽象成有向边,那么这幅图的拓扑排序结果就是上课顺序。
那么关键问题来了,如何进⾏拓扑排序?其实特别简单, 将后序遍历的结果进⾏反转,就是拓扑排序的结果。
建图函数和 207 中一样,这里直接拿来用,全部代码如下:
// 记录后序遍历结果
List<Integer> postorder = new ArrayList<>();
// 记录⼀次 traverse 递归经过的节点
boolean[] onPath;
// 记录遍历过的节点,防⽌⾛回头路
boolean[] visited;
// 记录图中是否有环
boolean hasCycle = false;
public int[] findOrder(int numCourses, int[][] prerequisites) {
visited = new boolean[numCourses];
onPath = new boolean[numCourses];
List<Integer>[] graph = buildGraph(numCourses, prerequisites);
for (int i = 0; i < numCourses; i++) {
traverse(graph,i);
}
if (hasCycle) {
return new int[]{};
}
Collections.reverse(postorder);
int[] result = new int[numCourses];
for (int i = 0; i < postorder.size(); i++) {
result[i] = postorder.get(i);
}
return result;
}
void traverse(List<Integer>[] graph, int s) {
if (onPath[s]) {
hasCycle = true;
}
if (visited[s] || hasCycle) {
return;
}
// 前序遍历位置
onPath[s] = true;
visited[s] = true;
for (Integer t : graph[s]) {
traverse(graph,t);
}
// 后序遍历位置
postorder.add(s);
onPath[s] = false;
}
List<Integer>[] buildGraph(int numCourses, int[][] prerequisites) {
List<Integer>[] graph = new LinkedList[numCourses];
for (int i = 0; i < graph.length; i++) {
graph[i] = new LinkedList<>();
}
for (int[] edge : prerequisites) {
int from = edge[1];
int to = edge[0];
// 修完课程 from 才能修课程 to
// 在图中添加⼀条从 from 指向 to 的有向边
graph[from].add(to);
}
return graph;
}