ACM 一个月学习计划 · DAY 07

Day 7 · 二维数组 + 第一周复盘

把一维的「一排格子」升级成二维的「一张表格」——从此代码里就有了坐标

今日目标:掌握二维/三维数组的定义与遍历,会用方向数组做坐标移动,并完成第一周复盘
预计用时:约 3 小时(含做题与复盘)  难度:入门 ~ 普及-
题目来源:洛谷 P5729 / P5730 / P5731 / P5732
← Day 06 · 数组进阶:桶计数

⏱今日时间安排

0 - 25 min读懂二维数组的定义、行列约定和遍历(第 1-2 节)
25 - 45 min方向数组与边界判断(第 3 节)——今天最重要的工具
45 - 60 min三维数组扫一眼就够(第 4 节,P5729 要用)
60 - 150 min做 4 道题,每题限时 30 分钟
150 - 180 min第一周复盘:错题重做 + 写「我犯过的错」清单(第 5 节)
💡 昨天内容的衔接

Day 5 你学会了「一排格子」——int a[100],用一个下标取数据。
Day 6 你学会了「用值当下标」——桶。
今天要把下标变成两个:a[i][j]。好处是,一旦有了两个下标,你就有了一张「地图」, 可以往里面画矩阵、画棋盘、画迷宫。后面 Day 18 开始的搜索题(DFS / BFS),全部建立在这一天的内容上。

1二维数组:给数据排成一张表格

从「一排抽屉」到「一格一格的货架」

一维数组像一排抽屉,只有一个编号:a[3] 就是第 3 个抽屉。

二维数组像一个货架:先说是第几层(行),再说是第几格(列),所以需要两个编号:a[3][5] 就是第 3 层第 5 格。

🎯 一句话记住约定

a[i][j] 里,i 是行(第几层、第几排),j 是列(第几个)。
习惯上 i 管「上下的变化」,j 管「左右的变化」。先写行,后写列,千万别反。

怎么写

// 一维:一排 100 个格子
int a[100];

// 二维:100 行 100 列,一共 100 x 100 = 10000 个格子
int b[100][100];

// 示例:3 行 4 列,并直接填好初值
int c[3][4] = {
    { 1,  2,  3,  4},
    { 5,  6,  7,  8},
    { 9, 10, 11, 12}
};

把 c 想象成一张表就很直观了:

1234 5678 9101112

红色那格是 c[0][3] —— 行下标 0、列下标 3,值是 4。
行和列的下标都是从 0 开始的,所以 3 行 4 列的下标范围是 i: 0~2、j: 0~3。

怎么遍历

跟一维数组比,只是多套了一层循环:

// 一维:一层循环
for(int i = 0; i < n; i++)
    cout << a[i] << " ";

// 二维:两层循环 —— 外层走行,内层走列
for(int i = 0; i < n; i++){          // 一行一行地走
    for(int j = 0; j < m; j++){      // 走到某一行后,从左到右扫一遍
        cout << b[i][j] << " ";
    }
    cout << endl;                    // 一行扫完了,换行
}
⚠️ 两层循环的顺序不能随便换

外层 i 走行、内层 j 走列 → 得到的是「一行一行地读」,也就是我们看书、看表格的顺序(叫行优先)。
如果把两层换过来(外层 j、内层 i),得到的是「一列一列地读」(列优先)。
两种本身都没错,但输出格式类题目对顺序有严格要求——今天 P5731、P5732 这两题, 换错顺序就会直接 WA。

为什么二维数组更容易越界

一维数组你要守住 1 个上界;二维数组要守住 2 个:行不能超过 n、列不能超过 m。

想干的事安全写法危险写法
访问 n×m 的表格里的每一格i<n && j<mi<=n && j<=m
访问坐标 (x, y),x 是行a[x][y]a[y][x](行列写反)
开 n×n 的方阵,下标从 1 开始a[n+5][n+5]a[n][n](越界)
🚫 最容易犯的两个错,今天 4 道题都躲不开

错 1:行列写反。a[行][列],不是 a[列][行]。写反了不报错,只是答案莫名其妙地不对。

错 2:数组开小。要存 n×n 的方阵,就至少开 [n][n];如果下标从 1 用到 n, 就得开 [n+1][n+1] 以上。多开 5 格,永远不吃亏。

2顺手知道一下:二维数组在内存里其实是「排成一排的」

这件事不背也能做题,但知道了能少踩很多坑。

声明 int b[3][4] 的时候,电脑实际上给你切了 12 个连续的 int, 然后规定「前 4 个算第 0 行、中间 4 个算第 1 行、后 4 个算第 2 行」。

内存里的位置第 1 个第 2 个第 3 个第 4 个第 5 个第 6 个…
对应元素b[0][0]b[0][1]b[0][2]b[0][3]b[1][0]b[1][1]…
🎯 由这件事推出的两条经验

