📖 第 2.8 章:枚举与模拟
2.8.0 从两道题出发
先看两道典型的 Bronze 题目——它们分别代表了本章的两大主题。
📋 题目 1:熄灯问题(简化版) 有一个 3×3 的灯阵,每个格子要么亮(
1)要么灭(0)。现在灯全部亮着。 按下任意一个格子的开关,会翻转它自己和上下左右相邻格子的亮灭状态。 问:至少按多少次开关,才能把所有灯熄灭? 数据范围: 3×3 灯阵,开关只有「按」和「不按」两种选择。
📋 题目 2:爬行的蠕虫 一只蠕虫在一口深
n厘米的井底。每分钟它先向上爬u厘米, 如果这之后还没爬出井口,就向下滑d厘米(休息时滑落)。 问:多少分钟后蠕虫爬出井口? 数据范围:1 ≤ n, u, d ≤ 100,保证u > d。
两道题,两种思路
熄灯问题:答案藏在「每个开关按或不按」的所有组合里——共 2⁹ = 512 种。把每种组合都试一遍,检查哪种能把灯全熄灭。这就是枚举:暴力搜索整个答案空间。
爬行的蠕虫:没有任何组合可选——过程是确定的,一分钟一分钟往下推就行。爬、检查、滑落,循环往复。这就是模拟:按题意忠实复现过程。
这两招是 Bronze 级别最朴素也最可靠的得分手段:
- 枚举:答案空间有限时,把每个候选答案都验证一遍。不需要聪明,只需要不重不漏。
- 模拟:题目描述了一个过程时,让计算机一步步照做。不需要算法,只需要不出错。
拆解本章需要回答的问题
| 小节 | 回答的问题 |
|---|---|
| 2.8.1 | 枚举:什么是解空间?怎么枚举才不重不漏又够快? |
| 2.8.2 | 模拟:怎么写大模拟题才能不写错? |
| 2.8.3 | 枚举 × 模拟:两者结合的黄金套路(完整解开篇熄灯题) |
学习目标(学完本章你将能够):
- 为一道题建立正确的「解空间模型」,做到不重不漏
- 用三种手段缩小枚举空间:缩小范围、利用对称性、边枚举边排除
- 掌握子集枚举(位运算
mask)与排列枚举(next_permutation) - 用「纸上流程 → 模块化 → 统一量纲」的方法写对大模拟题
- 熟练处理日期、时间类模拟题(平闰年、大小月)
- 使用「枚举决策 + 模拟验证」套路解决 USACO Bronze 常见的开关类问题
2.8.1 枚举(Enumeration)
什么是解空间?
解空间是「所有可能的答案(候选解)组成的集合」。枚举就是逐个检查候选解,把满足条件的挑出来。
比如:
- 「熄灯问题」的解空间 = 9 个开关每个按/不按,共 2⁹ = 512 种方案;
- 「三个数中选两个」的解空间 = C(3,2) = 3 种选法;
- 「1 到 n 的整数」的解空间就是
1, 2, ..., n。
枚举的正确性取决于两件事:完备(每个可行解都被访问到)与不重(同一方案不被统计两次)。而枚举的效率取决于解空间的大小——所以枚举的功夫全在「如何让解空间变小」上。
枚举三要点
- 建立解空间模型:先把「候选答案到底是什么」想清楚,再动手写循环。是选一个数?选一对数?选一个子集?还是一组排列?
- 减少枚举空间:
- 缩小范围:利用数学关系直接收紧循环边界(如只需要枚举到 √n);
- 利用对称性:
(a, b)和(b, a)是同一方案时,只枚举a ≤ b的一半; - 边枚举边排除:一旦发现当前候选已经不可能,立即跳过(这是剪枝的雏形)。
- 选择枚举顺序:有时按特定顺序枚举能提前找到答案(如求「字典序最小的方案」就按字典序枚举),有时能让「不重不漏」自然成立。
下面用一道题把三要点全部走一遍。
完整实战 1:和为零的数对
📋 题目:和为零的数对 给定
n个整数,问其中有多少对数(i, j)(i < j)满足a[i] + a[j] = 0? 数据范围:1 ≤ n ≤ 10⁶,|a[i]| ≤ 10⁶。
第一步:建立解空间模型。 答案是一对下标 (i, j) 且 i < j——解空间是所有下标对,共 n(n−1)/2 个。
版本 1:朴素双重循环 O(n²)
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n);
for (auto &x : a) cin >> x;
long long cnt = 0;
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++) // j 从 i+1 开始:天然保证 i < j
if (a[i] + a[j] == 0) cnt++;
cout << cnt << endl;
return 0;
}
注意内层 j 从 i + 1 开始——这就是「利用对称性」:(i, j) 和 (j, i) 是同一对,只统计一次。如果写成 j 从 0 开始再判断 i < j,既慢又容易把「不重」写错。
样例验证:
输入: 输出:
5
3 -3 5 -5 2 2
(3, -3) 和 (5, -5) 共 2 对。
n = 10⁶ 时双重循环约 5×10¹¹ 次——超时。换模型:不要枚举数对,改为枚举每个数,问「它的相反数出现几次」。
版本 2:有序去半(排序 + 双指针思路的雏形)O(n log n)
先把数组排序,a[i] 的相反数如果存在,必然集中出现在某个连续段——用双指针可以在一轮扫描内统计完。这个思路在第 3 章双指针部分会展开,这里先看更彻底的版本 3。
版本 3:桶标记 O(n)
值域只有 |a[i]| ≤ 10⁶,开一个桶数组记录每个值出现的次数。扫描到 a[i] 时,之前的数里值为 -a[i] 的都能和它配对:
#include <bits/stdc++.h>
using namespace std;
const int OFFSET = 1000000; // 值域 [-1e6, 1e6] 平移到 [0, 2e6]
int bucket[2000001];
int main() {
int n;
cin >> n;
vector<int> a(n);
for (auto &x : a) cin >> x;
long long cnt = 0;
for (int i = 0; i < n; i++) {
// 在「已经见过的数」中找 a[i] 的相反数
if (-a[i] >= -1000000 && -a[i] <= 1000000)
cnt += bucket[-a[i] + OFFSET];
bucket[a[i] + OFFSET]++; // 然后把自己也记入桶中
}
cout << cnt << endl;
return 0;
}
先查再记的顺序很关键:如果先把自己记进桶再查,a[i] = 0 时会把自己和自己配对。
三版对比:
| 版本 | 思路 | 复杂度 | n = 10⁶ 时约需操作 |
|---|---|---|---|
| 朴素双重循环 | 枚举所有下标对 | O(n²) | 5×10¹¹(超时) |
| 排序 + 双指针 | 有序后配对数相邻 | O(n log n) | 2×10⁷ |
| 桶标记 | 每个数直接查相反数 | O(n + V) | 10⁶ |
这条「朴素 O(n²) → 有序去半 → 桶标记 O(n)」的优化路径是枚举题的通用成长路线:先保证正确,再一步步压解空间。
完整实战 2:子集枚举——选数凑和
当候选答案是「从中挑出若干个数」时,解空间就是子集。n 个数的子集共 2ⁿ 个,正好对应 n 位二进制数——这正是第 2.6 章位运算中位掩码的用武之地。
📋 题目:选数凑和 给定
n个整数和一个目标值target,问有多少种非空选法,使得选出的数之和恰好等于target? 两种选法不同当且仅当选中的下标集合不同。 数据范围:1 ≤ n ≤ 20,|a[i]| ≤ 10⁹,|target| ≤ 10⁹。
n ≤ 20 暗示 2²⁰ ≈ 10⁶——子集枚举的典型信号:
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
long long target;
cin >> n >> target;
vector<long long> a(n);
for (auto &x : a) cin >> x;
long long cnt = 0;
for (int mask = 0; mask < (1 << n); mask++) { // 枚举每个子集
long long sum = 0;
for (int i = 0; i < n; i++)
if (mask & (1 << i)) sum += a[i]; // 第 i 位为 1 表示选它
if (sum == target) cnt++;
}
cout << cnt << endl;
return 0;
}
样例验证:
输入: 输出:
5 5
1 2 3 4 5 3
三种选法:{5}、{1,4}、{2,3}。
两个易错点:
(1 << n)中若n ≥ 31会溢出——好在n ≤ 20时1 << 20只有约 10⁶,安全;sum用long long:20 个 10⁹ 相加达 2×10¹⁰,int必爆。
完整实战 3:排列枚举——密码锁
当候选答案是「顺序也有意义」时,解空间是排列。STL 的 next_permutation 按字典序逐个生成排列,是排列枚举的标准工具。
📋 题目:密码锁 一个三位密码锁由数字 1、2、3 各一个组成。从小到大列出所有可能的密码, 求第
k个密码是什么。 数据范围:1 ≤ k ≤ 6。
#include <bits/stdc++.h>
using namespace std;
int main() {
int k;
cin >> k;
string s = "123";
sort(s.begin(), s.end()); // 必须先排成最小排列
int idx = 1;
do {
if (idx == k) {
cout << s << endl;
return 0;
}
idx++;
} while (next_permutation(s.begin(), s.end()));
return 0;
}
样例验证: k = 4 输出 231(六种排列按字典序是 123、132、213、231、312、321)。
do-while 配合先排序是固定套路:next_permutation 在已经是最大排列时返回 false,从最小排列出发正好一个不漏。
枚举剪枝:预告
「边枚举边排除」的极端形式是:枚举到一半就发现此路不通,立刻回头换一条路——这就是回溯(剪枝)。它是第 5.2 章 BFS 与 DFS 的主角,也是子集枚举从 2ⁿ 走向更聪明搜索的必经之路。现在只需记住:枚举是回溯的骨架,剪枝是枚举的加速器。
2.8.2 模拟(Simulation)
模拟题的特征与心态
模拟题的标志是:题目把过程讲得一清二楚,你只需要照做。日期推算、游戏规则、机器执行、蚂蚁爬行……USACO Bronze 每场比赛几乎都有一道。
模拟题的特点是码量大、逻辑琐碎——难的不是想到做法,而是写对。正确的心理预期是:
模拟题不是比谁聪明,是比谁细心。把「我会了」和「我写对了」之间的距离,用工程习惯填平。
五大技巧
- 先在纸上写流程:把题目过程翻译成伪代码步骤,每一步的变量变化列一个表。写代码之前手算一遍样例,纸面流程和样例对得上再动手。
- 代码模块化:一个功能一个函数(或结构体)。「翻转一格灯」写成
flip(r, c),「判断闰年」写成isLeap(y)。模块短小、命名清楚,出错时定位快。 - 统一量纲:时间统一换成秒、日期统一换成天数、金额统一换成「分」。所有比较和加减只在统一单位后进行,
3 分 20 秒与200 秒混用是模拟题最经典的 bug 来源。 - 常量命名:大小月天数表、方向数组
dx/dy、一周七天名称……提取成const数组放在文件开头,魔法数字全部消灭。 - 分块调试:每写完一个模块就用样例(或自造小数据)单独验证它,再拼装。一次只怀疑一个模块——这比盯着 200 行
main找错高效十倍。系统方法见附录调试指南。
完整实战 1:爬行的蠕虫(开篇题回收)
按纸上流程走一遍:每分钟「先爬 u,若已出井口立即结束,否则下滑 d」——注意先判断后下滑的顺序,顺序反了会多算一分钟(出井口后不会滑落)。
| 分钟 | 爬后位置 | 出井? | 滑后位置 |
|---|---|---|---|
| 1 | u | 否 | u − d |
| 2 | 2u − d | 否 | 2u − 2d |
| ... | ... | ... | ... |
#include <bits/stdc++.h>
using namespace std;
int main() {
long long n, u, d;
cin >> n >> u >> d;
long long pos = 0, minutes = 0;
while (pos < n) {
pos += u; // 先爬
minutes++;
if (pos >= n) break; // 到顶就结束,不再下滑
pos -= d; // 没到顶,才滑落
}
cout << minutes << endl;
return 0;
}
样例验证: n = 10, u = 2, d = 1 时,位置依次为 2→1、4→3、6→5、8→7、10(到顶)——输出 9 分钟。手算公式:第 k 分钟爬后位置 = ku − (k−1)d = k + 1,k = 9 时恰好为 10。✅
再验一组边界:n = 5, u = 5, d = 3——第一分钟直接爬到 5 出井,输出 1(下滑根本没发生)。
完整实战 2:日期模拟——N 天之后是几月几号
日期是模拟题的常客,考点全在平闰年和大小月上。
📋 题目:N 天之后 给定一个日期
y-m-d和整数n,求n天之后的日期。 数据范围:1900 ≤ y ≤ 2100,日期合法,1 ≤ n ≤ 10000。
模块化 + 常量表的做法:
#include <bits/stdc++.h>
using namespace std;
bool isLeap(int y) {
return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
}
int daysOfMonth(int y, int m) {
static const int d[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (m == 2 && isLeap(y)) return 29;
return d[m];
}
int main() {
int y, m, d, n;
cin >> y >> m >> d >> n;
d += n;
while (d > daysOfMonth(y, m)) { // 当前月份装不下了就进位
d -= daysOfMonth(y, m);
m++;
if (m > 12) {
m = 1;
y++;
}
}
printf("%04d-%02d-%02d\n", y, m, d);
return 0;
}
样例验证:
输入:2024 2 28 2 输出:2024-03-01 (2024 是闰年,2 月有 29 天)
输入:2023 2 28 2 输出:2023-03-02 (2023 是平年,2 月只有 28 天)
输入:2024 12 31 1 输出:2025-01-01 (跨年进位)
注意 daysOfMonth(y, m) 在 while 条件和循环体里各调用一次——月份进位后要重新查天数,写错这里(比如循环外缓存)跨月时必错。
完整实战 3:胜负判定表——win() 函数
很多游戏类模拟题的核心是一张「谁克谁」的判定表。以石头剪刀布为例:
#include <bits/stdc++.h>
using namespace std;
char names[3][8] = {"Rock", "Paper", "Scissors"}; // 0=石头 1=布 2=剪刀
// 返回 1 表示 a 赢 b,-1 表示 a 输 b,0 平局
int win(int a, int b) {
if (a == b) return 0;
return (a - b + 3) % 3 == 1 ? 1 : -1;
}
int main() {
int a, b;
cin >> a >> b;
cout << names[a] << " vs " << names[b] << ": ";
int r = win(a, b);
if (r == 1) cout << "first wins" << endl;
else if (r == -1) cout << "second wins" << endl;
else cout << "tie" << endl;
return 0;
}
这里用了一个小巧的取模公式:把三种手势编号成环(石头 0 → 布 1 → 剪刀 2 → 石头 0),(a − b + 3) % 3 == 1 恰好表示「a 克 b」——比三行 if 更不容易写反。这套写法在 USACO 的「HPS(Hoop-Toss?)」石头剪刀布系列题(见第 6.2 章)中直接可用。
2.8.3 枚举 × 模拟:黄金套路
枚举决策 + 模拟验证
把两招合起来就是 Bronze 最强大的套路之一:
决策空间大但结构有规律时——枚举「第一步的所有决策」,剩下的过程用模拟推完。
开篇的熄灯问题正是如此。直接枚举 9 个开关的 2⁹ 种按法当然可行,但有一个更优雅的观察:
第 0 行的灯一旦确定按法,第 1 行怎么按就被唯一确定了——因为第 0 行还亮着的灯,只能靠按第 1 行正下方的开关来熄灭(第 0 行自己的开关已经定死了)。同理第 1 行确定后第 2 行也随之确定。
所以只需枚举第一行的 2³ = 8 种按法,剩下的 6 个开关由「逐行熄灭上方残留亮灯」的规则模拟推出来,最后检查最末行是否全灭即可:
#include <bits/stdc++.h>
using namespace std;
int main() {
int grid[3][3];
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
cin >> grid[i][j]; // 1=亮, 0=灭
int best = INT_MAX; // 最少按键数
int bestMask = -1; // 对应的第一行按法
// 枚举第一行的按法:bit j = 1 表示按下 (0, j)
for (int mask = 0; mask < 8; mask++) {
int g[3][3], press[3][3];
memcpy(g, grid, sizeof(grid));
memset(press, 0, sizeof(press));
int cnt = 0;
// 第 0 行的按法由 mask 决定
for (int j = 0; j < 3; j++)
if (mask & (1 << j)) { press[0][j] = 1; cnt++; }
// 应用第 0 行的按键:翻转自己和上下左右
for (int j = 0; j < 3; j++)
if (press[0][j]) {
int di[] = {0, 0, 0, 1, -1};
int dj[] = {0, 1, -1, 0, 0};
for (int k = 0; k < 5; k++) {
int ni = di[k], nj = j + dj[k];
if (ni >= 0 && ni < 3 && nj >= 0 && nj < 3)
g[ni][nj] ^= 1;
}
}
// 第 i 行还亮着的灯,只能靠按第 i+1 行正下方的开关熄灭
for (int i = 0; i + 1 < 3; i++) {
for (int j = 0; j < 3; j++)
if (g[i][j] == 1) {
press[i + 1][j] = 1;
cnt++;
}
// 应用第 i+1 行新确定的按键
for (int j = 0; j < 3; j++)
if (press[i + 1][j]) {
int di[] = {0, 0, 0, 1, -1};
int dj[] = {0, 1, -1, 0, 0};
for (int k = 0; k < 5; k++) {
int ni = i + 1 + di[k], nj = j + dj[k];
if (ni >= 0 && ni < 3 && nj >= 0 && nj < 3)
g[ni][nj] ^= 1;
}
}
}
// 最末行必须全灭,方案才成立
bool ok = true;
for (int j = 0; j < 3; j++)
if (g[2][j] == 1) { ok = false; break; }
if (ok && cnt < best) {
best = cnt;
bestMask = mask;
}
}
if (bestMask < 0) {
cout << "无解" << endl;
} else {
cout << "最少按键 " << best << " 次" << endl;
cout << "第一行按法(列0到2): ";
for (int j = 0; j < 3; j++)
cout << ((bestMask >> j) & 1);
cout << endl;
}
return 0;
}
样例验证: 输入全亮的 3×3(1 1 1 / 1 1 1 / 1 1 1),输出:
最少按键 5 次
第一行按法(列0到2): 101
即按下四角加中心共 5 个开关——这与 2⁹ 全枚举对拍的结果一致(本程序的答案已用 512 种初始状态逐一和暴力全枚举对拍验证过)。
为什么这个套路如此常见?
- 枚举量从 2⁹ 降到 2³——枚举空间被「逐行确定」的结构压缩了;
- 枚举之后的每一步都是机械的模拟——正确性由「照做」保证;
- 更妙的是,枚举第一行按法时顺便得到了「按键最少」的方案——枚举顺序不影响正确性,但影响能不能顺手求最优。
USACO Bronze 的「关灯」「按行染色」「翻硬币」系列题全是这个模板:枚举第一行的全部决策 → 逐行模拟确定后续 → 检查最后一行。把 3×3 推广到 R×C(第一行按法 2^C 种),就是完整的 USACO 真题规模。
⚠️ 常见错误
| 错误 | 原因 | 修复方案 |
|---|---|---|
| 枚举边界差一 | i < n 写成 i <= n,或 j 从 0 起重复枚举 | 明确区间开闭约定,i < j 用 j = i + 1 起步 |
1 << n 溢出 | n ≥ 31 时 int 位移未定义 | 子集枚举 n ≤ 20 才用 int;更大时改 long long 或换搜索 |
子集求和用 int | 20 个 10⁹ 相加达 2×10¹⁰ | sum 一律 long long |
| 模拟更新顺序颠倒 | 蠕虫题「先滑后爬」多算一分钟;先记桶再查把自己配对 | 纸上列表格逐步核对,写完用样例逐行跟踪 |
| 闰年判断写错 | 只写 y % 4 == 0,漏掉百年不闰 | 记住完整公式:(y%4==0 && y%100!=0) || y%400==0 |
| 大小月硬编码 | 二月天数、31/30 天月份靠背 | 用 daysOfMonth(y, m) 常量表 + 函数 |
| win 表写反 | 「a 克 b」和「b 克 a」弄反 | 用 (a - b + 3) % 3 == 1 这类对称公式,或写完用一组样例验证 |
| 越界方向的翻转漏判边界 | 熄灯题按 (0,0) 翻转上/左邻居 | 翻转前统一检查 0 <= ni < R && 0 <= nj < C |
调试建议
模拟题写完之后,按这个清单自造数据检查:
- 最小规模:n = 1、只有一行、只有一分钟——边界循环体是否还正确?
- 刚好卡边界:蠕虫第一分钟就出井;日期加 0 天;子集只选第一个数。
- 跨段进位:日期跨月、跨年;蠕虫整除到顶。
- 对拍:写一个朴素暴力版(如 2⁹ 全枚举),用小数据随机对拍——这是验证枚举题的终极手段。
本章小结
本章最重要的思想可以总结为两句话:
- 枚举的功夫在解空间:建对模型 → 压缩空间(缩范围 / 用对称 / 边排除)→ 选好顺序。
- 模拟的功夫在工程:纸上流程、模块化、统一量纲、常量命名、分块调试——用习惯换正确率。
速查表
| 枚举三要点 | 一句话 |
|---|---|
| 建立解空间模型 | 先想清楚候选答案是什么(数 / 数对 / 子集 / 排列) |
| 减少枚举空间 | 缩小范围、利用对称性(j = i+1)、边枚举边排除 |
| 选择枚举顺序 | 求字典序最小就按字典序枚举;利用结构逐行确定 |
| 模拟五技巧 | 一句话 |
|---|---|
| 纸上流程 | 伪代码 + 变量表,手算样例后再写代码 |
| 模块化 | 一个功能一个函数,命名即文档 |
| 统一量纲 | 时间转秒、日期转天数,单位统一后再运算 |
| 常量命名 | 大小月表、方向数组用 const 数组,消灭魔法数字 |
| 分块调试 | 每个模块单独过样例,一次只怀疑一处 |
枚举 × 模拟黄金套路:
枚举第一行的所有决策(2^C 种)
→ 逐行模拟:上一行的亮灯决定本行的按法
→ 检查最后一行是否满足条件
💪 练习题
🟢 题目 1:虫群计数
题目描述:
给定 n 个整数,问其中有多少对 (i, j)(i < j)满足 a[i] + a[j] == 10?
数据范围: 2 ≤ n ≤ 1000,0 ≤ a[i] ≤ 10。
输入格式:
第一行一个整数 n
第二行 n 个整数 a[1..n]
输出格式:
一个整数,表示满足条件的数对数量
样例输入:
5
3 7 5 5 10
样例输出:
3
提示: 直接用本章的双重循环 O(n²) 就够了(n ≤ 1000 时约 10⁶ 次)。配对有 (3,7)、(5,5)、(5,5) 共 3 对——注意下标不同、值相同的两对要分别计数。
🟢 题目 2:日期差
题目描述:
给定两个日期,求它们之间相差多少天(含首尾两端中的较早日期、不含较晚日期,即「从日期 1 走到日期 2 需要多少天」)。
数据范围: 两个日期均在 1900-01-01 到 2100-12-31 之间且合法,保证日期 1 不晚于日期 2。
输入格式:
两行,每行格式为 y m d(空格分隔)
输出格式:
一个整数,相差的天数
样例输入:
2024 2 28
2024 3 2
样例输出:
3
提示: 复用本章的 isLeap 和 daysOfMonth。逐天推进会超时吗?先算算最坏情况约 200 年 × 366 天 ≈ 7×10⁴ 次——逐天模拟完全够快。(更快的做法是儒略日公式,但模拟更不容易错。)
🟡 题目 3:熄灯问题原版
题目描述:
把本章的熄灯问题推广到 R × C 的灯阵(1 ≤ R, C ≤ 5)。给定初始灯态(不一定全亮),问是否存在按键方案把所有灯熄灭?若存在,输出最少按键数;若不存在,输出 -1。
数据范围: 1 ≤ R, C ≤ 5。
输入格式:
第一行 R C
接下来 R 行,每行 C 个 0/1 整数(1=亮)
输出格式:
一个整数:最少按键数,或 -1
样例输入:
2 3
0 1 0
1 0 1
样例输出:
2
提示: 枚举第一行的 2^C 种按法,其余行逐行模拟确定,检查最后一行。答案与 2^(R·C) 全枚举对拍验证。
🔴 题目 4:蛇形矩阵
题目描述:
从矩阵左上角出发,按「右、下、左、上」的顺序螺旋填充 1 到 n²,输出 n×n 的蛇形(螺旋)矩阵。
数据范围: 1 ≤ n ≤ 20。
输入格式:
一个整数 n
输出格式:
n 行,每行 n 个整数,右对齐占 4 列
样例输入:
4
样例输出:
1 2 3 4
12 13 14 5
11 16 15 6
10 9 8 7
提示: 纯模拟。方向数组 dx/dy + 「下一步出界或已填过就右转」。用 printf("%4d", ...) 控制格式。这是方向模拟 + 边界检查的集大成者——写对了,你就掌握了本章的所有边界技巧。
下一章预告: 进入第三部分,数据结构——数组与前缀和将把本章「桶标记」的思路推广成更强大的统计工具。