文章目录
拓扑排序简介
有向无环图(DAG图)
什么是有向无环图?

- 像上面这样由若干节点组成的图,且节点的走向是有方向性的(节点间的指向),但是节点的走向又不能成环,这样的图就成为有向无环图。
- 对于每个节点指向该节点的箭头的数量称为该节点的入度
(重要),从该节点出发指向其他节点的箭头的数量称为该节点的出度
顶点活动图(AOV图)
- 在有向无环图的基础上,每个节点代表一个活动(事件),用箭头(边)来表示执行各活动的先后顺序(比如A--->B代表执行B活动之前必须先执行A活动),这样的图称为顶点活动图。
拓扑排序
- 对于一个顶点活动图,我们发现可以由图的结构(节点间的指向)得出所有事件执行的先后顺序,这就叫做拓扑排序。
- 但拓扑排序的结果可能是不唯一的,比如上图的"辣椒炒肉的工程图"中"准备餐具"和"买菜"这两个事件先后顺序随意。
那么对于这样一个顶点活动图,我们应该怎么找出排序结果呢?
下面给出大致的排序思路:
- 找到图中入度为0的点,输出
- 处理完一个点之后,删除该点对其他点的指向,也就是它所指向的所有节点的度都要 -1
- 重复1、2操作,直到图中没有剩余的点了或者没有入度为0的点为止(因为顶点活动图会出现带环这样的非法情况,对于这些环中的节点我们无法进行删除操作,因为它们的入度不会变为0)
代码实现拓扑排序

- 借助队列对上图来一次BFS即可:
- 初始化:把所有入度为0的点加入到队列中
- 当队列不为空的时候重复下面操作:
(1) 拿出队头元素,处理该节点(比如,依次储存它们最终得出正确的事件执行顺序)
(2) 删除与该节点相连的边(将该节点指向的所有节点的度 -1)
(3) 判断:原来被指向的节点的入度 -1后是否变成0,如果入度为0,那就可以加入到队列中等待被处理了
最终得到(不唯一):

