{ }codelper
再帰スコープP0183

入れ子のブロックで名前を探す

小さな言語で書いたプログラムが与えられる。
プログラムは空白で区切られたトークンの並びで、トークンは四種類だけである:ブロックを開く {、ブロックを閉じる }、代入 名前=値、質問 ?名前。
プログラム全体も一つのブロック(いちばん外側のブロック)で、{ と } はいつも対応している。
トークンは前から一度ずつ順に実行され、内側のブロックはその { の位置で実行される。
あるブロックの中に(その中の内側のブロックは除く)名前=値 が一つでもあれば、その名前はそのブロック全体のものになる。
代入が質問より後ろにあっても同じである。
質問 ?名前 は、その質問をすぐ囲むブロックから外側へ向かって、その名前を持つブロックに初めて出会うまで探す。
そのようなブロックがなければ "未定義" を出力する。
見つけたブロックがその名前にまだ一度も値を入れていなければ "未代入" を出力する。
そうでなければ、その質問より前にそのブロックがその名前に最後に入れた値を出力する。
ブロックが終わるとそのブロックの名前と値は消え、ほかのブロックには影響しない。
質問ごとに一行ずつ順に出力せよ。

入力
複数行にわたり、トークンが空白一つで区切られて与えられる(行末もトークンの終わり)。
トークンは 10,000 個以下、ブロックの入れ子の深さは外側のブロックを除いて 1,000 以下、質問は一つ以上。
名前は 1〜10 文字の英小文字、値は 0 以上 1,000,000,000 以下の整数(先頭に 0 を付けない)。
出力
質問ごとに一行:値、"未定義"、"未代入" のいずれか
例 1 · stdin
x=1 ?x {
?x x=2 ?x ?y
{ ?x }
} ?x ?z
stdout
1
未代入
2
未定義
2
1
未定義
main.py
入力(stdin)

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

$ 準備完了

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

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

この規則は Python の関数と同じです:関数の中のどこかで代入する名前は関数全体のローカル変数なので、代入より前に読むと外側の値ではなく UnboundLocalError になります(JavaScript の let も、同じブロックの let の行より前に読むと外側の値ではなく ReferenceError になります)。
解法はブロックごとに再帰関数を一回呼び、まずそのブロックが持つ名前ごとに「未代入」の枠を名前別の山のいちばん上に積み、順に実行し、終わったらその枠を取り除きます。
すると質問はその名前の山のいちばん上だけを見ればよくなります。
ここで解法コード自体の変数のスコープが大事です。
呼び出しごとに別でなければならないもの(ブロック番号、そのブロックが持つ名前のリスト、走査位置)はローカル変数に、すべての呼び出しが共有するもの(ブロックの数え上げ、出力)は外に置きます。
Python は関数の中で外の名前に代入するには global block_count が必要で、ないとその名前がローカル変数になって UnboundLocalError が出ます。
JavaScript・TypeScript は外の let をクロージャでそのまま書き換え、Java・C# は static フィールド、C・C++ はグローバル変数に置きます。
Craykon は本体のいちばん外側(関数の外、ブロックの外)で作った変数は関数の中でも同じ一つなので、関数の中で使う k を本体でも使うと、再帰呼び出しが外側の呼び出しの k を書き換えてしまいます — 関数の中だけで使う名前を選びます。
またブロックが 1,000 重なので再帰も 1,000 段を超えます。
Python の既定の上限は 1,000 なので sys.setrecursionlimit で上げる必要があり、Craykon は数千段まで大丈夫です。
罠は ① global の書き忘れ、② 再帰の上限、③ 代入した瞬間に名前を作る(すると代入前の質問が "unbound" ではなく外側の値を出します)、④ Craykon で本体と関数が同じ名前を使う、です。

Pythonmain.py · 65 行
import sys sys.setrecursionlimit(10_000)  # 블록이 1,000겹까지 겹친다. Python 기본 한도는 1,000 tokens = sys.stdin.read().split() # 짝 괄호 자리: match[i] = i번 '{'와 짝인 '}'의 자리match = [0] * len(tokens)opened = []for i, tok in enumerate(tokens):    if tok == "{":        opened.append(i)    elif tok == "}":        match[opened.pop()] = i # 이름마다 칸 더미: 맨 위 칸이 지금 보이는 블록의 것. 칸은 [블록 번호, 값], 값 -1은 아직 넣지 않음cells = {}block_count = 0out = []  def run(lo, hi):    """lo..hi-1 칸이 한 블록이다. 이 블록이 가진 이름을 먼저 모두 세운 뒤 차례로 실행한다."""    global block_count    block_count += 1    me = block_count    owned = []    i = lo    while i < hi:  # 1단계: 블록 안 어디에서든 넣는 이름은 블록 전체의 것        tok = tokens[i]        if tok == "{":            i = match[i] + 1            continue        if "=" in tok:            name = tok[: tok.index("=")]            stack = cells.setdefault(name, [])            if not stack or stack[-1][0] != me:                stack.append([me, -1])                owned.append(name)        i += 1    i = lo    while i < hi:  # 2단계: 차례로 실행        tok = tokens[i]        if tok == "{":            run(i + 1, match[i])            i = match[i] + 1            continue        if tok[0] == "?":            stack = cells.get(tok[1:])            if not stack:                out.append("未定義")            elif stack[-1][1] == -1:                out.append("未代入")            else:                out.append(str(stack[-1][1]))        else:            name, value = tok.split("=")            cells[name][-1][1] = int(value)  # 맨 위 칸은 이 블록의 것        i += 1    for name in owned:  # 블록을 나가면 이 블록의 칸을 걷는다        cells[name].pop()  run(0, len(tokens))print("\n".join(out))
JavaScriptmain.js · 58 行
const tokens = require("fs").readFileSync(0, "utf8").split(/\s+/).filter((t) => t !== ""); // 짝 괄호 자리: match[i] = i번 '{'와 짝인 '}'의 자리const match = new Array(tokens.length).fill(0);const opened = [];tokens.forEach((tok, i) => {  if (tok === "{") opened.push(i);  else if (tok === "}") match[opened.pop()] = i;}); // 이름마다 칸 더미: 맨 위 칸이 지금 보이는 블록의 것. 칸은 { block, value }, value -1은 아직 넣지 않음const cells = new Map();let blockCount = 0; // 함수 밖에서 만든 let — 안에서 고치면 이 하나가 바뀐다const out = []; // lo..hi-1 칸이 한 블록이다. 이 블록이 가진 이름을 먼저 모두 세운 뒤 차례로 실행한다function run(lo, hi) {  const me = ++blockCount;  const owned = [];  for (let i = lo; i < hi; i++) {    // 1단계: 블록 안 어디에서든 넣는 이름은 블록 전체의 것    const tok = tokens[i];    if (tok === "{") {      i = match[i];      continue;    }    const eq = tok.indexOf("=");    if (eq < 0) continue;    const name = tok.slice(0, eq);    if (!cells.has(name)) cells.set(name, []);    const stack = cells.get(name);    if (stack.length === 0 || stack[stack.length - 1].block !== me) {      stack.push({ block: me, value: -1 });      owned.push(name);    }  }  for (let i = lo; i < hi; i++) {    // 2단계: 차례로 실행    const tok = tokens[i];    if (tok === "{") {      run(i + 1, match[i]);      i = match[i];    } else if (tok[0] === "?") {      const stack = cells.get(tok.slice(1));      if (!stack || stack.length === 0) out.push("未定義");      else if (stack[stack.length - 1].value === -1) out.push("未代入");      else out.push(String(stack[stack.length - 1].value));    } else {      const [name, value] = tok.split("=");      const stack = cells.get(name);      stack[stack.length - 1].value = Number(value); // 맨 위 칸은 이 블록의 것    }  }  for (const name of owned) cells.get(name).pop(); // 블록을 나가면 이 블록의 칸을 걷는다} run(0, tokens.length);console.log(out.join("\n"));
TypeScriptmain.ts · 66 行
import { readFileSync } from "node:fs";const tokens: string[] = readFileSync(0, "utf8").split(/\s+/).filter((t) => t !== ""); // 짝 괄호 자리: match[i] = i번 '{'와 짝인 '}'의 자리const match: number[] = new Array<number>(tokens.length).fill(0);const opened: number[] = [];tokens.forEach((tok, i) => {  if (tok === "{") opened.push(i);  else if (tok === "}") match[opened.pop()!] = i;}); // 이름마다 칸 더미: 맨 위 칸이 지금 보이는 블록의 것. value -1은 아직 넣지 않음interface Cell {  block: number;  value: number;}const cells = new Map<string, Cell[]>();let blockCount = 0; // 함수 밖에서 만든 let — 안에서 고치면 이 하나가 바뀐다const out: string[] = []; // lo..hi-1 칸이 한 블록이다. 이 블록이 가진 이름을 먼저 모두 세운 뒤 차례로 실행한다function run(lo: number, hi: number): void {  const me = ++blockCount;  const owned: string[] = [];  for (let i = lo; i < hi; i++) {    // 1단계: 블록 안 어디에서든 넣는 이름은 블록 전체의 것    const tok = tokens[i];    if (tok === "{") {      i = match[i];      continue;    }    const eq = tok.indexOf("=");    if (eq < 0) continue;    const name = tok.slice(0, eq);    let stack = cells.get(name);    if (stack === undefined) {      stack = [];      cells.set(name, stack);    }    if (stack.length === 0 || stack[stack.length - 1].block !== me) {      stack.push({ block: me, value: -1 });      owned.push(name);    }  }  for (let i = lo; i < hi; i++) {    // 2단계: 차례로 실행    const tok = tokens[i];    if (tok === "{") {      run(i + 1, match[i]);      i = match[i];    } else if (tok[0] === "?") {      const stack = cells.get(tok.slice(1));      if (stack === undefined || stack.length === 0) out.push("未定義");      else if (stack[stack.length - 1].value === -1) out.push("未代入");      else out.push(String(stack[stack.length - 1].value));    } else {      const [name, value] = tok.split("=");      const stack = cells.get(name)!;      stack[stack.length - 1].value = Number(value); // 맨 위 칸은 이 블록의 것    }  }  for (const name of owned) cells.get(name)!.pop(); // 블록을 나가면 이 블록의 칸을 걷는다} run(0, tokens.length);console.log(out.join("\n"));
JavaMain.java · 70 行
import java.io.*;import java.util.*; public class Main {    // 이름마다 칸 더미의 한 칸. value -1은 아직 넣지 않음    static final class Cell {        final int block;        long value = -1;         Cell(int block) {            this.block = block;        }    }     static String[] tokens;    static int[] match;    static final Map<String, ArrayDeque<Cell>> cells = new HashMap<>();    static int blockCount = 0; // static 필드: 모든 호출이 같은 하나를 본다    static final StringBuilder out = new StringBuilder();     // lo..hi-1 칸이 한 블록이다. 이 블록이 가진 이름을 먼저 모두 세운 뒤 차례로 실행한다    static void run(int lo, int hi) {        int me = ++blockCount;        List<String> owned = new ArrayList<>(); // 지역 변수: 호출마다 따로        for (int i = lo; i < hi; i++) { // 1단계: 블록 안 어디에서든 넣는 이름은 블록 전체의 것            String tok = tokens[i];            if (tok.equals("{")) {                i = match[i];                continue;            }            int eq = tok.indexOf('=');            if (eq < 0) continue;            String name = tok.substring(0, eq);            ArrayDeque<Cell> stack = cells.computeIfAbsent(name, k -> new ArrayDeque<>());            if (stack.isEmpty() || stack.peek().block != me) {                stack.push(new Cell(me));                owned.add(name);            }        }        for (int i = lo; i < hi; i++) { // 2단계: 차례로 실행            String tok = tokens[i];            if (tok.equals("{")) {                run(i + 1, match[i]);                i = match[i];            } else if (tok.charAt(0) == '?') {                ArrayDeque<Cell> stack = cells.get(tok.substring(1));                if (stack == null || stack.isEmpty()) out.append("未定義\n");                else if (stack.peek().value == -1) out.append("未代入\n");                else out.append(stack.peek().value).append('\n');            } else {                int eq = tok.indexOf('=');                cells.get(tok.substring(0, eq)).peek().value = Long.parseLong(tok.substring(eq + 1));            }        }        for (String name : owned) cells.get(name).pop(); // 블록을 나가면 이 블록의 칸을 걷는다    }     public static void main(String[] args) throws IOException {        String all = new String(System.in.readAllBytes()).trim();        tokens = all.split("\\s+");        match = new int[tokens.length];        ArrayDeque<Integer> opened = new ArrayDeque<>();        for (int i = 0; i < tokens.length; i++) {            if (tokens[i].equals("{")) opened.push(i);            else if (tokens[i].equals("}")) match[opened.pop()] = i;        }        run(0, tokens.length);        System.out.print(out);    }}
C#Main.cs · 83 行
using System;using System.Collections.Generic;using System.Text; class Program{    // 이름마다 칸 더미의 한 칸. Value -1은 아직 넣지 않음    class Cell    {        public int Block;        public long Value = -1;    }     static string[] tokens;    static int[] match;    static readonly Dictionary<string, Stack<Cell>> cells = new Dictionary<string, Stack<Cell>>();    static int blockCount = 0; // static 필드: 모든 호출이 같은 하나를 본다    static readonly StringBuilder output = new StringBuilder();     // lo..hi-1 칸이 한 블록이다. 이 블록이 가진 이름을 먼저 모두 세운 뒤 차례로 실행한다    static void Run(int lo, int hi)    {        int me = ++blockCount;        var owned = new List<string>(); // 지역 변수: 호출마다 따로        for (int i = lo; i < hi; i++) // 1단계: 블록 안 어디에서든 넣는 이름은 블록 전체의 것        {            string tok = tokens[i];            if (tok == "{")            {                i = match[i];                continue;            }            int eq = tok.IndexOf('=');            if (eq < 0) continue;            string name = tok.Substring(0, eq);            if (!cells.TryGetValue(name, out var stack))            {                stack = new Stack<Cell>();                cells[name] = stack;            }            if (stack.Count == 0 || stack.Peek().Block != me)            {                stack.Push(new Cell { Block = me });                owned.Add(name);            }        }        for (int i = lo; i < hi; i++) // 2단계: 차례로 실행        {            string tok = tokens[i];            if (tok == "{")            {                Run(i + 1, match[i]);                i = match[i];            }            else if (tok[0] == '?')            {                if (!cells.TryGetValue(tok.Substring(1), out var stack) || stack.Count == 0) output.Append("未定義\n");                else if (stack.Peek().Value == -1) output.Append("未代入\n");                else output.Append(stack.Peek().Value).Append('\n');            }            else            {                int eq = tok.IndexOf('=');                cells[tok.Substring(0, eq)].Peek().Value = long.Parse(tok.Substring(eq + 1));            }        }        foreach (string name in owned) cells[name].Pop(); // 블록을 나가면 이 블록의 칸을 걷는다    }     static void Main()    {        tokens = Console.In.ReadToEnd().Split((char[])null, StringSplitOptions.RemoveEmptyEntries);        match = new int[tokens.Length];        var opened = new Stack<int>();        for (int i = 0; i < tokens.Length; i++)        {            if (tokens[i] == "{") opened.Push(i);            else if (tokens[i] == "}") match[opened.Pop()] = i;        }        Run(0, tokens.Length);        Console.Write(output);    }}
C++main.cpp · 70 行
#include <iostream>#include <string>#include <unordered_map>#include <vector> // 이름마다 칸 더미의 한 칸. value -1은 아직 넣지 않음struct Cell {    int block;    long long value;}; std::vector<std::string> tokens;std::vector<int> match;std::unordered_map<std::string, std::vector<Cell>> cells;int blockCount = 0;  // 전역 변수: 모든 호출이 같은 하나를 본다std::string out; // lo..hi-1 칸이 한 블록이다. 이 블록이 가진 이름을 먼저 모두 세운 뒤 차례로 실행한다void run(int lo, int hi) {    int me = ++blockCount;    std::vector<std::string> owned;  // 지역 변수: 호출마다 따로    for (int i = lo; i < hi; i++) {  // 1단계: 블록 안 어디에서든 넣는 이름은 블록 전체의 것        const std::string& tok = tokens[i];        if (tok == "{") {            i = match[i];            continue;        }        size_t eq = tok.find('=');        if (eq == std::string::npos) continue;        std::string name = tok.substr(0, eq);        auto& stack = cells[name];        if (stack.empty() || stack.back().block != me) {            stack.push_back({me, -1});            owned.push_back(name);        }    }    for (int i = lo; i < hi; i++) {  // 2단계: 차례로 실행        const std::string& tok = tokens[i];        if (tok == "{") {            run(i + 1, match[i]);            i = match[i];        } else if (tok[0] == '?') {            auto it = cells.find(tok.substr(1));            if (it == cells.end() || it->second.empty()) out += "未定義\n";            else if (it->second.back().value == -1) out += "未代入\n";            else out += std::to_string(it->second.back().value) + "\n";        } else {            size_t eq = tok.find('=');            cells[tok.substr(0, eq)].back().value = std::stoll(tok.substr(eq + 1));        }    }    for (const auto& name : owned) cells[name].pop_back();  // 블록을 나가면 이 블록의 칸을 걷는다} int main() {    std::ios::sync_with_stdio(false);    std::string tok;    while (std::cin >> tok) tokens.push_back(tok);    match.assign(tokens.size(), 0);    std::vector<int> opened;    for (int i = 0; i < (int)tokens.size(); i++) {        if (tokens[i] == "{") opened.push_back(i);        else if (tokens[i] == "}") {            match[opened.back()] = i;            opened.pop_back();        }    }    run(0, (int)tokens.size());    std::cout << out;}
Cmain.c · 100 行
#include <stdio.h>#include <string.h> #define MAXT 10000#define HASH 32768 /* 토큰을 미리 풀어 둔다: kind 0 '{', 1 '}', 2 넣기, 3 질문 */static int kind[MAXT], nid[MAXT], match[MAXT], ntok = 0;static long long val[MAXT]; /* 이름 → 번호(열린 주소 해시) */static char names[MAXT][12];static int nnames = 0, table[HASH]; int name_id(const char *s, int len) {    unsigned h = 0;    for (int i = 0; i < len; i++) h = h * 31 + (unsigned char)s[i];    for (h %= HASH;; h = (h + 1) % HASH) {        if (table[h] == -1) {            memcpy(names[nnames], s, len);            names[nnames][len] = '\0';            table[h] = nnames;            return nnames++;        }        const char *t = names[table[h]];        if ((int)strlen(t) == len && memcmp(t, s, len) == 0) return table[h];    }} /* 이름마다 칸 더미: top[이름]이 맨 위 칸, 칸마다 블록 번호·값(-1은 아직 넣지 않음)·아래 칸 */static int top[MAXT], cell_block[MAXT], cell_prev[MAXT], ncells = 0;static long long cell_value[MAXT];/* 블록마다 가진 이름: 호출마다 base부터 쌓고 나갈 때 base로 되돌린다 */static int owned[MAXT], nowned = 0;static int block_count = 0; /* 전역 변수: 모든 호출이 같은 하나를 본다 */ /* lo..hi-1 칸이 한 블록이다. 이 블록이 가진 이름을 먼저 모두 세운 뒤 차례로 실행한다 */void run(int lo, int hi) {    int me = ++block_count;    int base = nowned; /* 지역 변수: 호출마다 따로 */    for (int i = lo; i < hi; i++) { /* 1단계: 블록 안 어디에서든 넣는 이름은 블록 전체의 것 */        if (kind[i] == 0) {            i = match[i];            continue;        }        if (kind[i] != 2) continue;        int id = nid[i];        if (top[id] == -1 || cell_block[top[id]] != me) {            cell_block[ncells] = me;            cell_value[ncells] = -1;            cell_prev[ncells] = top[id];            top[id] = ncells++;            owned[nowned++] = id;        }    }    for (int i = lo; i < hi; i++) { /* 2단계: 차례로 실행 */        if (kind[i] == 0) {            run(i + 1, match[i]);            i = match[i];        } else if (kind[i] == 3) {            int c = top[nid[i]];            if (c == -1) puts("未定義");            else if (cell_value[c] == -1) puts("未代入");            else printf("%lld\n", cell_value[c]);        } else if (kind[i] == 2) {            cell_value[top[nid[i]]] = val[i]; /* 맨 위 칸은 이 블록의 것 */        }    }    while (nowned > base) { /* 블록을 나가면 이 블록의 칸을 걷는다 */        int id = owned[--nowned];        top[id] = cell_prev[top[id]];    }} int main(void) {    memset(table, -1, sizeof table);    memset(top, -1, sizeof top);    static char buf[32];    int opened[MAXT], nopen = 0;    while (scanf("%31s", buf) == 1) {        int i = ntok++;        if (buf[0] == '{') {            kind[i] = 0;            opened[nopen++] = i;        } else if (buf[0] == '}') {            kind[i] = 1;            match[opened[--nopen]] = i;        } else if (buf[0] == '?') {            kind[i] = 3;            nid[i] = name_id(buf + 1, (int)strlen(buf + 1));        } else {            char *eq = strchr(buf, '=');            kind[i] = 2;            nid[i] = name_id(buf, (int)(eq - buf));            sscanf(eq + 1, "%lld", &val[i]);        }    }    run(0, ntok);    return 0;}
Craykonmain.cray · 116 行
// 이름마다 칸 더미: 맨 위 칸이 지금 보이는 블록의 것. 칸은 [블록 번호, 값], 값 -1은 아직 넣지 않음// 함수 안에서 쓰는 이름(lo, hi, blockId, owned, k, tok …)은 아래 본문에서 쓰지 않는다:// 본문 맨 바깥에서 만든 이름은 모든 호출이 함께 쓰는 하나가 되기 때문이다실행(lo, hi){    blockCount += 1;    blockId = blockCount;    owned = [];    k = lo;    間 (k < hi)    {        tok = tokens[k];        もし (tok == "{")        {            k = match[k] + 1;        }        でなければ        {            eq = tok.位置検索("=");            もし (eq >= 0)            {                name = tok.切り出す(0, eq);                もし (!cells.含む(name))                {                    cells[name] = [];                }                stack = cells[name];                もし (stack.個数 == 0 || stack[stack.個数 - 1][0] != blockId)                {                    stack.追加([blockId, -1]);                    owned.追加(name);                }            }            k += 1;        }    }    k = lo;    間 (k < hi)    {        tok = tokens[k];        もし (tok == "{")        {            실행(k + 1, match[k]);            k = match[k];        }        でなければ もし (tok[0] == "?")        {            name = tok.切り出す(1);            もし (!cells.含む(name) || cells[name].個数 == 0)            {                out += "未定義\n";            }            でなければ            {                stack = cells[name];                cell = stack[stack.個数 - 1];                もし (cell[1] == -1)                {                    out += "未代入\n";                }                でなければ                {                    out += cell[1] + "\n";                }            }        }        でなければ        {            eq = tok.位置検索("=");            stack = cells[tok.切り出す(0, eq)];            stack[stack.個数 - 1][1] = tok.切り出す(eq + 1).数に(); // 맨 위 칸은 이 블록의 것        }        k += 1;    }    それぞれ (name の中 owned)    {        stack = cells[name];        stack.位置で取り除く(stack.個数 - 1);    }} tokens = [];line = コンソール.行読み();間 (line != なし){    それぞれ (t の中 line.分ける(" "))    {        もし (t != "")        {            tokens.追加(t);        }    }    line = コンソール.行読み();} // 짝 괄호 자리: match[i] = i번 '{'와 짝인 '}'의 자리match = 新リスト(tokens.個数, 0);opened = [];繰り返す (i = 0; i < tokens.個数; i++){    もし (tokens[i] == "{")    {        opened.追加(i);    }    でなければ もし (tokens[i] == "}")    {        match[opened[opened.個数 - 1]] = i;        opened.位置で取り除く(opened.個数 - 1);    }} cells = 辞書();blockCount = 0; // 본문에서 만든 이름이라 함수 안에서 바꾸면 이 하나가 바뀐다out = "";실행(0, tokens.個数);コンソール.続けて書く(out);