{ }codelper
トポロジカルソート深さ優先探索P0326

古くなったファイルだけ作り直す

ファイルが n 個あり、1 番から n 番までの番号がある。
i 番のファイルには、最後に直した時刻 t_i と、そのファイルを作るときに使う材料ファイルのリストがある。
材料リストに何もないファイルは人が直接書いた原本であり、作り直すことはない。
材料をたどって元のファイルに戻ることはない。
目標のファイル g を最新にするために g を「調べる」。
ファイル x を調べるとは次のことである。
x をすでに調べていれば何もしない。
そうでなければ、まず x の材料をリストに書かれた順に 1 つずつ調べる。
材料をすべて調べたあと、材料の中に今回作り直したファイルが 1 つでもあるか、直した時刻が t_x より大きいファイルが 1 つでもあれば、x を作り直す。
直した時刻が t_x と同じ材料は x より新しくない。
g を調べる間に一度も調べなかったファイルは、古くなっていても作り直さない。
例では、1 番を調べるとまず材料の 3 番を調べる。
3 番は材料の 4 番と 5 番を調べたあと、5 番(70)が自分(60)より新しいので作り直す。
次に 2 番を調べるが、材料の 4 番(30)が 2 番(40)より古いのでそのままにする。
1 番(80)はどの材料よりもあとで直されたが、3 番をいま作り直したので 1 番も作り直す。
6 番は材料の 5 番より古いが、1 番を作るのに使われないのでそのままにする。
作り直すファイルを、作り直す順に求めよ。

入力
1 行目に n と g が空白で区切られて与えられる。
2 行目に t_1 から t_n までが空白 1 つで区切られて与えられる。
続く n 行のうち i 行目には、i 番のファイルの材料の数 k と材料の番号 k 個が空白 1 つで区切られて与えられる(材料がなければ 0 だけ)。
1 つのファイルの材料リストに同じ番号は 2 回出ない。
1 ≤ n ≤ 5,000、1 ≤ g ≤ n、1 ≤ t_i ≤ 1,000,000,000、すべてのファイルの材料の数の合計は 10,000 以下。
g から材料をたどって下りるどの道にも、g を含めてファイルは 1,000 個を超えない
出力
1 行目に作り直すファイルの数。
2 行目にその番号を作り直す順に空白 1 つで区切って出力する。
作り直すファイルがなければ 1 行目に 0 だけを出力し、2 行目は出力しない
例 1 · stdin
6 1
80 40 60 30 70 10
2 3 2
1 4
2 4 5
0
0
1 5
stdout
2
3 1
main.py
入力(stdin)

実行すると、これらの行が順に input()・標準入力に渡されます(実行中に入力を求めません)。

$ 準備完了

コードを入力して解くにはパソコンで開いてください。モバイルでは問題と解答を見られます。

ほかの言語では?言語別の解答を見る · 8 言語の解答

材料をすべて調べ終えてから自分を書き留める順番は、深さ優先探索の「出るときに記録する」順(帰りがけ順)で、これがそのままトポロジカルソートです。
どのファイルも自分の材料より先には出ません。
入ってくる辺の数を数えてキューから取り出す方法もトポロジカルソートですが、ここでは目標に必要なファイルだけを、材料が書かれた順に作るので、再帰が合います。
再帰の深さは言語で違います。
Python は既定の上限が 1,000 なので、材料が 1,000 段続くと RecursionError で止まります。
sys.setrecursionlimit(10000) のように上げます。
JS・TS・Java・C#・C・C++・Craykon はこの深さでそのまま動きます。
関数の中から外の印のリストを書き換える方法も違います。
Python は内側の関数から外のリストの要素を変えるのは nonlocal なしででき、Java・C# は static フィールド、C はグローバル配列、JS・TS・C++ は外の変数をそのまま使います。
Craykon も関数の中で looked[x] = 真; のように外のリストを直接書き換えます。
罠 1: 時刻が同じなら新しくありません。
>= ではなく > です。
罠 2: 材料に書かれた時刻が古くても、その材料を今回作り直したなら自分も作り直します(例の 1 番)。
だから「調べた」と「作り直した」の印を別々に持ちます。
罠 3: 調べたファイルに印を付けないと、2 つのファイルが共有する材料を 2 回作ります。
罠 4: 1 番から n 番まで全部調べると、目標に必要ない古いファイル(例の 6 番)まで作ります。
罠 5: 材料を番号順に並べ替えて調べたり、選んだファイルを番号順に出力したりすると順番が変わります(例の答えは 1 3 ではなく 3 1)。

