{ }codelper
拓扑排序队列P0197

遵守先修课的学期计划

某个系有编号 1 到 n 的 n 门课,学期按第 1 学期、第 2 学期、第 3 学期……计数。
每门课开课的学期是固定的。
类型为 0 的课每个学期都开,类型为 1 的课只在奇数学期(1, 3, 5……)开,类型为 2 的课只在偶数学期(2, 4, 6……)开。
给出 m 条先修条件,一条条件“a b”表示要等修 a 号课的那个学期结束之后,才能修 b 号课。
同一条件可能给出多次。
一个学期修的课数没有限制,每门课都在能修的最早学期修。
也就是说,设一门课的各门先修课所在学期中最晚的是 L(没有先修课时 L = 0),这门课就在 L 之后、这门课开课的最早学期修。
条件可能形成环,使某些课永远无法修。
在环里的课,以及必须先修这类课的课,都不能修。
第一行给出 n 和 m,第二行给出 1 号到 n 号课的 n 个类型,用空格分隔。
接下来 m 行每行给出用空格分隔的 a 和 b。
第一行输出 1 号到 n 号课所在的学期,用空格分隔。
不能修的课输出 -1。
第二行输出修完所有能修的课所需的学期数,也就是第一行中最大的学期。
如果一门能修的课都没有,输出 0。

输入
1 ≤ n ≤ 10,000,0 ≤ m ≤ 10,000,课的类型是 0、1、2 之一,1 ≤ a, b ≤ n,a ≠ b
输出
两行:每门课的学期(不能修则 -1),所需的学期数
样例 1 · stdin
7 6
2 0 1 0 0 0 2
1 2
2 3
1 3
4 5
5 4
5 6
stdout
2 3 5 -1 -1 -1 2
5
main.py
输入(stdin)

运行时这些行会依次作为 input()/标准输入传入(运行中不会等待输入)。

$ 就绪

请在电脑上打开以编写和运行代码。在手机上可以查看题目和题解。

其他语言怎么写?查看各语言题解 · 8 种语言题解

先数好每门课还剩几门先修课(waiting),把变成 0 的课放进队列依次取出,这就是拓扑排序(Kahn 算法)。
取出一门课时,它的先修课都已修完,所以可以定下学期。
从只看先修课得到的最早学期 earliest 开始,如果这门课在那个学期不开,就推迟一个学期(奇偶用 t % 2 判断)。
然后把每门后续课的 earliest 提高到 max(当前值, 这门课的学期 + 1)。
队列各语言不同:Python 有 collections.deque 的 popleft()(列表的 pop(0) 每次都要把后面往前挪,很慢),Java ArrayDeque,C# Queue<int>,C++ std::queue;JS、TS 没有队列,shift() 也慢,所以只往数组里放,移动队头下标 head。
C 用数组加队头、队尾下标,Craykon 也只对列表 添加 并移动 head。
为每门课保存后续课列表的方法也不同:Python [[] for _ in range(n + 1)]([[]] * (n + 1) 是同一个列表被指向 n+1 次),Java List<List<Integer>>,C 用 head、next、to 三个数组串起来。
五个陷阱:① 用第一个到达的先修课来定学期是错的。
有 1→2→3 和 1→3 时,3 号课必须在 2 号课之后修。
总要看最晚的先修课。
② 如果把学期先填成 0,因为环而始终取不出来的课会留下 0。
要填 -1 或另外判断。
③ 用沿先修课往回走的递归来解,在环里会无限循环,在 10,000 步的链上会超过 Python 的递归上限(默认 1,000)而停止(Craykon 也只能到约 3,000 层)。
④ 同一条件重复时,如果后续课列表用集合只连一次,而 waiting 按行计数,那门课的 waiting 永远到不了 0,变成 -1。
两者要用同样的方式计数。
⑤ 为配合开课学期而推迟后的值必须传给后续课。
如果先不管开课学期把学期全部算完,最后再逐门课调整奇偶,被推迟的课后面的课会早一个学期。
没有先修课的课,类型为 2 时也是第 2 学期而不是第 1 学期。