① 越界的后果。写 b[0][4] 不会立刻崩溃,它其实悄悄访问到了 b[1][0]。 所以二维数组的越界常常是「答案错了但程序照跑」,非常难查——越要小心。

② 行优先遍历更快。按 i 外层、j 内层去扫,正好是顺着内存往后读, CPU 缓存喜欢这种顺序。数据量大时(几百万格)能明显更快。

所以「按行读入」是标准写法

// 读入一个 n 行 m 列的矩阵,标准写法
for(int i = 0; i < n; i++)
    for(int j = 0; j < m; j++)
        cin >> a[i][j];

输入里的数字本来就是「一行一行给的」,所以按行读入正好对上,不用任何额外处理。

3方向数组:让代码学会「上下左右」

这是今天最重要的工具,也是整个月计划里性价比最高的一个小技巧。

问题:站在格子 (i, j),怎么表示「它上面的格子」?

很自然:行减 1,列不变 → (i-1, j)。那下面、左边、右边呢?

方向行怎么变列怎么变新坐标
上 ↑-10(i-1, j)
下 ↓+10(i+1, j)
左 ←0-1(i, j-1)
右 →0+1(i, j+1)

如果每次都要写四段几乎一样的代码,又长又容易写错。于是我们把「变化的量」单独存成两个数组:

// dx 管行的变化,dy 管列的变化,下标 0~3 分别对应 上 下 左 右
int dx[4] = {-1, 1, 0, 0};
int dy[4] = { 0, 0, -1, 1};

// 于是「往第 d 个方向走一步」就变成了一句话:
int nx = x + dx[d];
int ny = y + dy[d];
💡 为什么叫 dx / dy

d 是 direction(方向)的缩写,x / y 是坐标。合起来就是「x 方向的增量、y 方向的增量」, 也就是数学里的 Δx、Δy。这里的 x 指的是行,y 指的是列。

顺时针顺序的版本(P5731 要用)

把 4 个方向按右 → 下 → 左 → 上排好,那么「右转」就等于 d = (d + 1) % 4, 因为 3 右转正好回到 0。

int dx[4] = { 0, 1,  0, -1};     // 0=右 1=下 2=左 3=上
int dy[4] = { 1, 0, -1,  0};

int d = 0;                        // 一开始朝右
d = (d + 1) % 4;                  // ★ 右转(转 4 次回到原点)
🎯 % 4 就是「绕圈」

0 → 1 → 2 → 3 → 0 → 1 …… 每次加 1 再对 4 取余,就能在一个圈里循环。 这个「加完取余」的动作今天要写好几遍,Day 8 的凯撒密码也是同一个套路(那里是对 26 取余)。

边界判断:必须写在 a[nx][ny] 前面

// ✅ 正确:先判断坐标合不合法,再访问
if(nx >= 0 && nx < n && ny >= 0 && ny < m){
    // 到这里才敢访问 a[nx][ny]
}

// ❌ 错误:先访问了再判断,越界已经发生了
if(a[nx][ny] == 0 && nx >= 0 && ...)   // 可能读到数组外面
⚠️ 四个条件一个都不能少

· nx >= 0 防止行变成负数(往上走出去了)
· nx < n 防止行超出下界
· ny >= 0、ny < m 是列的两个方向
四个条件用 && 连接。因为 && 有短路性质——只要前面有一个不成立, 后面的就不会算,所以把「判越界」写在左边、「访问数组」写在右边,是安全的。

「撞墙」其实有两层意思

写蛇形方阵的时候,「走不动了」有两种原因,两种都要转方向:

撞的是什么怎么判断
出界:走到了方阵外面nx < 0 || nx >= n || ny < 0 || ny >= n
撞到自己:那格已经填过数了a[nx][ny] != 0
💡 这就是「状态机」的入门味道

「当前方向 d」是一个状态,「撞墙就 d = (d+1)%4」是状态转移。 只要有这两样东西,程序就能自动绕圈——不用写四段重复循环。P5731 就靠这一招。

4三维数组:再往上加一个坐标

P5729 那个「玻璃立方体」是三维的,所以要用三个下标。

bool cut[25][25][25];      // cut[i][j][k]:小方块 (i,j,k) 被切掉了吗

// 遍历:三层循环
for(int i = 1; i <= w; i++)
    for(int j = 1; j <= x; j++)
        for(int k = 1; k <= h; k++)
            // 处理小方块 (i, j, k)
🎯 别怕,只是「多套一层」

二维要 2 层循环,三维要 3 层循环,思路完全一样:每多一个坐标,就多一层循环。
P5729 的范围是 w, x, h ≤ 20,所以最多 20×20×20 = 8000 个小方块, 每次切割最多标记 8000 个,100 次切割总共也就 80 万次操作——暴力标记完全够用。

