{ }codelper
トポロジカルソート辞書の検索64ビット整数P0215

互いを呼ぶセルがある計算表

計算表にセルが n 個あり、セルごとに名前と式が一つずつある。
一行は "名前 = 項 + 項 + … + 項" の形で、単語の間は空白一つで区切られる。
項は 0 以上の整数か、いずれかのセル(自分自身のこともある)の名前である。
セルの値は式に書かれた項の値をすべて足したもので、同じ名前が式に何度も出れば出た回数だけ足す。
式をたどっていくと最初のセルに戻ってくるセル、つまり輪に入ったセルは値が決まらない。
輪に入ったセルを式に使うセルも、そのようなセルを使うセルも値が決まらない(何段階を経ても同じである)。
入力に書かれた順に、セルごとに一行に "名前 値" を出力せよ。
値が決まらないセルは、値の代わりに ERR と出力する。

入力
一行目に n、続く n 行にセルが一つずつ与えられる。
1 ≤ n ≤ 5,000。
名前は英小文字で始まり英小文字と数字だけからなる 1〜8 文字で、互いに異なる。
式の項は 1〜8 個で、数は 0 以上 1,000,000 以下であり、式に出る名前はすべてどれかのセルの名前である。
すべての式に出る名前の項は合わせて 10,000 個以下である。
値が決まるセルの値は 10^15 以下である。
出力
n 行、各行 "名前 値" または "名前 ERR"
例 1 · stdin
8
a = 3 + b + c
b = 10
c = b + b + 1
d = d + 1
e = a + f
f = e
g = d
h = a + 2
stdout
a 34
b 10
c 21
d ERR
e ERR
f ERR
g ERR
h 36
main.py
入力(stdin)

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

$ 準備完了

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

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

セル j を式に使うセル i について矢印 j → i を引くと、値が決まるセルはトポロジカルソートで順番がつくセルと同じです。
カーン(Kahn)法がすっきりします:セルごとに「まだ値のわからない名前の項の数」(waiting)を数え、0 のセルをキューに入れ、取り出したセルの値をそれを使うセルに足しながら waiting を一つずつ減らします。
最後までキューに入れなかったセルが、まさに輪に入ったか輪にぶら下がったセルです — 輪を別に探す必要はありません。
落とし穴その一:輪に入ったセルだけを ERR にすると間違えます。
例の g は輪 d を使うので ERR です。
再帰でたどって「計算中」のセルに出会ったときそのセルだけ印をつけて 0 と数えると、輪を使うセルにでたらめな数が出ます。
落とし穴その二:b + b は b を二回足します(例の c = 21)。
名前を集合(Set)に集めると一回しか足されません — 待ち数にも「使うセル」のリストにも出た回数だけ入れます。
落とし穴その三:再帰で式をたどると 5,000 セルの鎖で 5,000 段の深さになります。
Python の既定の再帰上限 1,000 を超え、Craykon も数千段で止まります。
キューを使うカーン法は再帰がないので、この心配がありません。
落とし穴その四:値は 10^15 まで行き、32 ビットの int(約 21 億)を超えます(5,000 セルの鎖 × 100 万 = 50 億、倍々に増える鎖は 2^49)。
C・C++ は long long、Java・C# は long が必要で(C#・Java の int は黙って負の数に回り込みます)、Python の整数には上限がなく、JavaScript・TypeScript・Craykon の数は倍精度浮動小数点数で 2^53(約 9×10^15)まで正確なので、そのままで大丈夫です。
名前 → セル番号は辞書で引きます:Python dict、JavaScript・TypeScript Map、Java HashMap<String, Integer>、C# Dictionary<string, int>、C++ std::unordered_map<std::string, int>。
C には辞書がないので、セル番号を名前順に qsort しておき strcmp で二分探索します。
行の分け方も違います:単語の並びは [名前, "=", 項, "+", 項, …] なので、Python w[2::2] のように二つずつ飛ばせば項だけが残ります。
C は fgets で一行読んで strtok で切り、C++ は std::istringstream で単語を読みます。
数か名前かは最初の文字が数字かどうかで見分けます(名前は文字で始まります)。
Craykon は 辞書() で名前 → 番号を持ち、最初の文字は 文字.コード(w[k][0]) が 48〜57("0"〜"9")かどうかで見分けます。
キューはリストに追加するだけにして、前から読む位置(head)を進めます。

