{ }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 种语言题解

若单元格 i 的公式用到单元格 j,就画一条箭头 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 在几千层时也会停止。
用队列的 Kahn 方法没有递归,就没有这个问题。
陷阱四:值最多到 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");    }}