⚠️ 三维数组非常吃内存,不要乱开

bool cut[25][25][25] 只有 15625 字节,随便开。
但如果开成 int a[1000][1000][1000],那是 40 亿字节 ≈ 4GB,直接爆炸。
开三维数组前,一定要先算:三个维度的上限各是多少。

顺便说一下:局部数组 vs 全局数组

写在哪儿初值能开多大
main() 里面(局部)随机值,必须手动清 0受栈空间限制,约几 MB
main() 外面(全局)自动全是 0大得多,可以放心用
💡 竞赛里的省心习惯

把数组都开到 main() 外面(全局),这样既自动清零,又能开得更大, 一举两得。今天四道题的数组我都写在全局位置。

5第一周复盘:这 6 天你到底学了什么

今天是第一个复盘日。复盘不是重看一遍,而是逼自己回答「我现在能不能不看题解写出来」。

第 1 周知识地图

Day主题核心工具代表题
1输入输出规范cin / cout、题目格式对齐P1001 P1421
2循环与累加for、整数溢出、浮点累加P1422 P1035
3while 与模拟终止条件、按规则推进状态P5720 P2669
4多重循环与图形「行 → 列」两层循环拆图形P5721 P5725
5一维数组遍历、计数、求最值P1046 P5724
6桶计数用「值」当下标,O(1) 查询P1554 P2550
7二维数组坐标 + 方向数组P5729 P5730 P5731 P5732

复盘任务(必须做完,别跳)

⚠️ 计划书里给的三个任务

任务 1:把这周所有 WA 过的题重做一遍,看看是不是真的会了。
任务 2:整理一份「我犯过的错」清单。
任务 3:自测——能否在 40 分钟内独立 AC 这三题?
    P1047 [NOIP 2005 普及组] 校门外的树
    P1085 [NOIP 2004 普及组] 不高兴的津津
    P1909 [NOIP 2016 普及组] 买铅笔
(这三题是机动题,第一周进度顺利的话就当加练)

「我犯过的错」清单模板

不用写得漂亮,就拿张纸照着填。前 6 天最常见的就是下面这 8 条,犯过的打勾:

#错误典型症状
1数组开小了答案时对时错,或者突然 RE
2循环边界少 1 / 多 1< 写成了 <=
3该开 long long 没开大测试点答案变成负数
4变量忘了初始化小样例对,大样例错
5行末多打了空格提示 WA 但肉眼看着一样
6输出顺序搞反了比如 P2550 奖项要从高到低
7只看了半个数据范围P1554 那样「看着大其实能枚举」
8忘记清 0(多组数据)这一次的答案带着上一次的残留
✅ 复盘的正确姿势

不要「看一遍题解觉得自己懂了」。正确做法是:把之前 WA 的题打开,关掉题解,从空文件重新写一遍, 提交,AC。只有重新写出来才算会。

6四道例题详解

下面每道题的代码我都用 g++ -std=c++14 -O2 -Wall 实际编译运行过,输出是你看到的真实结果。

P5729 【深基5.例7】工艺品制作 入门 · 三维标记数组

题意:有一个 w×x×h 的实心玻璃立方体,由一个个 1×1×1 的小方块拼成,小方块用坐标 (i, j, k) 表示。 现在做 q 次切割,每次给出长方体空洞的两个对角点,落在里面的小方块会被激光蒸发。 问全部切完之后还剩多少个小方块。

⚠️ 数据范围 / 关键约束

1 ≤ w, x, h ≤ 20,1 ≤ q ≤ 100。
并且 1 ≤ x₁ ≤ x₂ ≤ w,1 ≤ y₁ ≤ y₂ ≤ x,1 ≤ z₁ ≤ z₂ ≤ h (也就是说,给的坐标一定合法、一定是「小的在前」,不用自己交换)。

因为 w, x, h 都只有 20,整个立方体最多 8000 个小方块, 所以「先全部标记,最后数一遍」这种暴力做法完全跑得动。

思路

别去想「体积怎么减」这种数学做法——切割可能互相重叠,减法很容易算错。

换个角度:给每个小方块插一面小旗子,被切到的就插上旗。最后数一遍「没插旗的有几个」。 重叠多少次都无所谓,因为插第二次旗跟第一次没区别(true 还是 true)。

这就是 Day 6 学过的「标记桶」思想,只不过坐标从 1 个变成了 3 个。

  • 开 bool cut[25][25][25],全部默认 false(放全局就自动是 false)。
  • 每次切割,用三层循环把 [x₁,x₂]×[y₁,y₂]×[z₁,z₂] 里的所有格子标成 true。
  • 最后用三层循环数一遍 !cut[i][j][k] 的个数。
