{ }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);