この規則は 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 syssys.setrecursionlimit(10_000) # 블록이 1,000겹까지 겹친다. Python 기본 한도는 1,000tokens = 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);