样例输入
4 4 4
1
1 1 1 2 2 2
样例输出
56

4×4×4 = 64 个小方块。切掉的是 i∈[1,2]、j∈[1,2]、k∈[1,2],也就是 2×2×2 = 8 个。
64 − 8 = 56。

完整代码

#include <iostream>
using namespace std;

// 三维标记数组:cut[i][j][k] = true 表示小方块 (i,j,k) 被激光蒸发了
bool cut[25][25][25];

int main(){
    int w, x, h, q;
    cin >> w >> x >> h;          // 长 宽 高
    cin >> q;                    // 切割次数

    for(int t = 0; t < q; t++){
        int x1, y1, z1, x2, y2, z2;
        cin >> x1 >> y1 >> z1 >> x2 >> y2 >> z2;
        // 把长方体里每一个小方块都打上「已蒸发」的标记
        for(int i = x1; i <= x2; i++)
            for(int j = y1; j <= y2; j++)
                for(int k = z1; k <= z2; k++)   // ★ 三个上界分别是 x2 / y2 / z2
                    cut[i][j][k] = true;
    }

    int ans = 0;
    for(int i = 1; i <= w; i++)
        for(int j = 1; j <= x; j++)
            for(int k = 1; k <= h; k++)
                if(!cut[i][j][k]) ans++;   // 没被标记的才剩下

    cout << ans << endl;
    return 0;
}
🚨 头号陷阱:把 h 当成空洞的高度上界

第三层循环的上界必须是 z₂,不是 h!这一条特别容易写错, 因为题目第一行给的变量叫 x, h,而切割参数里又有 z₁, z₂,名字长得像。

我把写错的版本(把 k <= z2 改成 k <= h)实际编译跑了一遍,结果是这样:

· 官方样例 4 4 4 / 1 / 1 1 1 2 2 2 → 输出 48(正确是 56)
· 自造数据 2 3 4 / 2 / 1 1 1 1 3 4 / 2 2 2 2 2 2 → 输出 9(正确是 11)

原因很简单:k <= h 会把空洞在高度方向拉长到整个立方体的高, 一刀切下去的方块数就变多了。比如官方样例里本该切 2×2×2 = 8 块,写错后变成 2×2×4 = 16 块,64 − 16 = 48。

所以这题的「上界」一定要盯紧:切割用 z₂,最后统计才用 h。

✅ 实测结果

官方样例 4 4 4 / 1 / 1 1 1 2 2 2 → 输出 56 完全一致

自造数据 2 3 4 / 2 / 1 1 1 1 3 4 / 2 2 2 2 2 2 → 输出 11 正确
(体积 2×3×4 = 24;第一次切掉 i=1 的一整片,3×4 = 12 个; 第二次切掉 (2,2,2) 这 1 个;剩 24 − 12 − 1 = 11)

最大范围 20 20 20 / 1 / 1 1 1 20 20 20 → 输出 0 正确
(整个立方体被切光)

易错点

  • 三个循环的上界别写混。切割时上界是 x2, y2, z2,统计时上界才是 w, x, h。
  • 变量名有坑。题目第一行是 w x h,但后面给的 6 个参数是按 (x₁,y₁,z₁),(x₂,y₂,z₂) 说的。 也就是说第二维(对应 w 的那个 x 上限)在切割参数里叫 y,读题时把对应关系写在纸上最保险。
  • 别用「总体积减每次切掉的体积」——两次切割可能重叠,重复的部分会被减两次,答案偏小。
  • 数组开 [25][25][25] 就够(上限 20),多开一点不心疼。
P5730 【深基5.例10】显示屏 普及- · 打表 + 按位输出

题意:每个阿拉伯数字都可以显示成 3×5 的点阵(X 是亮点,. 是暗点)。 给一个数字位数 n 和一串数字,把它们在显示屏上的效果打印出来,相邻两个数字之间要空一列。

⚠️ 数据范围 / 关键约束

1 ≤ n ≤ 100。题面里说第二行是一个「长度为 n 的自然数」—— 自然数可以以 0 开头(样例给的第一个字符就是 0), 所以必须用字符串或字符数组读,绝不能用 int / long long 读,否则前导 0 会丢。

思路:先打表,再按行拼

这题没有「算法」,考的是把一个大输出拆成规则的小块的能力。三步走:

  1. 打表:把 0~9 的点阵写死成一个表。这里用 string pat[10][5], 也就是「10 个数字 × 每个 5 行 × 每行 3 个字符」的二维字符串数组。
  2. 一行一行地输出:最外层循环 r 从 0 到 4,代表 5 行。
  3. 每行里按顺序拼接每个数字:内层循环 i 从 0 到 n−1, 输出 pat[数字][r];不是第一个数字时,先输出一个 . 当间隔列。
🎯 间隔是怎么来的

