【LeetCode】拓扑排序

时间:2024-10-02 07:17:03

拓扑排序

  • 拓扑排序 模板
    • 图的存储结构
      • 有权重
    • 拓扑排序
  • AcWing 848. 有向图的拓扑序列
  • 207. 课程表
  • 210. 课程表 II
  • 蚂蚁三面
  • vivo提前批

邻接表存储和遍历的标准模板

拓扑排序 模板

  • 拓扑排序
    用bfs来访问所有的节点,把入度为0的节点加入到队列中;每次从队列中取出一个节点,相当于从图删除这个节点,这样此节点的后继节点的入度都减1,再把入度为0的节点加到队列中。
  • 时间复杂度
    时间复杂度 O(n+m), n 表示点数,m 表示边数
  • java 模板
    用邻接表存储
    BFS遍历队列

图的存储结构

由于树是特殊的图,所以我们存储树也是用的邻接表或邻接图。
邻接表是指对于图上的每个结点来说,用一个单链表存储它的临接点。
在这里插入图片描述
我们需要h[],e[],ne[],idx来表示这个邻接点:

  • h[v] : 指向节点v形成的链表的头节点
  • e[i] : 第i个点的值v(表示下标和值的映射)
  • ne[i] : 表示邻接表链表中 i 节点的下一个节点
  • idx : 当前遍历到的节点的索引,等边都插入完idx就是节点个数

在节点a的后面插入b:
idx为当前遍历到的节点即b,因此e[idx] = b
我们用头插法插入到a的那条邻接表,h[a]为a的邻接表的头节点,所以ne[idx] = h[a]
头节点变成我们新插入的节点b

    private static void add(int a, int b){
        e[idx] = b;
        ne[idx] = h[a];
        h[a] = idx++;
    }
  • 1
  • 2
  • 3
  • 4
  • 5

有权重

有权重还需要一个w[i]来存储当前边的权重

  • w[i]: 指i的权重

构建邻接表:
其中a->b, c为a到b的权重

void add(int a, int b, int c) { //构建邻接表
    e[idx] = b;
    w[idx] = c;
    ne[idx] = h[a]; 
    h[a] = idx++;  //h[a] 一直指向a邻接表头插法起点,其实是最后一个,指针保留的方式也是向前
}
1. idx一直向前,如果a是第一次出现,则h[a]的值对应ne中位置即是起点。
2. 插入的方式是类似头插法,每次邻接表中的新元素出现,则插入邻接链表的第一个。也可以这样理解,是每次插到最后,让h[a]指向最后一个元素,遍历的时候倒着向前遍历。
3. 如果指向下一个为空时,指针值为-1.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

拓扑排序

