🪆 B4353 程序套娃

信息与未来 2025 · 元编程题图解 · 让代码生成代码
📌 一句话先讲清
这是道"程序生成程序"的元编程题你的题解程序读入 nk,输出一段字符串——这段字符串会被存为 p_1.cpp。然后 p_1 运行后输出 p_2 的代码;p_2 运行后输出 p_3 的代码;一直到第 np_n 必须正好是数字 k

核心做法两步:
倒着想 — 先确定 p_n(就是字符串 "k"),再往前包一层一层
字符串转义 — 把上一层的代码当作字符串嵌进这一层,遇到 "\ 要转义

一、题目在玩什么?(俄罗斯套娃)

真实的俄罗斯套娃 🪆:打开最外层的木娃娃,里面藏着一个小一号的;再打开,又藏着更小一号的……一直到最里面。

⚠️ 先理清角色关系(很多人第一眼搞错)
你写的题解程序p_1
你写的题解程序读入 n, k,输出一段字符串,这段字符串才会被 Dr. X 保存为 p_1.cpp

完整流程:
[你的题解程序]
   读入 n=3, k=4096
   ↓ 输出字符串(一段 cpp 代码)
[p_1.cpp]  ← Dr. X 把你的输出保存成这个文件
   ↓ 编译运行
[p_2.cpp]
   ↓ 编译运行
[p_3.cpp]  ← 必须 = "4096" = k
也就是说:从你的题解开始,中间会产生 n 个文件 p_1..p_n,而 p_1..p_{n-1} 都需要被编译运行(共 n−1 次),最终 p_n 必须等于 k。

这道题把套娃游戏搬到了代码世界:

👇 下面这堆套娃 = 你题解程序输出的字符串(被存为 p_1.cpp), 然后被反复编译运行 👇
点"编译运行",看一层层套娃被剥开

每一层都是一段会"生孩子"的代码。编译运行 p_i,它的屏幕输出被保存成 p_{i+1}.cpp——就像剥开一层套娃,里面又有一个套娃。直到最里面的 p_n 不是 cpp,而是一个赤裸裸的数字 k

二、关键洞察:程序就是字符串

💡 思维启蒙
一般我们写代码是"让程序处理数据"。但其实——代码本身也是一种数据。它就是一串字符。

既然代码是字符串,那 "输出代码"和"输出字符串"是同一件事。一个能输出代码的程序,本质就是 cout << "..."

搞清这件事,这题就解了一半。

最简单情况:n=1, k=99(就是题目样例 1)

n=1 意味着中间没有任何编译运行步骤,题解程序输出什么,什么就直接是 p_n = p_1 = k。所以题解程序只要把 k 打印出来就行:

// 题解程序 (n=1 时):
#include <iostream>
using namespace std;
int main() {
    int n, k;
    cin >> n >> k;
    cout << k;   // 输出 99
    return 0;
}

这个题解程序的屏幕输出就是 99 这两个字符,被 Dr. X 保存为 p_1.cpp:

[题解程序] cout << 99;
   ↓ 输出
[p_1.cpp]  99    ← 文件内容就是 "99", 即 p_1 = k = 99 ✓
💡 边界要注意
n=1 时 p_1.cpp 这个文件不需要是个能编译的 cpp 程序,题目允许它就是一行整数。所以题解里 cout << k 直接把数字打出去就行,不用包装成 cpp 代码。
这就是为什么后面通用代码里 for (i = n-1; i >= 1; i--) 当 n=1 时循环 0 次,code = to_string(k) 不被包装,刚好就是 "99"。

再深一层:n=2, k=99

现在中间多了一次编译运行。题解程序输出 p_1.cpp,p_1 运行后输出的内容要等于 p_2 = k = 99。所以 p_1 是个"输出 99 的程序":

#include <iostream>
using namespace std;
int main() { cout << "99"; return 0; }

这份代码就是题解程序需要输出的字符串内容(将被存为 p_1.cpp)。

再深一层:n=3, k=4096(题目样例 2)

要求 p_3 = "4096",所以 p_2 是"输出 4096 的程序",所以 p_1 是"输出『输出 4096 的程序』的程序"——把 p_2 当成一个大字符串塞进 cout:

#include <iostream>
using namespace std;
int main() {
    cout << "#include <iostream>\nusing namespace std;\n"
         << "int main() { cout << \"4096\"; return 0; }";
    return 0;
}