10 个数字 × 每个 3 列 + 9 个间隔列 = 30 + 9 = 39 列。 样例输出的每一行正好 39 个字符,可以对一遍。

间隔也能用 3 个点写成 3 列,但题目只说「一列间隔」,所以是 1 个 .。

0~9 的点阵表(对照样例抄下来最稳)

数字第 1 行第 2 行第 3 行第 4 行第 5 行
0XXXX.XX.XX.XXXX
1..X..X..X..X..X
2XXX..XXXXX..XXX
3XXX..XXXX..XXXX
4X.XX.XXXX..X..X
5XXXX..XXX..XXXX
6XXXX..XXXX.XXXX
7XXX..X..X..X..X
8XXXX.XXXXX.XXXX
9XXXX.XXXX..XXXX
样例输入
10
0123456789
样例输出
XXX...X.XXX.XXX.X.X.XXX.XXX.XXX.XXX.XXX
X.X...X...X...X.X.X.X...X.....X.X.X.X.X
X.X...X.XXX.XXX.XXX.XXX.XXX...X.XXX.XXX
X.X...X.X.....X...X...X.X.X...X.X.X...X
XXX...X.XXX.XXX...X.XXX.XXX...X.XXX.XXX

完整代码

#include <iostream>
#include <string>
using namespace std;

// 打表:pat[d][r] = 数字 d 的第 r 行(0 <= d <= 9, 0 <= r <= 4)
string pat[10][5] = {
    {"XXX", "X.X", "X.X", "X.X", "XXX"},   // 0
    {"..X", "..X", "..X", "..X", "..X"},   // 1
    {"XXX", "..X", "XXX", "X..", "XXX"},   // 2
    {"XXX", "..X", "XXX", "..X", "XXX"},   // 3
    {"X.X", "X.X", "XXX", "..X", "..X"},   // 4
    {"XXX", "X..", "XXX", "..X", "XXX"},   // 5
    {"XXX", "X..", "XXX", "X.X", "XXX"},   // 6
    {"XXX", "..X", "..X", "..X", "..X"},   // 7
    {"XXX", "X.X", "XXX", "X.X", "XXX"},   // 8
    {"XXX", "X.X", "XXX", "..X", "XXX"}    // 9
};

int main(){
    int n;
    string s;
    cin >> n >> s;                 // ★ 用 string 读,前导 0 不会丢

    for(int r = 0; r < 5; r++){    // 一行一行地输出
        for(int i = 0; i < (int)s.size(); i++){
            if(i) cout << ".";                     // 数字之间一列间隔
            cout << pat[s[i] - '0'][r];            // 查表
        }
        cout << endl;
    }
    return 0;
}
💡 s[i] - '0' 是把字符变成数字

s[i] 是字符 '7',不能直接当数组下标用。因为字符 '0'~'9' 在 ASCII 表里是连续的, 所以 '7' - '0' 正好等于整数 7。这是 Day 8 的重点,今天先混个眼熟。

✅ 实测结果

官方样例 10 / 0123456789 → 输出 5 行,与官方样例逐字符完全一致 (每行 39 个字符,10×3 + 9 个间隔)。

边界 1 / 7 → 输出 XXX / ..X / ..X / ..X / ..X 正确

最大数据 100 / 0123456789 重复 10 次 → 正常输出 5 行,每行 100×3 + 99 = 399 个字符,无异常。

易错点

  • 用 int 读那串数字 → 前导 0 丢掉,位数也可能超出 int 范围。必须用 string。
  • 间隔列漏了或多打了。每行长度应该是 3n + (n−1)。可以用 if(i) cout << "."; 来避免行首/行末多一个点。
  • 循环顺序写成「先按数字、再按行」 → 输出会变成一个数字挤在一起,不是横排。必须是外层行、内层数字。
  • 点阵表是「背下来或者对着样例抄」,抄错一个 X 就是 WA。写完把样例跑一遍最保险。
P5731 【深基5.习6】蛇形方阵 普及- · 状态机模拟

题意:给一个不大于 9 的正整数 n,输出 n×n 的蛇形方阵: 从左上角填 1 开始,顺时针一圈一圈往里填,直到填满 n²。 每个数字占 3 个字符,不够的前面补空格。

⚠️ 数据范围 / 关键约束

1 ≤ n ≤ 9。所以最大是 9×9 方阵,最大数字 81,都是两位数—— 每个数字占 3 个字符时正好不会挤在一起。

题面还特别说了一句:「每个数字都会占用 3 个字符,前面使用空格补齐」。 也就是说格式是 printf("%3d"),数字之间不加额外空格。

思路:当前方向 + 撞墙就右转

新手最容易想到的做法是「写四个阶段:先往右走 n 格,再往下走 n−1 格,再往左走 n−1 格……」—— 能写出来,但层数一多,边界就要调半天,非常容易错一位。