Pythonmain.py · 44 行
import sysfrom collections import deque lines = sys.stdin.read().split("\n")n = int(lines[0])names = []const = []  # const[i] = i번 칸 식에 적힌 수들의 합refs = []   # refs[i] = i번 칸 식에 적힌 이름들(같은 이름이 두 번 나오면 두 번)for line in lines[1 : 1 + n]:    w = line.split()    names.append(w[0])    c = 0    r = []    for term in w[2::2]:  # w = [이름, "=", 항, "+", 항, "+", ...]        if term[0].isdigit():            c += int(term)        else:            r.append(term)    const.append(c)    refs.append(r) index = {name: i for i, name in enumerate(names)}users = [[] for _ in range(n)]  # users[j] = j번 칸을 식에 쓰는 칸들(쓴 횟수만큼)waiting = [0] * n               # 아직 값을 모르는, 식에 쓴 이름의 수for i in range(n):    for name in refs[i]:        users[index[name]].append(i)        waiting[i] += 1 # 위상 정렬(Kahn 알고리즘): 기다릴 것이 없는 칸부터 값을 정하고, 그 칸을 쓰는 칸의 기다림을 하나씩 줄인다value = const[:]known = [False] * nqueue = deque(i for i in range(n) if waiting[i] == 0)while queue:    j = queue.popleft()    known[j] = True    for i in users[j]:        value[i] += value[j]        waiting[i] -= 1        if waiting[i] == 0:            queue.append(i) # 끝까지 기다림이 남은 칸은 고리에 들었거나 고리에 든 칸을 (거쳐서라도) 쓴다print("\n".join(f"{names[i]} {value[i] if known[i] else 'ERR'}" for i in range(n)))
JavaScriptmain.js · 43 行
const lines = require("fs").readFileSync(0, "utf8").split("\n");const n = Number(lines[0]);const names = [];const value = []; // 처음엔 식에 적힌 수들의 합, 이름 칸의 값을 알게 될 때마다 더한다const refs = [];  // refs[i] = i번 칸 식에 적힌 이름들(같은 이름이 두 번 나오면 두 번)for (let i = 1; i <= n; i++) {  const w = lines[i].trim().split(" "); // [이름, "=", 항, "+", 항, ...]  names.push(w[0]);  let c = 0;  const r = [];  for (let k = 2; k < w.length; k += 2) {    if (/^[0-9]/.test(w[k])) c += Number(w[k]);    else r.push(w[k]);  }  value.push(c);  refs.push(r);} const index = new Map(names.map((name, i) => [name, i]));const users = Array.from({ length: n }, () => []); // users[j] = j번 칸을 식에 쓰는 칸들(쓴 횟수만큼)const waiting = new Array(n).fill(0); // 아직 값을 모르는, 식에 쓴 이름의 수refs.forEach((r, i) => {  for (const name of r) {    users[index.get(name)].push(i);    waiting[i]++;  }}); // 위상 정렬(Kahn 알고리즘): 기다릴 것이 없는 칸부터 값을 정하고, 그 칸을 쓰는 칸의 기다림을 하나씩 줄인다const known = new Array(n).fill(false);const queue = [];for (let i = 0; i < n; i++) if (waiting[i] === 0) queue.push(i);for (let head = 0; head < queue.length; head++) {  const j = queue[head];  known[j] = true;  for (const i of users[j]) {    value[i] += value[j]; // 10^15까지라 배정밀도 수로도 정확하다    if (--waiting[i] === 0) queue.push(i);  }} // 끝까지 기다림이 남은 칸은 고리에 들었거나 고리에 든 칸을 (거쳐서라도) 쓴다console.log(names.map((name, i) => `${name} ${known[i] ? value[i] : "ERR"}`).join("\n"));
TypeScriptmain.ts · 45 行
import { readFileSync } from "node:fs"; const lines: string[] = readFileSync(0, "utf8").split("\n");const n: number = Number(lines[0]);const names: string[] = [];const value: number[] = []; // 처음엔 식에 적힌 수들의 합, 이름 칸의 값을 알게 될 때마다 더한다const refs: string[][] = []; // refs[i] = i번 칸 식에 적힌 이름들(같은 이름이 두 번 나오면 두 번)for (let i = 1; i <= n; i++) {  const w: string[] = lines[i].trim().split(" "); // [이름, "=", 항, "+", 항, ...]  names.push(w[0]);  let c = 0;  const r: string[] = [];  for (let k = 2; k < w.length; k += 2) {    if (/^[0-9]/.test(w[k])) c += Number(w[k]);    else r.push(w[k]);  }  value.push(c);  refs.push(r);} const index = new Map<string, number>(names.map((name, i): [string, number] => [name, i]));const users: number[][] = Array.from({ length: n }, (): number[] => []); // users[j] = j번 칸을 식에 쓰는 칸들(쓴 횟수만큼)const waiting: number[] = new Array<number>(n).fill(0); // 아직 값을 모르는, 식에 쓴 이름의 수refs.forEach((r, i) => {  for (const name of r) {    users[index.get(name)!].push(i); // 식에 쓴 이름은 모두 어느 줄에선가 정해진다    waiting[i]++;  }}); // 위상 정렬(Kahn 알고리즘): 기다릴 것이 없는 칸부터 값을 정하고, 그 칸을 쓰는 칸의 기다림을 하나씩 줄인다const known: boolean[] = new Array<boolean>(n).fill(false);const queue: number[] = [];for (let i = 0; i < n; i++) if (waiting[i] === 0) queue.push(i);for (let head = 0; head < queue.length; head++) {  const j = queue[head];  known[j] = true;  for (const i of users[j]) {    value[i] += value[j]; // 10^15까지라 number로도 정확하다    if (--waiting[i] === 0) queue.push(i);  }} // 끝까지 기다림이 남은 칸은 고리에 들었거나 고리에 든 칸을 (거쳐서라도) 쓴다console.log(names.map((name, i) => `${name} ${known[i] ? value[i] : "ERR"}`).join("\n"));
JavaMain.java · 63 行
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.ArrayDeque;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map; public class Main {    public static void main(String[] args) throws IOException {        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));        int n = Integer.parseInt(in.readLine().trim());        String[] names = new String[n];        long[] value = new long[n]; // 처음엔 식에 적힌 수들의 합. 10^15까지라 long        List<List<String>> refs = new ArrayList<>(); // i번 칸 식에 적힌 이름들(같은 이름이 두 번 나오면 두 번)        Map<String, Integer> index = new HashMap<>();        for (int i = 0; i < n; i++) {            String[] w = in.readLine().trim().split(" "); // [이름, "=", 항, "+", 항, ...]            names[i] = w[0];            index.put(w[0], i);            List<String> r = new ArrayList<>();            for (int k = 2; k < w.length; k += 2) {                if (Character.isDigit(w[k].charAt(0))) value[i] += Long.parseLong(w[k]);                else r.add(w[k]);            }            refs.add(r);        }         List<List<Integer>> users = new ArrayList<>(); // users.get(j) = j번 칸을 식에 쓰는 칸들(쓴 횟수만큼)        for (int i = 0; i < n; i++) users.add(new ArrayList<>());        int[] waiting = new int[n]; // 아직 값을 모르는, 식에 쓴 이름의 수        for (int i = 0; i < n; i++) {            for (String name : refs.get(i)) {                users.get(index.get(name)).add(i);                waiting[i]++;            }        }         // 위상 정렬(Kahn 알고리즘): 기다릴 것이 없는 칸부터 값을 정하고, 그 칸을 쓰는 칸의 기다림을 하나씩 줄인다        boolean[] known = new boolean[n];        ArrayDeque<Integer> queue = new ArrayDeque<>();        for (int i = 0; i < n; i++) if (waiting[i] == 0) queue.add(i);        while (!queue.isEmpty()) {            int j = queue.poll();            known[j] = true;            for (int i : users.get(j)) {                value[i] += value[j];                if (--waiting[i] == 0) queue.add(i);            }        }         // 끝까지 기다림이 남은 칸은 고리에 들었거나 고리에 든 칸을 (거쳐서라도) 쓴다        StringBuilder out = new StringBuilder();        for (int i = 0; i < n; i++) {            out.append(names[i]).append(' ');            if (known[i]) out.append(value[i]);            else out.append("ERR");            out.append('\n');        }        System.out.print(out);    }}
C#Main.cs · 65 行
using System;using System.Collections.Generic;using System.Text; class Program{    static void Main()    {        int n = int.Parse(Console.ReadLine().Trim());        var names = new string[n];        var value = new long[n]; // 처음엔 식에 적힌 수들의 합. 10^15까지라 long        var refs = new List<string>[n]; // i번 칸 식에 적힌 이름들(같은 이름이 두 번 나오면 두 번)        var index = new Dictionary<string, int>();        for (int i = 0; i < n; i++)        {            string[] w = Console.ReadLine().Trim().Split(' '); // [이름, "=", 항, "+", 항, ...]            names[i] = w[0];            index[w[0]] = i;            refs[i] = new List<string>();            for (int k = 2; k < w.Length; k += 2)            {                if (char.IsDigit(w[k][0])) value[i] += long.Parse(w[k]);                else refs[i].Add(w[k]);            }        }         var users = new List<int>[n]; // users[j] = j번 칸을 식에 쓰는 칸들(쓴 횟수만큼)        for (int i = 0; i < n; i++) users[i] = new List<int>();        var waiting = new int[n]; // 아직 값을 모르는, 식에 쓴 이름의 수        for (int i = 0; i < n; i++)        {            foreach (string name in refs[i])            {                users[index[name]].Add(i);                waiting[i]++;            }        }         // 위상 정렬(Kahn 알고리즘): 기다릴 것이 없는 칸부터 값을 정하고, 그 칸을 쓰는 칸의 기다림을 하나씩 줄인다        var known = new bool[n];        var queue = new Queue<int>();        for (int i = 0; i < n; i++) if (waiting[i] == 0) queue.Enqueue(i);        while (queue.Count > 0)        {            int j = queue.Dequeue();            known[j] = true;            foreach (int i in users[j])            {                value[i] += value[j];                if (--waiting[i] == 0) queue.Enqueue(i);            }        }         // 끝까지 기다림이 남은 칸은 고리에 들었거나 고리에 든 칸을 (거쳐서라도) 쓴다        var sb = new StringBuilder();        for (int i = 0; i < n; i++)        {            sb.Append(names[i]).Append(' ');            if (known[i]) sb.Append(value[i]);            else sb.Append("ERR");            sb.Append('\n');        }        Console.Write(sb.ToString());    }}
C++main.cpp · 61 行
#include <cctype>#include <iostream>#include <queue>#include <sstream>#include <string>#include <unordered_map>#include <vector> int main() {    std::ios::sync_with_stdio(false);    std::cin.tie(nullptr);    std::string line;    std::getline(std::cin, line);    int n = std::stoi(line);    std::vector<std::string> names(n);    std::vector<long long> value(n, 0);  // 처음엔 식에 적힌 수들의 합. 10^15까지라 long long    std::vector<std::vector<std::string>> refs(n);  // i번 칸 식에 적힌 이름들(같은 이름이 두 번 나오면 두 번)    std::unordered_map<std::string, int> index;    for (int i = 0; i < n; i++) {        std::getline(std::cin, line);        std::istringstream words(line);        std::string term, sign;        words >> names[i] >> sign;  // 이름, "="        index[names[i]] = i;        while (words >> term) {            if (std::isdigit(static_cast<unsigned char>(term[0]))) value[i] += std::stoll(term);            else refs[i].push_back(term);            words >> sign;  // "+" (줄 끝이면 읽지 못하고 끝난다)        }    }     std::vector<std::vector<int>> users(n);  // users[j] = j번 칸을 식에 쓰는 칸들(쓴 횟수만큼)    std::vector<int> waiting(n, 0);          // 아직 값을 모르는, 식에 쓴 이름의 수    for (int i = 0; i < n; i++) {        for (const auto& name : refs[i]) {            users[index[name]].push_back(i);            waiting[i]++;        }    }     // 위상 정렬(Kahn 알고리즘): 기다릴 것이 없는 칸부터 값을 정하고, 그 칸을 쓰는 칸의 기다림을 하나씩 줄인다    std::vector<bool> known(n, false);    std::queue<int> queue;    for (int i = 0; i < n; i++)        if (waiting[i] == 0) queue.push(i);    while (!queue.empty()) {        int j = queue.front();        queue.pop();        known[j] = true;        for (int i : users[j]) {            value[i] += value[j];            if (--waiting[i] == 0) queue.push(i);        }    }     // 끝까지 기다림이 남은 칸은 고리에 들었거나 고리에 든 칸을 (거쳐서라도) 쓴다    std::string out;    for (int i = 0; i < n; i++) out += names[i] + " " + (known[i] ? std::to_string(value[i]) : "ERR") + "\n";    std::cout << out;    return 0;}
Cmain.c · 90 行
#include <ctype.h>#include <stdio.h>#include <stdlib.h>#include <string.h> #define MAXN 5000#define MAXREF 10000 static char names[MAXN][9];static long long value[MAXN]; /* 처음엔 식에 적힌 수들의 합. 10^15까지라 long long */static char refName[MAXREF][9]; /* 식에 적힌 이름들(같은 이름이 두 번 나오면 두 번) */static int refOwner[MAXREF];    /* 그 이름을 쓴 칸 */static int nref; static int sorted[MAXN]; /* 칸 번호를 이름 차례로 늘어놓은 것: 이름 → 번호를 이진 탐색으로 찾는다 */static int byName(const void* a, const void* b) {    return strcmp(names[*(const int*)a], names[*(const int*)b]);}static int findCell(const char* name, int n) {    int lo = 0, hi = n - 1;    while (lo <= hi) {        int mid = (lo + hi) / 2;        int c = strcmp(names[sorted[mid]], name);        if (c == 0) return sorted[mid];        if (c < 0) lo = mid + 1;        else hi = mid - 1;    }    return -1;} /* 같은 칸을 쓰는 칸들을 배열로 엮는다: head[j]에서 시작해 next[e]로 이어지고, user[e]가 쓰는 칸 */static int head[MAXN], next[MAXREF], user[MAXREF];static int waiting[MAXN]; /* 아직 값을 모르는, 식에 쓴 이름의 수 */static int known[MAXN];static int queue[MAXN]; int main(void) {    char line[256];    if (!fgets(line, sizeof line, stdin)) return 0;    int n = atoi(line);    for (int i = 0; i < n; i++) {        fgets(line, sizeof line, stdin);        char* word = strtok(line, " \r\n"); /* 이름 */        strcpy(names[i], word);        strtok(NULL, " \r\n"); /* "=" */        while ((word = strtok(NULL, " \r\n")) != NULL) {            if (strcmp(word, "+") == 0) continue;            if (isdigit((unsigned char)word[0])) {                value[i] += atoll(word);            } else {                strcpy(refName[nref], word);                refOwner[nref++] = i;            }        }    }     for (int i = 0; i < n; i++) {        sorted[i] = i;        head[i] = -1;    }    qsort(sorted, n, sizeof sorted[0], byName);    for (int e = 0; e < nref; e++) {        int j = findCell(refName[e], n);        user[e] = refOwner[e];        next[e] = head[j];        head[j] = e;        waiting[refOwner[e]]++;    }     /* 위상 정렬(Kahn 알고리즘): 기다릴 것이 없는 칸부터 값을 정하고, 그 칸을 쓰는 칸의 기다림을 하나씩 줄인다 */    int qhead = 0, qtail = 0;    for (int i = 0; i < n; i++)        if (waiting[i] == 0) queue[qtail++] = i;    while (qhead < qtail) {        int j = queue[qhead++];        known[j] = 1;        for (int e = head[j]; e != -1; e = next[e]) {            int i = user[e];            value[i] += value[j];            if (--waiting[i] == 0) queue[qtail++] = i;        }    }     /* 끝까지 기다림이 남은 칸은 고리에 들었거나 고리에 든 칸을 (거쳐서라도) 쓴다 */    for (int i = 0; i < n; i++) {        if (known[i]) printf("%s %lld\n", names[i], value[i]);        else printf("%s ERR\n", names[i]);    }    return 0;}
Craykonmain.cray · 80 行
n = コンソール.行読み().空白除去().数に();names = [];value = []; // 처음엔 식에 적힌 수들의 합, 이름 칸의 값을 알게 될 때마다 더한다refs = []; // refs[i] = i번 칸 식에 적힌 이름들(같은 이름이 두 번 나오면 두 번)index = 辞書(); // 이름 → 칸 번호繰り返す (i = 0; i < n; i++){    w = コンソール.行読み().空白除去().分ける(" "); // [이름, "=", 항, "+", 항, ...]    names.追加(w[0]);    index[w[0]] = i;    c = 0;    r = [];    繰り返す (k = 2; k < w.個数; k += 2)    {        첫글자 = 文字.コード(w[k][0]);        もし (첫글자 >= 48 && 첫글자 <= 57)        {            c += w[k].数に();        }        でなければ        {            r.追加(w[k]);        }    }    value.追加(c);    refs.追加(r);} users = []; // users[j] = j번 칸을 식에 쓰는 칸들(쓴 횟수만큼)繰り返す (i = 0; i < n; i++){    users.追加([]);}waiting = 新リスト(n, 0); // 아직 값을 모르는, 식에 쓴 이름의 수繰り返す (i = 0; i < n; i++){    それぞれ (name の中 refs[i])    {        users[index[name]].追加(i);        waiting[i] += 1;    }} // 위상 정렬(Kahn 알고리즘): 기다릴 것이 없는 칸부터 값을 정하고, 그 칸을 쓰는 칸의 기다림을 하나씩 줄인다known = 新リスト(n, 偽);queue = [];繰り返す (i = 0; i < n; i++){    もし (waiting[i] == 0)    {        queue.追加(i);    }}繰り返す (head = 0; head < queue.個数; head++){    j = queue[head];    known[j] = 真;    それぞれ (i の中 users[j])    {        value[i] += value[j];        waiting[i] -= 1;        もし (waiting[i] == 0)        {            queue.追加(i);        }    }} // 끝까지 기다림이 남은 칸은 고리에 들었거나 고리에 든 칸을 (거쳐서라도) 쓴다繰り返す (i = 0; i < n; i++){    もし (known[i])    {        コンソール.書く(names[i] + " " + value[i]);    }    でなければ    {        コンソール.書く(names[i] + " ERR");    }}