"4096" 外面的那对引号:在 p_2 里它就是普通引号,但塞进 p_1 的字符串里,得写成 \"4096\"——这就是转义。再多套一层,转义就要再翻一倍变成 \\\"……越往外越夸张。这就是套娃题的本质难点。

三、解题思路:倒推 + 转义

正着想会绕晕:p_1 长啥样?它的内容要包含 p_2,而 p_2 又要包含 p_3……根本不知道从哪写起。

倒着想立刻清晰:

1
p_n 是已知的 — 题目要求它必须等于 kcode = "4096"
2
p_{n-1} 是"输出 p_n 的程序" — 把 code 转义后塞进 cout。 code = "#include<iostream>...int main(){cout<<\"4096\";return 0;}"
3
p_{n-2} 是"输出 p_{n-1} 的程序" — 把上一步的 code 再转义、再塞进新的 cout 里。 code = "#include<iostream>...cout<<\"#include<iostream>...cout<<\\\"4096\\\";return 0;}\";...}"
4
循环 n−1 次,每次把当前 code 包一层、转义一次,最后得到的 code 就是 p_1。
🧠 算法骨架(三行说完)
string code = to_string(k);              // 起点: p_n
for (int i = n - 1; i >= 1; i--)
    code = "模板头" + escape(code) + "模板尾";  // 包一层
cout << code;                            // 输出 p_1

四、关键工具:字符串转义

把"一段代码"塞进"另一段代码的字符串字面量"里,要做三种转义:

原字符转义后原因
"\"否则会提前结束字符串字面量
\\\反斜杠本身在字符串里是转义符,要双写
真换行\n字符串字面量里不能直接换行

动手玩玩:套娃转义可视化

下面输入框里随便写段代码,点"转义一层"会显示嵌入到上一层字符串里应该长什么样。再点会再转一层——你可以直观看到套得越深,反斜杠越多:

⚠️ 看出规律了吗?
每多套一层," 前面的反斜杠数量翻倍。第 8 层的反斜杠会非常壮观——但只要 escape 函数写对了,程序帮你自动处理,你完全不用手写。

五、完整代码(C++14,信奥常用写法)

20 行不到,简洁清晰:

#include <bits/stdc++.h>
using namespace std;

// 把字符串 s 中的特殊字符转义,使它能作为 cout 字符串字面量的内容
string escape(const string &s) {
    string r;
    for (char c : s) {
        if (c == '\\')      r += "\\\\";   // \  →  \\
        else if (c == '"')  r += "\\\"";   // "  →  \"
        else if (c == '\n') r += "\\n";    // 真换行 → \n
        else r += c;
    }
    return r;
}

int main() {
    int n, k;
    cin >> n >> k;

    // 起点: p_n 就是数字 k 的字符串形式
    string code = to_string(k);

    // 倒推: 每次把 code 包成"输出 code 的程序"
    for (int i = n - 1; i >= 1; i--) {
        code = "#include<iostream>\nusing namespace std;\n"
               "int main(){cout<<\"" + escape(code) + "\";return 0;}";
    }

    // 此时 code 就是 p_1.cpp 的完整内容
    cout << code;   // 输出这段字符串, Dr. X 会把它保存为 p_1.cpp
    return 0;
}

逐句拆解

escape 函数 — 把代码字符串转换成"能塞进另一段字符串字面量"的形式。三种特殊字符各管各的转义。

code = to_string(k); — 关键起点。p_n 就是数字 k,转成字符串。

for (i = n-1; i >= 1; i--) — 循环 n−1 次,因为从 p_n 倒推到 p_1 要包 n−1 层。

④ 模板字符串#include<iostream>...cout<<"..."...,中间挖个洞填 escape(code)

cout << code; — 循环结束后 code 就是 p_1,直接输出即可。

六、本地怎么验证套娃链是对的?

这题没法用样例直接 check,因为输出是个程序,得真的跑 n−1 遍编译运行才能看到最终是不是 k。教学校学生的做法:写个小脚本,自动跑这个链。

办法一:命令行手动套娃(适合小 n)

$ echo "3 4096" | ./solve > p1.cpp        # 题解程序输出 p_1
$ g++ p1.cpp -o p1 && ./p1 > p2.cpp        # 编译运行 p_1, 输出存为 p_2
$ g++ p2.cpp -o p2 && ./p2 > p3.cpp        # 编译运行 p_2, 输出存为 p_3
$ cat p3.cpp
4096                                         # ← 是 k 就对了!