这种方法队列是用数组q表示的,hh指向队首节点 初始化为0,tt指向最后一个节点初始化为-1;
当hh<=tt 说明队列中有元素,就进行遍历。
最后如果所有元素都入过队,即tt==n-1 就说明有拓扑序列

    private static boolean topsort(){
        // 把入度为0的节点入队
        for (int i = 1; i <= n; i++) { // i是点的编号 看题目的编号是如何给的
            if(d[i]==0){
                q[++tt] = i;//入队
            }

        }
        // BFS 访问
        while(hh <= tt){
            // 取队头节点t
            int t = q[hh++];
            // 遍历t的单链表,删除节点t,所以将节点t的所有后继j的入度-1
            for (int i = h[t]; i != -1; i = ne[i]) {
                int j = e[i];  // 索引对应的值 因为入度用值作为下标
                if(--d[j] == 0){
                    q[++tt] = j; // 入度为0加入队列
                }

            }
        }
        // 如果所有n个节点都加入过队列,就存在拓扑序
        return tt == n - 1;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

直接用Queue表示队列

AcWing 848. 有向图的拓扑序列

【题目描述】
给定一个n个点m条边的有向图,点的编号是1到n,图中可能存在重边和自环。
请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出-1。
若一个由图中所有点构成的序列A满足:对于图中的每条边(x, y),x在A中都出现在y之前,则称A是该图的一个拓扑序列。
【输入格式】
第一行包含两个整数n和m
接下来m行,每行包含两个整数x和y,表示存在一条从点x到点y的有向边(x, y)。
【输出格式】
共一行,如果存在拓扑序列,则输出拓扑序列。
否则输出-1。
【数据范围】
1≤n,m≤105
【输入样例】
3 3
1 2
2 3
1 3
【输出样例】
1 2 3


import java.util.Arrays;
import java.util.Scanner;

/**
 * @author 
 * @date 2021/6/18
 */
public class Tuopu {
    static int N = 100010;

    // 每个节点的入度
    static int[] d = new int[N];
    // 邻接表:树的存储由h[],e[],ne[],idx构成
    static int[] h = new int[N]; // h[v] 指向节点v形成的链表的头节点
    static int[] e = new int[N]; // 第i个点的值(表示下标和值的映射)
    static int[] ne = new int[N]; // 表示邻接表链表中 i 节点的下一个节点
    static int idx = 1; // 当前遍历到的节点的索引,等边都插入完idx就是节点个数
    static int n,cnt = 1; // n个点个数 cnt队列中有多少元素

    // 模拟队列
    static int[] q = new int[N];
    static int hh,tt=-1;

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        // 节点个数n
        n = in.nextInt();
        // 边的个数m
        int m = in.nextInt();

        Arrays.fill(h,-1);
        for (int i = 0; i < m; i++) {
            int a = in.nextInt();
            int b = in.nextInt();
            // 在节点a的后边插入b
            add(a,b);
            // 入度+1
            d[b]++;
        }

        // 存在拓扑序列,则输出
        if(topsort()){
            for (int i = 0; i < n; i++) {

                System.out.print(q[i] + " ");

            }
        }
        else{
            System.out.println(-1);
        }
        
    }

    /**
     * 在节点a的后面插入b
     * @param a
     * @param b
     */
    private static void add(int a, int b){
        e[idx] = b;
        ne[idx] = h[a];
        h[a] = idx++;
    }

    private static boolean topsort(){
        // 把入度为0的节点入队
        for (int i = 1; i <= n; i++) { // i是点的编号
            if(d[i]==0){
                q[++tt] = i;//入队
            }

        }
        // BFS 访问
        while(hh <= tt){
            // 取队头节点t
            int t = q[hh++];
            // 遍历t的单链表,删除节点t,所以将节点t的所有后继j的入度-1
            for (int i = h[t]; i != -1; i = ne[i]) {
                int j = e[i];  // 索引对应的值 因为入度用值作为下标
                if(--d[j] == 0){
                    q[++tt] = j; // 入度为0加入队列
                }

            }
        }
        // 如果所有n个节点都加入过队列,就存在拓扑序
        return tt == n - 1;
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91

207. 课程表

207. 课程表

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。

例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

示例 1:

输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
示例 2:

输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成​课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。

提示:

1 <= numCourses <= 105
0 <= <= 5000
prerequisites[i].length == 2
0 <= ai, bi < numCourses
prerequisites[i] 中的所有课程对 互不相同

class Solution {
    int N = 100010;
    // 邻接表
    int[] h = new int[N]; // 邻接表的头节点 h[v] 其中v为节点值
    int[] e = new int[N]; // e[i] = v 索引和值的映射
    int[] ne = new int[N]; // 下一个节点
    int idx = 1;
    // 入度
    int[] d = new int[N];
    // 模拟队列
    int[] q = new int[N];
    int tt = -1,hh;
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        // 将所有边都用邻接表表示
        Arrays.fill(h,-1);
        for(int i=0;i<prerequisites.length;i++){
            // 在节点a的后边插入b
            add(prerequisites[i][0],prerequisites[i][1]);
            // 入度+1
            d[prerequisites[i][1]]++;
        }

        return topSort(numCourses);

    }

    // 插入节点b
    private void add(int a, int b){
        e[idx] = b;
        ne[idx] = h[a];
        h[a] = idx++;
    }

    private boolean topSort(int numCourses){
        // 把所有入度为0的入队

        for(int i=0;i<numCourses;i++){
            if(d[i]==0){
                q[++tt] = i;
            }
        }
        //  BFS 遍历队列
        while(hh<=tt){
            // 队首节点出队
            int t = q[hh++];

            // 找到t对应的邻接表,将其入度都减1
            for(int i = h[t];i!=-1;i = ne[i]){
                int j = e[i];// i对应的值
                if(--d[j] == 0){ // 入度为0 入队
                    q[++tt] = j;
                }
            }
        }
        // 如果所有n个节点都加入过队列,就存在拓扑序
        return tt == numCourses -1;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

210. 课程表 II

210. 课程表 II
现在你总共有 n 门课需要选,记为 0 到 n-1。

在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]

给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。

可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。

示例 1:

输入: 2, [[1,0]]
输出: [0,1]
解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
示例 2:

输入: 4, [[1,0],[2,0],[3,1],[3,2]]
输出: [0,1,2,3] or [0,2,1,3]
解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
说明:

输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法。
你可以假定输入的先决条件中没有重复的边。
提示:

这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。
通过 DFS 进行拓扑排序 - 一个关于Coursera的精彩视频教程(21分钟),介绍拓扑排序的基本概念。
拓扑排序也可以通过 BFS 完成。


和207一样 只是要把排序输出

class Solution {
        int N = 100010;
    // 邻接表
    int[] h = new int[N]; // 邻接表的头节点 h[v] 其中v为节点值
    int[] e = new int[N]; // e[i] = v 索引和值的映射
    int[] ne = new int[N]; // 下一个节点
    int idx = 1;
    // 入度
    int[] d = new int[N];
    // 模拟队列
    int[] q = new int[N];
    int tt = -1,hh;
    // 存储答案
    int[] result;
    int index;
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        result = new int[numCourses];
        index = numCourses-1;
        // 将所有边都用邻接表表示
        Arrays.fill(h,-1);
        for(int i=0;i<prerequisites.length;i++){
            // 在节点a的后边插入b
            add(prerequisites[i][0],prerequisites[i][1]);
            // 入度+1
            d[prerequisites[i][1]]++;
        }

        return topSort(numCourses);

    }

    // 插入节点b
    private void add(int a, int b){
        e[idx] = b;
        ne[idx] = h[a];
        h[a] = idx++;
    }

    private int[] topSort(int numCourses){
        // 把所有入度为0的入队

        for(int i=0;i<numCourses;i++){
            if(d[i]==0){
                q[++tt] = i;
            }
        }
        //  BFS 遍历队列
        while(hh<=tt){
            // 队首节点出队
            int t = q[hh++];
            result[index--] = t;

            // 找到t对应的邻接表,将其入度都减1
            for(int i = h[t];i!=-1;i = ne[i]){
                int j = e[i];// i对应的值
                if(--d[j] == 0){ // 入度为0 入队
                    q[++tt] = j;
                }
            }
        }
        if (index !=-1) {
            return new int[0];
        }
        return result;
    }    
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

蚂蚁三面

给n个三元组,例如[‘a’,‘b’,‘N’],[‘c’,‘d’,‘S’],[‘a’,‘d’,‘S’], N和S分别表示前一个节点在后一个节点的北边或南边。 判断给的这个三元组是否成立

vivo提前批

图像从传感器到输出JPEG格式图片经过很多node处理,这些node构成一个图像处理的pipeline,其中的有些节点依赖于其他节点输出。A->B表示B的执行依赖于A。
假设每个node执行时间为A(t),即node A需要执行t秒,没有依赖的node可以并行执行。编写一个方法输入一个有向无环图pipeline,输出执行完需要的最短时间。
【输入】:第一行输入node的执行时间,第二行输入node的依赖关系。
3,1,2,5,3,1
2,3,4;5;5,6;0;0;6;0
【输出】:最短时间。
9