更稳的写法是状态机,三步就能说清:

  1. 站在 (0, 0),方向 d = 0(朝右)。
  2. 循环 v 从 1 到 n²:先在当前格子填 v,然后试着往 d 方向走一步。
  3. 如果下一步出界或者已经填过数,就 d = (d+1) % 4(右转),再走。

这样每一圈都不用单独写,程序会自己绕进去。

🎯 为什么「先填再走」这个顺序很重要

如果写成「先走再填」,那么最后一格(n²)会被填到方阵外面去——因为填完第 n²−1 格之后还会再走一步。 所以循环体里一定是:先写 a[x][y] = v;,再考虑移动。

走一遍 n = 4 的过程

填到几当前位置 (行,列)当前方向发生了什么
1 → 4(0,0)→(0,3)右一直往右
4(0,3)右 → 下右边出界了,右转
5 → 7(1,3)→(3,3)下一直往下
7(3,3)下 → 左下边出界了,右转
8 → 10(3,2)→(3,0)左一直往左
10(3,0)左 → 上左边出界了,右转
11 → 12(2,0)→(1,0)上一直往上
12(1,0)上 → 右(0,0) 已经填过了,右转
13 → 16(1,1)→(1,2)→(2,2)→(2,1)右 → 下 → 左继续绕内圈,最后一格是 16,填满结束

结果就是下面这张方阵:

1234 1213145 1116156 10987

红色是第二圈(内层)的数字。

样例输入
4
样例输出
  1  2  3  4
 12 13 14  5
 11 16 15  6
 10  9  8  7

注意每行开头:第 1 行前面有 2 个空格(因为 1 是个位数),第 2~4 行前面只有 1 个空格。 这就是 %3d 的效果——右对齐,宽度 3。

完整代码

#include <iostream>
#include <cstdio>
using namespace std;

int a[15][15];                       // 蛇形方阵,0 表示还没填

// 方向数组:0 右、1 下、2 左、3 上(顺时针)
int dx[4] = {0, 1, 0, -1};
int dy[4] = {1, 0, -1, 0};

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

    int x = 0, y = 0, d = 0;         // 从 (0,0) 出发,朝右
    for(int v = 1; v <= n * n; v++){
        a[x][y] = v;                 // ★ 先填当前格,再考虑往哪走
        int nx = x + dx[d], ny = y + dy[d];
        // 撞墙(出界)或者撞到已填过的格子,就右转
        if(nx < 0 || nx >= n || ny < 0 || ny >= n || a[nx][ny] != 0){
            d = (d + 1) % 4;         // ★ 右转
            nx = x + dx[d];
            ny = y + dy[d];
        }
        x = nx;
        y = ny;
    }

    for(int i = 0; i < n; i++){
        for(int j = 0; j < n; j++)
            printf("%3d", a[i][j]);  // ★ 每个数字占 3 个字符,右对齐
        printf("\n");
    }
    return 0;
}
💡 为什么这里用了 printf 而不是 cout

cout 也可以做到(cout << setw(3) << a[i][j]),但那需要额外 #include <iomanip>, 而且要记住 setw 只对紧接着的一个输出生效,写循环时容易漏。 printf("%3d") 更短更直观,竞赛里「格式敏感」的输出基本都用它。

✅ 实测结果

官方样例 4 → 输出与官方样例逐字符完全一致(每行 12 个字符)。
实际输出:第一行   1  2  3  4(前面 2 个空格), 第二行  12 13 14  5,第三行  11 16 15  6, 第四行  10  9  8  7。

边界 1 → 输出   1(1×1 方阵) 正确

最大数据 9 → 正常输出 9×9 方阵,每行 27 个字符,中心是 81,四周 1/9/17/25… 完全符合蛇形规律。
第一行   1  2  3  4  5  6  7  8  9, 第二行  32 33 34 35 36 37 38 39 10, 中心行  29 54 71 80 81 76 63 42 13。

易错点

  • 「先填再走」写反了 → 最后一格跑到方阵外面或者被覆盖,WA。
  • 只判断出界,忘了判断「已经填过」 → 内圈会一直转圈覆盖已有的数字(会死循环或者填错)。两个条件必须都要有。
  • 输出宽度写成 %3d 但其实要求不同:这题就是一个数字 3 格、无额外空格。 别自己脑补成「数字之间再加空格」。
  • 方阵数组的初始化:全局数组默认是 0,刚好能用来表示「没填过」。如果写成局部数组记得初始化。
  • 方向数组的顺序:必须是「右 下 左 上」才是顺时针。写成「上 右 下 左」就变成从中间开始了。
P5732 【深基5.习7】杨辉三角 入门 · 二维递推