办法二:写个自动套娃脚本(推荐)

把上面的过程写成 shell 脚本,一键验证:

#!/bin/bash
# test.sh - 测试 B4353 套娃链
N=$1; K=$2
echo "$N $K" | ./solve > p1.cpp
for i in $(seq 1 $((N-1))); do
    g++ "p$i.cpp" -o "p$i" 2> /dev/null
    ./"p$i" > "p$((i+1)).cpp"
done
echo "最终输出: $(cat p$N.cpp)"
echo "期望: $K"
[ "$(cat p$N.cpp)" = "$K" ] && echo "✅ AC" || echo "❌ WA"

# 用法: ./test.sh 5 12345

办法三:在 C++ 里模拟(进阶)

更优雅的做法:在题解里加一个本地模拟模式,直接在内存里"运行"每一层,不需要真的编译。但因为每层是真的 cpp 代码,内存模拟不现实——所以这题本地调试必须真编译,跟 B4352 用 fake_judge 不一样。

💡 调试小窍门
如果 p_i 编译报错,八成是转义漏了。比如某个 " 没转义,会导致字符串提前结束、后面的代码乱掉。
这时候 cat p_i.cpp 看一眼那段乱码,通常就能定位到 escape 函数漏处理了哪个字符。

七、复杂度分析:会不会超 100 KB?

设模板长度为常数 T(约 80 字节),模板里有 c 个需要转义的字符(约 4 个: "\)。

L_ip_i 的长度,s_i 是其中需要转义的字符数:

这是个指数递推,s_n = c·(2^n − 1)。但 n ≤ 8,所以 s_1 ≈ 4 × 255 ≈ 1000,L_1 ≈ 8 × 80 + 1000 ≈ 1.6 KB远远小于 100 KB

八、思维升华:这道题在教什么?

🎯 三层认知
第一层:代码就是数据
以前你写代码处理数字、字符串、数组。这题让你处理"代码本身"——代码无非也是一串字符。

第二层:程序可以生成程序
这就是元编程(Meta-programming)的思想。编译器本身就是"读代码、生成代码"的程序;代码生成器、模板引擎、AI 编程助手……都在做这件事。

第三层:倒推往往比正推容易
正着想:"p_1 应该长什么样?"——一片混乱。倒着想:"p_n = k,所以 p_{n-1} 是输出 k 的程序"——立刻清晰。
以后遇到"经过 n 步变成 X"的题,先从终点倒着想一步

📌 这种"代码处理代码"的思想以后还会遇到

编译器原理 — 编译器读 C++ 代码,生成汇编/机器码,本质就是"代码 → 代码"。
Quine(自打印程序) — 一个程序输出自己的源代码,本题的"小弟兄"。
代码生成器 — protobuf、SQL ORM、UI 框架都会自动生成大量样板代码。
AI 写代码 — GPT/Claude 本质上也是"输入文本,输出代码字符串"。
JavaScript 的 eval / Python 的 exec — 把字符串当代码执行。

九、知识点总结

知识点用在这题哪里
字符串拼接+ 把模板和上层代码连起来
字符串转义处理 "\、换行三种特殊字符
循环 / 倒推for (i = n-1; i >= 1; i--) 包 n−1 层
to_string把整数 k 转成字符串作为起点
边界处理n=1 时直接输出 k(代码里 for 循环 0 次自动覆盖)
元编程思想"代码也是数据"的根本认知
🎯 三句话记住
1. 程序就是字符串,"输出程序"=cout << "字符串"
2. 套娃题用倒推:先定终点 p_n = k,再一层层往外包。
3. 每包一层做一次转义:"\"\\\、换行→\n

十、迁移训练

Quine(自打印程序) — 经典挑战:写一个程序,它的输出和自己的源代码完全一样。本题是 Quine 的"远房表亲",理解了这题再去看 Quine,会发现思路相通。
P1097 / 字符串处理类题目 — 任何需要做转义、解析、格式化的题,都是"操作代码/字符串"的练习。
eval / 元编程实战 — 用 Python 写个"输出 Python 代码,运行后输出 Python 代码……"的玩具,体会"代码即数据"。