但以上操作都建立在外面首先要有一个这样的图形数据结构,所以我们要先用代码实现一个顶点活动图结构
如何建图?
灵活使用Java提供的容器
建立顶点活动图的方式是使用"邻接表"(两种方式):
- 使用HashMap:
Map<Integer,List<Integer>> edges=new HashMap<>();
- 其实我们并不用像二叉树那样建立一个指向清晰的图形结构,我们的做法是先将每个节点独立表示出来放到一块,不用管其互相的指向,但是要将该节点所指向的所有节点一并绑定与该节点储存。
- 具体做法就是
Map<Integer,List<Integer>>,key值为节点,value储存的就是该节点所指向的所有节点(放到一个容器中) - 还有就是要将每个节点的入度记录起来,我们单独找来一个数组(或者使用一个HashSet,它们各有适用性,在接下来的题目中体现),数组下标代表每个节点,每个位置储存该节点的入度,具体就是当我们得到:B--->A 这样的信息之后是不是就知道了A的入度要 +1了?(并且此时在
Map<Integer,List<Integer>>中B就对应着key值,且A就要加入到value:List中了)
- 直接用List:
List<List<Integer>> edges=new HashMap<>();
- edges的下标来代表节点(也就是活动):0下标代表活动0、1下标代表活动1... ,对应下标位置的List储存的就是该节点所指向的所有节点
- 那么也用同样的方法将节点的入度储存起来
由以上这些结构就可以使用上述的"借助队列来一次BFS"这个方法进行拓扑排序的实现了
一、课程表
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。
例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
解题思路
- 判断是否可能完成所有课程的学习,就是判断是否可以正常对顶点活动图进行拓扑排序,那么如果图中有环结构就不能正常排序
- 所以根据上面介绍的方法进行:收集活动执行顺序的信息(A--->B)来建图------>对图进行拓扑排序------>最后判断排序结果
代码实现及解析
java
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
int n=numCourses;
Map<Integer,List<Integer>> edges=new HashMap<>();//用邻接表储存图
int[] inDegree=new int[n];//存储入度信息(自动初始化为0)
//建图(收集所有的像"A--->B"这样的信息)
for(int[] infor:prerequisites){
int B=infor[0],A=infor[1];//得到A--->B这样的信息
if(!edges.containsKey(A)){
edges.put(A,new ArrayList<>());
}
edges.get(A).add(B);//将A--->B信息放入邻接表中
inDegree[B]++;//B入度+1
}
//把所有入度为0的节点先入队列
Queue<Integer> que=new LinkedList<>();
for(int i=0;i<inDegree.length;i++){
if(inDegree[i]==0){
que.offer(i);//别忘了inDegree的下标代表节点
}
}
//开始BFS:
while(!que.isEmpty()){
int tmp=que.poll();//处理掉该节点
//将该节点指向的所有节点的度-1
for(int x:edges.getOrDefault(tmp,new ArrayList<>())){
inDegree[x]--;
if(inDegree[x]==0){//如果入度变为0,那就可以加入到队列中了
que.offer(x);
}
}
}
//要注意最后是用入度表来判断图是否带环的
for(int x:inDegree){//处理完之后如果顶点活动图不带环(也就是可以将所有课程排序),那所有节点的入度应该都为0
if(x!=0) return false;
}
return true;
}
}
总结
复习解题思路和代码实现及解析
二、LCR 114. 火星词典
现有一种使用英语字母的外星文语言,这门语言的字母顺序与英语顺序不同。
给定一个字符串列表 words ,作为这门语言的词典,words 中的字符串已经 按这门新语言的字母顺序进行了排序 。
请你根据该词典还原出此语言中已知的字母顺序,并 按字母递增顺序 排列。若不存在合法字母顺序,返回 "" 。若存在多种可能的合法字母顺序,返回其中 任意一种 顺序即可。
字符串 s 字典顺序小于 字符串 t 有两种情况:
在第一个不同字母处,如果 s 中的字母在这门外星语言的字母顺序中位于 t 中字母之前,那么 s 的字典顺序小于 t 。
如果前面 min(s.length, t.length) 字母都相同,那么 s.length < t.length 时,s 的字典顺序也小于 t 。
示例 1:
输入:words = ["wrt","wrf","er","ett","rftt"]
输出:"wertf"
解题思路
-
当我们可以零零散散地获得一些事件的相对顺序,但要求得出所有事件的完整执行顺序(或判断是否可以完整执行所有事件)时,就应该想到这就是顶点活动图,使用拓扑排序来解决
-
在本题中我们可以对于每一个单词组合得出一个字母顺序对(比如,a--->b),然后要求推导出所有出现的字母的完整字典序,就是典型的例子。
-
这种题目就是要用拓扑排序来解决,要记住这种题目的特性及解决方法。
代码实现及解析
java
class Solution {
Map<Character,List<Character>> edges=new HashMap<>();//邻接表
Map<Character,Integer> inDegree=new HashMap<>();//储存每个节点的入度
boolean check;//处理一种特殊情况的标记
public String alienOrder(String[] words) {
//将入度表初始化,不然开始那批入度为0的节点整个过程压根就不会出现在表中
for(String word:words){
for(int i=0;i<word.length();i++){
if(!inDegree.containsKey(word.charAt(i))){
inDegree.put(word.charAt(i),0);
}
}
}
//1.收集信息并建图
for(int i=0;i<words.length;i++){
for(int j=i+1;j<words.length;j++){//每次检查两个单词进行信息的收集
getInfor(words,i,j);//收集信息,在邻接表和入度表中更新对应信息
if(check) return "";//words[i],words[j]这两个单词压根排序就不合法
}
}
//2.将所有入度为0的节点先入队列
Queue<Character> que=new LinkedList<>();
for(char x:inDegree.keySet()){
if(inDegree.get(x)==0){
que.offer(x);
}
}
//3.开始BFS
StringBuilder ret=new StringBuilder();
while(!que.isEmpty()){
char tmpChar=que.poll();
ret.append(tmpChar);//记录正确的外星字母顺序
//断掉tmpChar所指向的这些所有节点的箭头(这些节点的入度-1)
for(char t:edges.getOrDefault(tmpChar,new ArrayList<>())){
inDegree.put(t,inDegree.get(t)-1);
if(inDegree.get(t)==0) que.offer(t);//如果t的入度变为了0,那又可以将t入队列了
}
}
//4.检查、判断
for(int value:inDegree.values()){
if(value!=0) return "";//说明仍存在入度不为0的节点,不符合条件
}
return ret.toString();
}
public void getInfor(String[] words,int i,int j){
int n=Math.min(words[i].length(),words[j].length());
String tmp1=words[i],tmp2=words[j];
int index=0;//记录最终比较到了两个单词的哪一位
while(index<n){
char ch1=tmp1.charAt(index),ch2=tmp2.charAt(index);
if(ch1!=ch2){//得到ch1--->ch2这样的信息
if(!edges.containsKey(ch1)){
edges.put(ch1,new ArrayList<>());
}
if(!edges.get(ch1).contains(ch2)){//不同的二元单词组可能会获得相同的"ch1--->ch2"信息,不能重复记录
edges.get(ch1).add(ch2);//将信息ch1--->ch2存入邻接表中
inDegree.put(ch2,inDegree.get(ch2)+1);//ch2入度+1
}
break;//后面的字符就不能再处理了
}
index++;
}
//遍历结束,如果发现index在排序较后的words[j]中走到了头,但在位置较前的words[i]中却没走到头,
//说明了明明前面遍历过程中字母都相等,但单词words[i]长度较长却排序在了前面,这压根就是不合法的,直接return ""
if(index<words[i].length()&&index==words[j].length()) check=true;//标记遇到不合法单词排序
}
}
总结
复习解题思路和代码实现及解析