力扣CV工程师刚学的拓扑排序!包教包会!
何为拓扑排序?
所谓拓扑排序,是有向无环图才有的一种排序。对于一个给定的包含n个节点的有向图G,我们给出它的节点编号的一种排列,如果满足:
对于图G中的任意一条有向边(u,v),u在排列中都出现在v的前面。
那么就称该排列是图G的拓扑排序。
第一次读拓扑排序的概念可能会感觉难以理解,从这个概念上出发,我们需要捋清楚的问题是为什么只有有向无环图才有拓扑排序?原因很简单,如果有向无环图中拥有环,假设在环中有两点必须互相出现在各自的面前,那么就不可能存在一个满足要求的排列来作为拓扑排序。
另外,我们也可以得知,一个有向无环图的拓扑排序也可能不止一种,考虑极端情况下,整个有向无环图中只有点而没有边,那么任何一种编号排序都可以作为其拓扑排序。
在《算法》第四版中,提到了有向无环图应用广泛的模型是给定一组任务并安排它们的执行顺序,而在这类问题中最重要的一种限制条件就是优先级限制,它指明了哪些任务必须在哪些任务之前完成,比较经典的例子也就是待会举的例子中,就是课程表的例子,在修习某一门课之前,你必须拥有另外一门课的基础,这意味着你学习的课程将会有先后之分。例如在学习机器学习之前,你必须先学习Python和数据挖掘导论。同样的例子还有任务调度、Java继承的类和extends关系、电子表格中的单元格和公式以及符号链接的文件名和链接等。在这类问题中,我们应该把具体的任务都抽象成点,而任务的优先级限制抽象成边,从而把场景构建成一张有向无环图,利用拓扑排序来求得一个可行的解。
那么如何判断一幅有向图是不是有向无环图呢?利用深度优先搜索就可以解决这个问题,由系统维护的递归调用的栈表示的正是“当前”正在遍历的有向路径,一旦我们找到了一条有向边v->w并且w已经在栈中,则说明找到了一个环。
拓扑排序例题
Leetcode 207 课程表
本题使用了两种不同的思想,分别是深度优先搜索DFS和广度优先搜索BFS。
广度优先搜索就是新建一个队列,然后把入度为0(也就是没有任何先修课程)的课程都加入到队列中,说明可以直接学习,也说明它们可以作为拓扑排序最前面的节点。在遍历边的时候,维护每个课程入度的同时,也要维护一个以当前课程为先修课程的课程链表,然后在队列里取出进行处理时,将该列表出来以减少课程链表中的课程的入度,如果入度为0才可加入队列中,最后只需要判断从队列中取出的课程数目和总课程数目即可知道是否有拓扑排序。如果需要拓扑排序的具体序列只需要加入一个数组记录从队列里出列的元素即可。
//广度优先搜索BFS
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
//拓扑排序概念 有向无环图才有此排序
//拓扑排序:对DAG的顶点进行排序,使得对每一条有向边(u,v),均有u在v先出现。
//代表每个元素的入度 入度为指向该顶点的边的总数
int[] indegrees=new int[numCourses];
//邻接表降低时间复杂度
List<List<Integer>> adjacency=new ArrayList<>();
Queue<Integer> queue=new LinkedList<>();
for(int i=0;i<numCourses;i++){
adjacency.add(new ArrayList<>());
}
for(int[] cp:prerequisites){
//循环入度+1 因为学习cp[0]之前要先学习cp[1]
indegrees[cp[0]]++;
//邻接表 [0,1]则让1位置的链表加上0的值
adjacency.get(cp[1]).add(cp[0]);
}
//取出所有入度为0的课程
for(int i=0;i<numCourses;i++){
if(indegrees[i]==0) queue.add(i);
}
while(!queue.isEmpty()){
int pre=queue.poll();
numCourses--;
//从邻接表中取出入度为0的项所对应的链表 会是它完成后才能完成的课程的链表
//当前这节课学完了 所以以当前这节课作为先修课程的课程入度都可以减一
//当入度为0时 加入队列
for(int cur:adjacency.get(pre))
if(--indegrees[cur]==0) queue.add(cur);
}
//所有点的入度为0 说明是有向无环图
return numCourses==0;
}
深度优先算法跟广度优先算法的思路有点不一样,它的本质思想是判断有向图中是否存在环。同样还是要维护一个以当前课程作为先修课程的课程链表,不同之处在于通过递归去寻找一条可行的路径,如果DFS了所有节点都没有发现环,说明有解。这里可以把节点分为三类,分别是未被DFS访问的节点,已被当前节点启动的DFS访问的节点和被其他节点启动的DFS访问的节点。
//深度优先搜索DFS
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
//这里的数组保存的同样是以当前课程作为先修课程的课程链表
List<List<Integer>> adjacency=new ArrayList<>();
for(int i=0;i<numCourses;i++){
adjacency.add(new ArrayList<>());
}
//这里的flags数组用于表示当前节点的访问情况
//1代表已被当前节点启动的DFS访问
//-1代表已被其他节点启动的DFS访问
//0代表未被DFS访问
int[] flags=new int[numCourses];
for(int[] cp:prerequisites){
adjacency.get(cp[1]).add(cp[0]);
}
for(int i=0;i<numCourses;i++){
if(!dfs(adjacency,flags,i)) return false;
}
return true;
}
private boolean dfs(List<List<Integer>> adjacency,int[] flags,int i){
//说明在本轮DFS中被第二次访问,即出现环,直接false
if(flags[i]==1) return false;
//说明当前访问节点已被其他节点启动的DFS访问,无需再重复搜索
if(flags[i]==-1) return true;
//能到达这里的都是0
flags[i]=1;
for(Integer j:adjacency.get(i)){
if(!dfs(adjacency,flags,j)) return false;
}
flags[i]=-1;
return true;
}
}
Leetcode 1203 项目管理
这道题目也是比较经典的拓扑排序,我们可以抽象出每个小组都必须是一幅有向无环图,并且需要求出题目的拓扑排序。另外,注意**“同一小组的项目,排序后在列表中彼此相邻”**这句话,它说明在组与组之间,也同样需要是有向无环图,我们也需要解决组之间的拓扑排序。基于此,我们可以将其分成两步,第一步是解决组与组之间的依赖关系,如果存在拓扑排序,再去确定组内的拓扑排序即可。
class Solution {
public int[] sortItems(int n, int m, int[] group, List<List<Integer>> beforeItems) {
List<List<Integer>> groupItem = new ArrayList<List<Integer>>();
for (int i = 0; i < n + m; ++i) {
groupItem.add(new ArrayList<Integer>());
}
// 组间和组内依赖图
List<List<Integer>> groupGraph = new ArrayList<List<Integer>>();
for (int i = 0; i < n + m; ++i) {
groupGraph.add(new ArrayList<Integer>());
}
List<List<Integer>> itemGraph = new ArrayList<List<Integer>>();
for (int i = 0; i < n; ++i) {
itemGraph.add(new ArrayList<Integer>());
}
// 组间和组内入度数组
int[] groupDegree = new int[n + m];
int[] itemDegree = new int[n];
List<Integer> id = new ArrayList<Integer>();
for (int i = 0; i < n + m; ++i) {
id.add(i);
}
int leftId = m;
// 给未分配的 item 分配一个 groupId
for (int i = 0; i < n; ++i) {
if (group[i] == -1) {
group[i] = leftId;
leftId += 1;
}
groupItem.get(group[i]).add(i);
}
// 依赖关系建图
for (int i = 0; i < n; ++i) {
int curGroupId = group[i];
for (int item : beforeItems.get(i)) {
int beforeGroupId = group[item];
if (beforeGroupId == curGroupId) {
itemDegree[i] += 1;
itemGraph.get(item).add(i);
} else {
groupDegree[curGroupId] += 1;
groupGraph.get(beforeGroupId).add(curGroupId);
}
}
}
// 组间拓扑关系排序
List<Integer> groupTopSort = topSort(groupDegree, groupGraph, id);
if (groupTopSort.size() == 0) {
return new int[0];
}
int[] ans = new int[n];
int index = 0;
// 组内拓扑关系排序
for (int curGroupId : groupTopSort) {
int size = groupItem.get(curGroupId).size();
if (size == 0) {
continue;
}
List<Integer> res = topSort(itemDegree, itemGraph, groupItem.get(curGroupId));
if (res.size() == 0) {
return new int[0];
}
for (int item : res) {
ans[index++] = item;
}
}
return ans;
}
public List<Integer> topSort(int[] deg, List<List<Integer>> graph, List<Integer> items) {
Queue<Integer> queue = new LinkedList<Integer>();
for (int item : items) {
if (deg[item] == 0) {
queue.offer(item);
}
}
List<Integer> res = new ArrayList<Integer>();
while (!queue.isEmpty()) {
int u = queue.poll();
res.add(u);
for (int v : graph.get(u)) {
if (--deg[v] == 0) {
queue.offer(v);
}
}
}
return res.size() == items.size() ? res : new ArrayList<Integer>();
}
}