Pythonmain.py · 36 行
import sysfrom collections import deque  def first_open(t, kind):    """t학기부터 세어 그 과목이 열리는 가장 이른 학기"""    if (kind == 1 and t % 2 == 0) or (kind == 2 and t % 2 == 1):        return t + 1    return t  data = sys.stdin.buffer.read().split()n, m = int(data[0]), int(data[1])kind = [0] + [int(x) for x in data[2:2 + n]]  # 0: 모든 학기, 1: 홀수 학기, 2: 짝수 학기after = [[] for _ in range(n + 1)]  # after[a] = a를 들어야 들을 수 있는 과목들waiting = [0] * (n + 1)            # 아직 듣지 않은 선수 과목의 수for i in range(m):    a, b = int(data[2 + n + 2 * i]), int(data[3 + n + 2 * i])    after[a].append(b)    waiting[b] += 1 earliest = [1] * (n + 1)  # 선수 과목만 보면 들을 수 있는 가장 이른 학기term = [-1] * (n + 1)     # 끝까지 -1이면 고리 때문에 들을 수 없다q = deque(v for v in range(1, n + 1) if waiting[v] == 0)while q:    a = q.popleft()    term[a] = first_open(earliest[a], kind[a])  # 선수 과목을 다 들었으니 이제 학기가 정해진다    for b in after[a]:        # 선수 과목 가운데 가장 늦은 것의 다음 학기        earliest[b] = max(earliest[b], term[a] + 1)        waiting[b] -= 1        if waiting[b] == 0:            q.append(b) print(" ".join(map(str, term[1:])))print(max([0] + term[1:]))  # 들을 수 있는 과목이 없으면 0
JavaScriptmain.js · 35 行
const data = require("fs").readFileSync(0, "utf8").trim().split(/\s+/).map(Number);const [n, m] = data;const kind = [0, ...data.slice(2, 2 + n)]; // 0: 모든 학기, 1: 홀수 학기, 2: 짝수 학기 // t학기부터 세어 그 과목이 열리는 가장 이른 학기function firstOpen(t, k) {  if ((k === 1 && t % 2 === 0) || (k === 2 && t % 2 === 1)) return t + 1;  return t;} const after = Array.from({ length: n + 1 }, () => []); // after[a] = a를 들어야 들을 수 있는 과목들const waiting = new Array(n + 1).fill(0); // 아직 듣지 않은 선수 과목의 수for (let i = 0; i < m; i++) {  const a = data[2 + n + 2 * i], b = data[3 + n + 2 * i];  after[a].push(b);  waiting[b]++;} const earliest = new Array(n + 1).fill(1); // 선수 과목만 보면 들을 수 있는 가장 이른 학기const term = new Array(n + 1).fill(-1); // 끝까지 -1이면 고리 때문에 들을 수 없다const queue = [];for (let v = 1; v <= n; v++) if (waiting[v] === 0) queue.push(v);// shift()는 매번 앞을 당겨 느리니 머리 번호 head로 꺼낸다for (let head = 0; head < queue.length; head++) {  const a = queue[head];  term[a] = firstOpen(earliest[a], kind[a]); // 선수 과목을 다 들었으니 이제 학기가 정해진다  for (const b of after[a]) {    earliest[b] = Math.max(earliest[b], term[a] + 1); // 가장 늦은 선수 과목의 다음 학기    if (--waiting[b] === 0) queue.push(b);  }} const terms = term.slice(1);console.log(terms.join(" "));console.log(terms.reduce((x, y) => Math.max(x, y), 0));
TypeScriptmain.ts · 36 行
import { readFileSync } from "node:fs"; const data: number[] = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);const n = data[0], m = data[1];const kind: number[] = [0, ...data.slice(2, 2 + n)]; // 0: 모든 학기, 1: 홀수 학기, 2: 짝수 학기 // t학기부터 세어 그 과목이 열리는 가장 이른 학기function firstOpen(t: number, k: number): number {  if ((k === 1 && t % 2 === 0) || (k === 2 && t % 2 === 1)) return t + 1;  return t;} const after: number[][] = Array.from({ length: n + 1 }, () => []); // after[a] = a를 들어야 들을 수 있는 과목들const waiting: number[] = new Array<number>(n + 1).fill(0); // 아직 듣지 않은 선수 과목의 수for (let i = 0; i < m; i++) {  const a = data[2 + n + 2 * i], b = data[3 + n + 2 * i];  after[a].push(b);  waiting[b]++;} const earliest: number[] = new Array<number>(n + 1).fill(1); // 선수 과목만 보면 들을 수 있는 가장 이른 학기const term: number[] = new Array<number>(n + 1).fill(-1); // 끝까지 -1이면 고리 때문에 들을 수 없다const queue: number[] = [];for (let v = 1; v <= n; v++) if (waiting[v] === 0) queue.push(v);for (let head = 0; head < queue.length; head++) {  const a = queue[head];  term[a] = firstOpen(earliest[a], kind[a]); // 선수 과목을 다 들었으니 이제 학기가 정해진다  for (const b of after[a]) {    earliest[b] = Math.max(earliest[b], term[a] + 1); // 가장 늦은 선수 과목의 다음 학기    if (--waiting[b] === 0) queue.push(b);  }} const terms = term.slice(1);console.log(terms.join(" "));console.log(terms.reduce((x, y) => Math.max(x, y), 0));
JavaMain.java · 61 行
import java.io.*;import java.util.*; public class Main {    // t학기부터 세어 그 과목이 열리는 가장 이른 학기(kind 0: 모든 학기, 1: 홀수 학기, 2: 짝수 학기)    static int firstOpen(int t, int kind) {        if ((kind == 1 && t % 2 == 0) || (kind == 2 && t % 2 == 1)) return t + 1;        return t;    }     public static void main(String[] args) throws IOException {        StreamTokenizer st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));        st.nextToken();        int n = (int) st.nval;        st.nextToken();        int m = (int) st.nval;        int[] kind = new int[n + 1];        for (int v = 1; v <= n; v++) {            st.nextToken();            kind[v] = (int) st.nval;        }        List<List<Integer>> after = new ArrayList<>(); // after.get(a) = a를 들어야 들을 수 있는 과목들        for (int v = 0; v <= n; v++) after.add(new ArrayList<>());        int[] waiting = new int[n + 1]; // 아직 듣지 않은 선수 과목의 수        for (int i = 0; i < m; i++) {            st.nextToken();            int a = (int) st.nval;            st.nextToken();            int b = (int) st.nval;            after.get(a).add(b);            waiting[b]++;        }         int[] earliest = new int[n + 1]; // 선수 과목만 보면 들을 수 있는 가장 이른 학기        Arrays.fill(earliest, 1);        int[] term = new int[n + 1];        Arrays.fill(term, -1); // 끝까지 -1이면 고리 때문에 들을 수 없다        ArrayDeque<Integer> q = new ArrayDeque<>();        for (int v = 1; v <= n; v++) {            if (waiting[v] == 0) q.add(v);        }        while (!q.isEmpty()) {            int a = q.poll();            term[a] = firstOpen(earliest[a], kind[a]); // 선수 과목을 다 들었으니 이제 학기가 정해진다            for (int b : after.get(a)) {                earliest[b] = Math.max(earliest[b], term[a] + 1); // 가장 늦은 선수 과목의 다음 학기                if (--waiting[b] == 0) q.add(b);            }        }         StringBuilder sb = new StringBuilder();        int most = 0;        for (int v = 1; v <= n; v++) {            if (v > 1) sb.append(' ');            sb.append(term[v]);            most = Math.max(most, term[v]);        }        sb.append('\n').append(most).append('\n');        System.out.print(sb);    }}
C#Main.cs · 61 行
using System;using System.Collections.Generic;using System.Text; class Program{    // t학기부터 세어 그 과목이 열리는 가장 이른 학기(kind 0: 모든 학기, 1: 홀수 학기, 2: 짝수 학기)    static int FirstOpen(int t, int kind)    {        if ((kind == 1 && t % 2 == 0) || (kind == 2 && t % 2 == 1)) return t + 1;        return t;    }     static void Main()    {        var data = Console.In.ReadToEnd().Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);        int n = int.Parse(data[0]), m = int.Parse(data[1]);        var kind = new int[n + 1];        for (int v = 1; v <= n; v++) kind[v] = int.Parse(data[1 + v]);        var after = new List<int>[n + 1]; // after[a] = a를 들어야 들을 수 있는 과목들        for (int v = 0; v <= n; v++) after[v] = new List<int>();        var waiting = new int[n + 1]; // 아직 듣지 않은 선수 과목의 수        for (int i = 0; i < m; i++)        {            int a = int.Parse(data[2 + n + 2 * i]), b = int.Parse(data[3 + n + 2 * i]);            after[a].Add(b);            waiting[b]++;        }         var earliest = new int[n + 1]; // 선수 과목만 보면 들을 수 있는 가장 이른 학기        Array.Fill(earliest, 1);        var term = new int[n + 1];        Array.Fill(term, -1); // 끝까지 -1이면 고리 때문에 들을 수 없다        var q = new Queue<int>();        for (int v = 1; v <= n; v++)        {            if (waiting[v] == 0) q.Enqueue(v);        }        while (q.Count > 0)        {            int a = q.Dequeue();            term[a] = FirstOpen(earliest[a], kind[a]); // 선수 과목을 다 들었으니 이제 학기가 정해진다            foreach (int b in after[a])            {                earliest[b] = Math.Max(earliest[b], term[a] + 1); // 가장 늦은 선수 과목의 다음 학기                if (--waiting[b] == 0) q.Enqueue(b);            }        }         var sb = new StringBuilder();        int most = 0;        for (int v = 1; v <= n; v++)        {            if (v > 1) sb.Append(' ');            sb.Append(term[v]);            most = Math.Max(most, term[v]);        }        sb.Append('\n').Append(most).Append('\n');        Console.Write(sb);    }}
C++main.cpp · 50 行
#include <algorithm>#include <iostream>#include <queue>#include <vector> // t학기부터 세어 그 과목이 열리는 가장 이른 학기(kind 0: 모든 학기, 1: 홀수 학기, 2: 짝수 학기)int firstOpen(int t, int kind) {    if ((kind == 1 && t % 2 == 0) || (kind == 2 && t % 2 == 1)) return t + 1;    return t;} int main() {    std::ios::sync_with_stdio(false);    std::cin.tie(nullptr);    int n, m;    std::cin >> n >> m;    std::vector<int> kind(n + 1);    for (int v = 1; v <= n; v++) std::cin >> kind[v];    std::vector<std::vector<int>> after(n + 1); // after[a] = a를 들어야 들을 수 있는 과목들    std::vector<int> waiting(n + 1, 0);          // 아직 듣지 않은 선수 과목의 수    for (int i = 0; i < m; i++) {        int a, b;        std::cin >> a >> b;        after[a].push_back(b);        waiting[b]++;    }     std::vector<int> earliest(n + 1, 1); // 선수 과목만 보면 들을 수 있는 가장 이른 학기    std::vector<int> term(n + 1, -1);    // 끝까지 -1이면 고리 때문에 들을 수 없다    std::queue<int> q;    for (int v = 1; v <= n; v++) {        if (waiting[v] == 0) q.push(v);    }    while (!q.empty()) {        int a = q.front();        q.pop();        term[a] = firstOpen(earliest[a], kind[a]); // 선수 과목을 다 들었으니 이제 학기가 정해진다        for (int b : after[a]) {            earliest[b] = std::max(earliest[b], term[a] + 1); // 가장 늦은 선수 과목의 다음 학기            if (--waiting[b] == 0) q.push(b);        }    }     int most = 0;    for (int v = 1; v <= n; v++) {        std::cout << (v > 1 ? " " : "") << term[v];        most = std::max(most, term[v]);    }    std::cout << "\n" << most << "\n";}
Cmain.c · 59 行
#include <stdio.h>#include <stdlib.h> // t학기부터 세어 그 과목이 열리는 가장 이른 학기(kind 0: 모든 학기, 1: 홀수 학기, 2: 짝수 학기)int first_open(int t, int kind) {    if ((kind == 1 && t % 2 == 0) || (kind == 2 && t % 2 == 1)) return t + 1;    return t;} int main(void) {    int n, m;    if (scanf("%d %d", &n, &m) != 2) return 0;    int *kind = malloc((n + 1) * sizeof(int));    for (int v = 1; v <= n; v++) scanf("%d", &kind[v]);    // 이음 목록을 배열 셋으로: head[a]에서 시작해 next[e]로 따라가며 to[e]를 본다    int *head = malloc((n + 1) * sizeof(int));    int *next = malloc((m + 1) * sizeof(int));    int *to = malloc((m + 1) * sizeof(int));    int *waiting = calloc(n + 1, sizeof(int)); // 아직 듣지 않은 선수 과목의 수    int *earliest = malloc((n + 1) * sizeof(int)); // 선수 과목만 보면 들을 수 있는 가장 이른 학기    int *term = malloc((n + 1) * sizeof(int));    int *queue = malloc((n + 1) * sizeof(int));    for (int v = 0; v <= n; v++) {        head[v] = -1;        earliest[v] = 1;        term[v] = -1; // 끝까지 -1이면 고리 때문에 들을 수 없다    }    for (int e = 0; e < m; e++) {        int a, b;        scanf("%d %d", &a, &b);        to[e] = b;        next[e] = head[a];        head[a] = e;        waiting[b]++;    }     int qh = 0, qt = 0;    for (int v = 1; v <= n; v++) {        if (waiting[v] == 0) queue[qt++] = v;    }    while (qh < qt) {        int a = queue[qh++];        term[a] = first_open(earliest[a], kind[a]); // 선수 과목을 다 들었으니 이제 학기가 정해진다        for (int e = head[a]; e != -1; e = next[e]) {            int b = to[e];            if (term[a] + 1 > earliest[b]) earliest[b] = term[a] + 1; // 가장 늦은 선수 과목의 다음 학기            if (--waiting[b] == 0) queue[qt++] = b;        }    }     int most = 0;    for (int v = 1; v <= n; v++) {        printf(v > 1 ? " %d" : "%d", term[v]);        if (term[v] > most) most = term[v];    }    printf("\n%d\n", most);    free(kind); free(head); free(next); free(to); free(waiting); free(earliest); free(term); free(queue);    return 0;}
Craykonmain.cray · 64 行
// t학기부터 세어 그 과목이 열리는 가장 이른 학기(종류 0: 모든 학기, 1: 홀수 학기, 2: 짝수 학기)열리는학기(t, 종류){    如果 (종류 == 1 && t % 2 == 0 || 종류 == 2 && t % 2 == 1)    {        返回 t + 1;    }    返回 t;} 첫줄 = 控制台.读数字();n = 첫줄[0];m = 첫줄[1];// 목록은 10,000칸까지라 과목 번호에서 1을 빼 0번부터 둔다kind = 控制台.读数字(); after = []; // after[a] = a를 들어야 들을 수 있는 과목들循环 (v = 0; v < n; v++){    after.添加([]);}waiting = 新列表(n, 0); // 아직 듣지 않은 선수 과목의 수循环 (i = 0; i < m; i++){    쌍 = 控制台.读数字();    after[쌍[0] - 1].添加(쌍[1] - 1);    waiting[쌍[1] - 1] += 1;} earliest = 新列表(n, 1); // 선수 과목만 보면 들을 수 있는 가장 이른 학기term = 新列表(n, -1); // 끝까지 -1이면 고리 때문에 들을 수 없다queue = [];循环 (v = 0; v < n; v++){    如果 (waiting[v] == 0)    {        queue.添加(v);    }}// 앞에서 꺼내는 대신 머리 번호 head를 옮긴다head = 0;当 (head < queue.数量){    a = queue[head];    head++;    term[a] = 열리는학기(earliest[a], kind[a]); // 선수 과목을 다 들었으니 이제 학기가 정해진다    遍历 (b 在 after[a])    {        earliest[b] = 数学.较大值(earliest[b], term[a] + 1); // 가장 늦은 선수 과목의 다음 학기        waiting[b] -= 1;        如果 (waiting[b] == 0)        {            queue.添加(b);        }    }} most = 0;遍历 (t 在 term){    most = 数学.较大值(most, t);}控制台.输出(term);控制台.输出(most);