题意:给出 n(1 ≤ n ≤ 20),输出杨辉三角的前 n 行。 每行相邻两个数之间空一格,行末不要多空格。

⚠️ 数据范围 / 关键约束

题面写的是 n(1 ≤ n ≤ 20)(这道题的「说明/提示」里没有额外数据范围,范围就在题目描述里)。
n ≤ 20 意味着最后一行有 20 个数,最大的是 C(19,9) = 92378,用 int 完全放得下, 不需要 long long。

思路:每个数 = 上一行「左上 + 正上」

看样例的规律:

1
1 1
1 2 1        // 2 = 上一行的 1 + 1
1 3 3 1      // 3 = 上一行的 1 + 2(第一个 3),另一个 3 = 2 + 1
1 4 6 4 1    // 6 = 上一行的 3 + 3
1 5 10 10 5 1

换成下标写法(行、列都从 1 开始数):

🎯 递推公式

a[i][j] = a[i-1][j-1] + a[i-1][j]

意思是:第 i 行第 j 个数,等于它左上方的数加上它正上方的数。
这一行的两端(a[i][1] 和 a[i][i])没有两个上方的数可用,直接规定为 1。

位置上长什么样

行\列12345
11
211
3121
41331
514641

看第 5 行第 3 列的 6:左上 a[4][2] = 3,正上 a[4][3] = 3,3 + 3 = 6 ✔

样例输入
6
样例输出
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1

完整代码

#include <iostream>
using namespace std;

int a[25][25];                       // a[i][j] 存第 i 行第 j 个数(从 1 开始)

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

    for(int i = 1; i <= n; i++){
        a[i][1] = 1;                 // 每行第一个是 1
        a[i][i] = 1;                 // 每行最后一个是 1
        for(int j = 2; j < i; j++)
            a[i][j] = a[i-1][j-1] + a[i-1][j];   // ★ 左上 + 正上
    }

    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= i; j++){
            if(j > 1) cout << " ";   // ★ 数字之间一个空格,行末不留空格
            cout << a[i][j];
        }
        cout << endl;
    }
    return 0;
}
💡 内层循环为什么是 j = 2; j < i

第 i 行只有 i 个数,两端已经手动填成 1 了,所以中间只有 j = 2, 3, ..., i-1 这些位置需要用公式算。
如果写成 j <= i,会把两端算成 a[i-1][i-1] + a[i-1][i], 而 a[i-1][i] 根本没填过(是 0),虽然结果碰巧还是 1,但写法不严谨,n 大一点就容易出问题。

💡 「行末不留空格」的通用套路

不要写「输出数字 + 输出空格」,而是写「不是第一个就先后补一个空格,再输出数字」, 也就是 if(j > 1) cout << " ";。
这样最后一个数字后面自然就没有多余空格。洛谷对行末空格通常是宽容的, 但很多比赛(比如 NOIP)是严格比对的,现在就养成好习惯。

✅ 实测结果

官方样例 6 → 输出 6 行,与官方样例逐字符完全一致。

边界 1 → 输出 1 正确(只有一行一个数)

最大数据 20 → 正常输出 20 行,数字都在 int 范围内(最大 92378),无溢出。

易错点

  • 忘了给两端赋 1 → 整张表全是 0 或者乱掉。
  • 行末多一个空格 → 有些评测机判 WA。用 if(j > 1) 控制。
  • 数组开小了:行、列都从 1 用到 n,所以至少要开 [n+1][n+1]。开 25 就够用,别开 [20][20]。
  • 递推公式记反:是 a[i-1][j-1] + a[i-1][j],不是 a[i-1][j] + a[i-1][j+1]。记不住就对着样例第 5 行的 6 推一遍。
  • 这题是「用上一行算这一行」的典型,也叫递推。Day 17 会专门学递推,今天的杨辉三角就是它的热身。

✗今日常见错误汇总

#错误写法后果正确写法
1a[j][i]行列写反,答案莫名其妙地不对a[i][j](先行后列)
2int b[20][20] 存 n=20 的方阵下标 20 越界 → 玄学 WA / REint b[25][25]
3for(j=0;j<=m;j++)多扫一列,越界for(j=0;j<m;j++)
4if(a[nx][ny]==0 && nx>=0 …)先访问后判断,越界读先判边界,再访问
5只用 nx>=0 判越界漏掉上界,越界四个条件全写
6蛇形方阵「先走再填」最后一格填到方阵外先填 a[x][y],再移动
7P5730 用 int 读数字串前导 0 丢失 / n=100 时直接溢出用 string 读
8行末多打一个空格严格评测机判 WAif(j>1) cout<<" ";
9把 h 当成空洞的 z 上界官方样例就挂(输出 48,应为 56)切的时候上界用 z₂
10局部数组忘记清零乱码答案数组开在全局,或 ={0}

✓自测:你真的会了吗

