这些规则和 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);