Pythonmain.py · 37 行
import sys sys.setrecursionlimit(10000)  # 재료를 따라 1,000단계까지 내려간다(기본 한도는 1,000) def main():    data = sys.stdin.read().split()    n, goal = int(data[0]), int(data[1])    t = [0] + [int(x) for x in data[2:2 + n]]    deps = [[]]    pos = 2 + n    for _ in range(n):        k = int(data[pos])        deps.append([int(x) for x in data[pos + 1:pos + 1 + k]])        pos += 1 + k     looked = [False] * (n + 1)   # 이미 살핀 파일    rebuilt = [False] * (n + 1)  # 이번에 다시 만든 파일    order = []                   # 다시 만든 차례     def look(x):        looked[x] = True        stale = False        for a in deps[x]:        # 재료를 적힌 차례대로 먼저 살핀다            if not looked[a]:                look(a)            if rebuilt[a] or t[a] > t[x]:                stale = True        if stale:                # 재료를 모두 살핀 뒤에야 자기를 만든다            rebuilt[x] = True            order.append(x)     look(goal)    print(len(order))    if order:        print(" ".join(map(str, order))) main()
JavaScriptmain.js · 34 行
const data = require("fs").readFileSync(0, "utf8").split(/\s+/).filter(Boolean).map(Number);const n = data[0];const goal = data[1];const t = [0, ...data.slice(2, 2 + n)];const deps = [[]];let pos = 2 + n;for (let i = 0; i < n; i++) {  const k = data[pos];  deps.push(data.slice(pos + 1, pos + 1 + k));  pos += 1 + k;} const looked = new Array(n + 1).fill(false); // 이미 살핀 파일const rebuilt = new Array(n + 1).fill(false); // 이번에 다시 만든 파일const order = []; // 다시 만든 차례 function look(x) {  looked[x] = true;  let stale = false;  for (const a of deps[x]) {    // 재료를 적힌 차례대로 먼저 살핀다    if (!looked[a]) look(a);    if (rebuilt[a] || t[a] > t[x]) stale = true;  }  if (stale) {    // 재료를 모두 살핀 뒤에야 자기를 만든다    rebuilt[x] = true;    order.push(x);  }} look(goal);console.log(order.length);if (order.length > 0) console.log(order.join(" "));
TypeScriptmain.ts · 35 行
import { readFileSync } from "node:fs";const data: number[] = readFileSync(0, "utf8").split(/\s+/).filter(Boolean).map(Number);const n: number = data[0];const goal: number = data[1];const t: number[] = [0, ...data.slice(2, 2 + n)];const deps: number[][] = [[]];let pos = 2 + n;for (let i = 0; i < n; i++) {  const k = data[pos];  deps.push(data.slice(pos + 1, pos + 1 + k));  pos += 1 + k;} const looked: boolean[] = new Array<boolean>(n + 1).fill(false); // 이미 살핀 파일const rebuilt: boolean[] = new Array<boolean>(n + 1).fill(false); // 이번에 다시 만든 파일const order: number[] = []; // 다시 만든 차례 function look(x: number): void {  looked[x] = true;  let stale = false;  for (const a of deps[x]) {    // 재료를 적힌 차례대로 먼저 살핀다    if (!looked[a]) look(a);    if (rebuilt[a] || t[a] > t[x]) stale = true;  }  if (stale) {    // 재료를 모두 살핀 뒤에야 자기를 만든다    rebuilt[x] = true;    order.push(x);  }} look(goal);console.log(order.length);if (order.length > 0) console.log(order.join(" "));
JavaMain.java · 58 行
import java.io.BufferedReader;import java.io.InputStreamReader;import java.io.StreamTokenizer;import java.util.ArrayList;import java.util.List; public class Main {    static int[] t;    static int[][] deps;    static boolean[] looked;   // 이미 살핀 파일    static boolean[] rebuilt;  // 이번에 다시 만든 파일    static List<Integer> order = new ArrayList<>(); // 다시 만든 차례     static void look(int x) {        looked[x] = true;        boolean stale = false;        for (int a : deps[x]) {          // 재료를 적힌 차례대로 먼저 살핀다            if (!looked[a]) look(a);            if (rebuilt[a] || t[a] > t[x]) stale = true;        }        if (stale) {                     // 재료를 모두 살핀 뒤에야 자기를 만든다            rebuilt[x] = true;            order.add(x);        }    }     public static void main(String[] args) throws Exception {        StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));        in.nextToken();        int n = (int) in.nval;        in.nextToken();        int goal = (int) in.nval;        t = new int[n + 1];        for (int i = 1; i <= n; i++) {            in.nextToken();            t[i] = (int) in.nval;        }        deps = new int[n + 1][];        for (int i = 1; i <= n; i++) {            in.nextToken();            int k = (int) in.nval;            deps[i] = new int[k];            for (int j = 0; j < k; j++) {                in.nextToken();                deps[i][j] = (int) in.nval;            }        }        looked = new boolean[n + 1];        rebuilt = new boolean[n + 1];        look(goal);         StringBuilder out = new StringBuilder();        out.append(order.size()).append('\n');        for (int i = 0; i < order.size(); i++) out.append(i > 0 ? " " : "").append(order.get(i));        if (!order.isEmpty()) out.append('\n');        System.out.print(out);    }}
C#Main.cs · 50 行
using System;using System.Collections.Generic; class Program{    static int[] t;    static int[][] deps;    static bool[] looked;   // 이미 살핀 파일    static bool[] rebuilt;  // 이번에 다시 만든 파일    static List<int> order = new List<int>(); // 다시 만든 차례     static void Look(int x)    {        looked[x] = true;        bool stale = false;        foreach (int a in deps[x])       // 재료를 적힌 차례대로 먼저 살핀다        {            if (!looked[a]) Look(a);            if (rebuilt[a] || t[a] > t[x]) stale = true;        }        if (stale)                       // 재료를 모두 살핀 뒤에야 자기를 만든다        {            rebuilt[x] = true;            order.Add(x);        }    }     static void Main()    {        string[] data = Console.In.ReadToEnd().Split((char[])null, StringSplitOptions.RemoveEmptyEntries);        int n = int.Parse(data[0]), goal = int.Parse(data[1]);        t = new int[n + 1];        for (int i = 1; i <= n; i++) t[i] = int.Parse(data[1 + i]);        deps = new int[n + 1][];        int pos = 2 + n;        for (int i = 1; i <= n; i++)        {            int k = int.Parse(data[pos]);            deps[i] = new int[k];            for (int j = 0; j < k; j++) deps[i][j] = int.Parse(data[pos + 1 + j]);            pos += 1 + k;        }        looked = new bool[n + 1];        rebuilt = new bool[n + 1];        Look(goal);         Console.WriteLine(order.Count);        if (order.Count > 0) Console.WriteLine(string.Join(" ", order));    }}
C++main.cpp · 44 行
#include <iostream>#include <vector> std::vector<int> t;std::vector<std::vector<int>> deps;std::vector<bool> looked;   // 이미 살핀 파일std::vector<bool> rebuilt;  // 이번에 다시 만든 파일std::vector<int> order;     // 다시 만든 차례 void look(int x) {    looked[x] = true;    bool stale = false;    for (int a : deps[x]) {              // 재료를 적힌 차례대로 먼저 살핀다        if (!looked[a]) look(a);        if (rebuilt[a] || t[a] > t[x]) stale = true;    }    if (stale) {                         // 재료를 모두 살핀 뒤에야 자기를 만든다        rebuilt[x] = true;        order.push_back(x);    }} int main() {    std::ios::sync_with_stdio(false);    std::cin.tie(nullptr);    int n, goal;    std::cin >> n >> goal;    t.assign(n + 1, 0);    for (int i = 1; i <= n; i++) std::cin >> t[i];    deps.assign(n + 1, {});    for (int i = 1; i <= n; i++) {        int k;        std::cin >> k;        deps[i].resize(k);        for (int &a : deps[i]) std::cin >> a;    }    looked.assign(n + 1, false);    rebuilt.assign(n + 1, false);    look(goal);     std::cout << order.size() << '\n';    for (std::size_t i = 0; i < order.size(); i++) std::cout << (i ? " " : "") << order[i];    if (!order.empty()) std::cout << '\n';}
Cmain.c · 44 行
#include <stdio.h> #define MAXN 5001#define MAXM 10000 int t[MAXN];int start[MAXN + 1];   /* i번 파일의 재료는 dep[start[i]] … dep[start[i + 1] - 1] */int dep[MAXM + 1];int looked[MAXN];      /* 이미 살핀 파일 */int rebuilt[MAXN];     /* 이번에 다시 만든 파일 */int order[MAXN], made = 0; void look(int x) {    looked[x] = 1;    int stale = 0;    for (int j = start[x]; j < start[x + 1]; j++) {   /* 재료를 적힌 차례대로 먼저 살핀다 */        int a = dep[j];        if (!looked[a]) look(a);        if (rebuilt[a] || t[a] > t[x]) stale = 1;    }    if (stale) {                                      /* 재료를 모두 살핀 뒤에야 자기를 만든다 */        rebuilt[x] = 1;        order[made++] = x;    }} int main(void) {    int n, goal;    scanf("%d %d", &n, &goal);    for (int i = 1; i <= n; i++) scanf("%d", &t[i]);    int m = 0;    for (int i = 1; i <= n; i++) {        int k;        scanf("%d", &k);        start[i] = m;        for (int j = 0; j < k; j++) scanf("%d", &dep[m++]);    }    start[n + 1] = m;     look(goal);    printf("%d\n", made);    for (int i = 0; i < made; i++) printf(i + 1 < made ? "%d " : "%d\n", order[i]);    return 0;}
Craykonmain.cray · 48 行
첫줄 = コンソール.数読み();n = 첫줄[0];goal = 첫줄[1];t = コンソール.数読み(); // t[i - 1]: i번 파일을 고친 시각deps = [];繰り返す (i = 0; i < n; i++){    줄 = コンソール.数読み();    재료 = [];    繰り返す (j = 1; j <= 줄[0]; j++)    {        재료.追加(줄[j] - 1);    }    deps.追加(재료);} looked = 新リスト(n, 偽); // 이미 살핀 파일rebuilt = 新リスト(n, 偽); // 이번에 다시 만든 파일order = []; // 다시 만든 차례 살피기(x){    looked[x] = 真;    stale = 偽;    それぞれ (a の中 deps[x])    {        もし (!looked[a])        {            살피기(a);        }        もし (rebuilt[a] || t[a] > t[x])        {            stale = 真;        }    }    もし (stale)    {        rebuilt[x] = 真;        order.追加(x + 1);    }} 살피기(goal - 1);コンソール.書く(order.個数);もし (order.個数 > 0){    コンソール.書く(order.つなぐ(" "));}