第 1 题:行列写反了会怎样?

有人写 a[5][3] = 7;,但本意是「第 3 行第 5 列 = 7」。

答:如果行列的上限不同(比如 5 行 3 列的矩阵),写成 a[5][3] 就是直接越界—— 行下标最大只能是 4,列下标最大只能是 2,两个都超了。
如果行列上限恰好相同(方阵),那不会越界,但数据会跑到转置的位置上, 程序照跑不误、答案全错,属于最难查的一类 bug。写 a[i][j] 时永远在心里念一遍「先行后列」。

第 2 题:n×m 的表格,两层循环怎么写?

要求按「一行一行」的顺序,把每个数读进来。

答:

for(int i = 0; i < n; i++)
    for(int j = 0; j < m; j++)
        cin >> a[i][j];

外层 i < n 走行,内层 j < m 走列,都是小于不是小于等于。 数组至少要开 a[n][m],习惯上开 a[1005][1005] 这种大一点的。

第 3 题:方向数组的三个小问

(a) dx[4] = {0, 1, 0, -1}; dy[4] = {1, 0, -1, 0}; 里 d = 2 是哪个方向?
(b) 从 (i, j) 往 d = 3 的方向走一步,新坐标是什么?
(c) 右转 5 次之后 d 等于几?

答:
(a) dx[2] = 0(行不变)、dy[2] = -1(列减 1)→ 左。
(b) dx[3] = -1、dy[3] = 0 → (i-1, j),也就是上方。
(c) 每次右转是 d = (d+1)%4。转 5 次 = 转 4 次(回到原地)+ 1 次 → 从 0 变成 1,即下。

第 4 题:杨辉三角第 n 行有几个数?最大的数大概多大?

n = 20 的时候。

答:第 i 行有 i 个数,所以第 20 行有 20 个数。
最大值在中间,是组合数 C(19,9) = 92378,int 完全够用(int 上限约 21 亿)。
但如果题目把 n 开到 100,中间的数会暴涨到 10²⁸ 级别,那时候就必须用高精度了—— 所以看题目一定要看 n 的上限。

第 5 题(手写代码):打印一个 n×n 的方阵,对角线(i == j)打 1,其余打 0
for(int i = 0; i < n; i++){
    for(int j = 0; j < n; j++){
        if(j) cout << " ";
        if(i == j) cout << 1;      // 主对角线
        else cout << 0;
    }
    cout << endl;
}

★ 想一想:如果要打副对角线(从右上到左下),条件是什么?
答:i + j == n - 1。

▤速查卡(截图存手机)

【定义与初始化】
  int a[105][105];                 // 开在 main 外面 → 自动全 0
  int c[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};

【按行遍历(最常用)】
  for(int i = 0; i < n; i++)
      for(int j = 0; j < m; j++)
          cin >> a[i][j];          // 或 cout,或处理

【方向数组】
  顺时针(右 下 左 上):
      int dx[4] = {0, 1,  0, -1};
      int dy[4] = {1, 0, -1,  0};
  上下左右:
      int dx[4] = {-1, 1, 0, 0};
      int dy[4] = { 0, 0, -1, 1};
  右转: d = (d + 1) % 4;
  走一步:nx = x + dx[d];  ny = y + dy[d];

【边界判断(必须写在访问数组之前)】
  if(nx >= 0 && nx < n && ny >= 0 && ny < m) { ... }

【蛇形方阵状态机骨架】
  for(int v = 1; v <= n*n; v++){
      a[x][y] = v;                            // 先填
      int nx = x + dx[d], ny = y + dy[d];
      if(出界 || a[nx][ny] != 0) {            // 撞墙
          d = (d + 1) % 4;                    // 右转
          nx = x + dx[d]; ny = y + dy[d];
      }
      x = nx; y = ny;                         // 再走
  }

【杨辉三角递推】
  a[i][1] = a[i][i] = 1;
  for(j = 2; j < i; j++)
      a[i][j] = a[i-1][j-1] + a[i-1][j];

【定宽输出】
  printf("%3d", x);                // 右对齐,宽度 3(需 <cstdio>)
  cout << setw(3) << x;            // 需 <iomanip>,只对下一个输出生效

【防越界铁律】
  1 行 x m 列 → 至少开 [n+1][m+1],习惯开 [n+5][m+5]
  循环用 i < n,不用 i <= n
  先判边界,再访问 a[nx][ny]

→明天预告 · Day 08

字符与字符串基础

今天你已经偷偷用到了两个「字符级」的操作:P5730 里的 s[i] - '0', 还有把数字串当 string 读进来。明天要把这套东西系统地学一遍:

另外别忘了:今天的复盘任务(错题重做 + 犯错清单 + 40 分钟自测三题)如果还没做完,明天开新内容之前先补上。