把一维的「一排格子」升级成二维的「一张表格」——从此代码里就有了坐标
| 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),全部建立在这一天的内容上。
一维数组像一排抽屉,只有一个编号: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 想象成一张表就很直观了:
红色那格是 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<m | i<=n && j<=m |
| 访问坐标 (x, y),x 是行 | a[x][y] | a[y][x](行列写反) |
| 开 n×n 的方阵,下标从 1 开始 | a[n+5][n+5] | a[n][n](越界) |
错 1:行列写反。a[行][列],不是 a[列][行]。写反了不报错,只是答案莫名其妙地不对。
错 2:数组开小。要存 n×n 的方阵,就至少开 [n][n];如果下标从 1 用到 n,
就得开 [n+1][n+1] 以上。多开 5 格,永远不吃亏。
这件事不背也能做题,但知道了能少踩很多坑。
声明 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];
输入里的数字本来就是「一行一行给的」,所以按行读入正好对上,不用任何额外处理。
这是今天最重要的工具,也是整个月计划里性价比最高的一个小技巧。
很自然:行减 1,列不变 → (i-1, j)。那下面、左边、右边呢?
| 方向 | 行怎么变 | 列怎么变 | 新坐标 |
|---|---|---|---|
| 上 ↑ | -1 | 0 | (i-1, j) |
| 下 ↓ | +1 | 0 | (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];
d 是 direction(方向)的缩写,x / y 是坐标。合起来就是「x 方向的增量、y 方向的增量」, 也就是数学里的 Δx、Δy。这里的 x 指的是行,y 指的是列。
把 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 就靠这一招。
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,直接爆炸。
开三维数组前,一定要先算:三个维度的上限各是多少。
| 写在哪儿 | 初值 | 能开多大 |
|---|---|---|
main() 里面(局部) | 随机值,必须手动清 0 | 受栈空间限制,约几 MB |
main() 外面(全局) | 自动全是 0 | 大得多,可以放心用 |
把数组都开到 main() 外面(全局),这样既自动清零,又能开得更大,
一举两得。今天四道题的数组我都写在全局位置。
今天是第一个复盘日。复盘不是重看一遍,而是逼自己回答「我现在能不能不看题解写出来」。
| Day | 主题 | 核心工具 | 代表题 |
|---|---|---|---|
| 1 | 输入输出规范 | cin / cout、题目格式对齐 | P1001 P1421 |
| 2 | 循环与累加 | for、整数溢出、浮点累加 | P1422 P1035 |
| 3 | while 与模拟 | 终止条件、按规则推进状态 | 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。只有重新写出来才算会。
下面每道题的代码我都用 g++ -std=c++14 -O2 -Wall 实际编译运行过,输出是你看到的真实结果。
题意:有一个 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)。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; }
第三层循环的上界必须是 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),多开一点不心疼。题意:每个阿拉伯数字都可以显示成 3×5 的点阵(X 是亮点,. 是暗点)。
给一个数字位数 n 和一串数字,把它们在显示屏上的效果打印出来,相邻两个数字之间要空一列。
1 ≤ n ≤ 100。题面里说第二行是一个「长度为 n 的自然数」——
自然数可以以 0 开头(样例给的第一个字符就是 0),
所以必须用字符串或字符数组读,绝不能用 int / long long 读,否则前导 0 会丢。
这题没有「算法」,考的是把一个大输出拆成规则的小块的能力。三步走:
string pat[10][5],
也就是「10 个数字 × 每个 5 行 × 每行 3 个字符」的二维字符串数组。r 从 0 到 4,代表 5 行。i 从 0 到 n−1,
输出 pat[数字][r];不是第一个数字时,先输出一个 . 当间隔列。10 个数字 × 每个 3 列 + 9 个间隔列 = 30 + 9 = 39 列。 样例输出的每一行正好 39 个字符,可以对一遍。
间隔也能用 3 个点写成 3 列,但题目只说「一列间隔」,所以是 1 个 .。
| 数字 | 第 1 行 | 第 2 行 | 第 3 行 | 第 4 行 | 第 5 行 |
|---|---|---|---|---|---|
| 0 | XXX | X.X | X.X | X.X | XXX |
| 1 | ..X | ..X | ..X | ..X | ..X |
| 2 | XXX | ..X | XXX | X.. | XXX |
| 3 | XXX | ..X | XXX | ..X | XXX |
| 4 | X.X | X.X | XXX | ..X | ..X |
| 5 | XXX | X.. | XXX | ..X | XXX |
| 6 | XXX | X.. | XXX | X.X | XXX |
| 7 | XXX | ..X | ..X | ..X | ..X |
| 8 | XXX | X.X | XXX | X.X | XXX |
| 9 | XXX | X.X | XXX | ..X | XXX |
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。if(i) cout << "."; 来避免行首/行末多一个点。X 就是 WA。写完把样例跑一遍最保险。题意:给一个不大于 9 的正整数 n,输出 n×n 的蛇形方阵: 从左上角填 1 开始,顺时针一圈一圈往里填,直到填满 n²。 每个数字占 3 个字符,不够的前面补空格。
1 ≤ n ≤ 9。所以最大是 9×9 方阵,最大数字 81,都是两位数—— 每个数字占 3 个字符时正好不会挤在一起。
题面还特别说了一句:「每个数字都会占用 3 个字符,前面使用空格补齐」。
也就是说格式是 printf("%3d"),数字之间不加额外空格。
新手最容易想到的做法是「写四个阶段:先往右走 n 格,再往下走 n−1 格,再往左走 n−1 格……」—— 能写出来,但层数一多,边界就要调半天,非常容易错一位。
更稳的写法是状态机,三步就能说清:
(0, 0),方向 d = 0(朝右)。v 从 1 到 n²:先在当前格子填 v,然后试着往 d 方向走一步。d = (d+1) % 4(右转),再走。这样每一圈都不用单独写,程序会自己绕进去。
如果写成「先走再填」,那么最后一格(n²)会被填到方阵外面去——因为填完第 n²−1 格之后还会再走一步。
所以循环体里一定是:先写 a[x][y] = v;,再考虑移动。
| 填到几 | 当前位置 (行,列) | 当前方向 | 发生了什么 |
|---|---|---|---|
| 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,填满结束 |
结果就是下面这张方阵:
红色是第二圈(内层)的数字。
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; }
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。
%3d 但其实要求不同:这题就是一个数字 3 格、无额外空格。
别自己脑补成「数字之间再加空格」。题意:给出 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。
| 行\列 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| 1 | 1 | ||||
| 2 | 1 | 1 | |||
| 3 | 1 | 2 | 1 | ||
| 4 | 1 | 3 | 3 | 1 | |
| 5 | 1 | 4 | 6 | 4 | 1 |
看第 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),无溢出。
if(j > 1) 控制。[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 推一遍。| # | 错误写法 | 后果 | 正确写法 |
|---|---|---|---|
| 1 | a[j][i] | 行列写反,答案莫名其妙地不对 | a[i][j](先行后列) |
| 2 | int b[20][20] 存 n=20 的方阵 | 下标 20 越界 → 玄学 WA / RE | int b[25][25] |
| 3 | for(j=0;j<=m;j++) | 多扫一列,越界 | for(j=0;j<m;j++) |
| 4 | if(a[nx][ny]==0 && nx>=0 …) | 先访问后判断,越界读 | 先判边界,再访问 |
| 5 | 只用 nx>=0 判越界 | 漏掉上界,越界 | 四个条件全写 |
| 6 | 蛇形方阵「先走再填」 | 最后一格填到方阵外 | 先填 a[x][y],再移动 |
| 7 | P5730 用 int 读数字串 | 前导 0 丢失 / n=100 时直接溢出 | 用 string 读 |
| 8 | 行末多打一个空格 | 严格评测机判 WA | if(j>1) cout<<" "; |
| 9 | 把 h 当成空洞的 z 上界 | 官方样例就挂(输出 48,应为 56) | 切的时候上界用 z₂ |
| 10 | 局部数组忘记清零 | 乱码答案 | 数组开在全局,或 ={0} |
有人写 a[5][3] = 7;,但本意是「第 3 行第 5 列 = 7」。
答:如果行列的上限不同(比如 5 行 3 列的矩阵),写成 a[5][3] 就是直接越界——
行下标最大只能是 4,列下标最大只能是 2,两个都超了。
如果行列上限恰好相同(方阵),那不会越界,但数据会跑到转置的位置上,
程序照跑不误、答案全错,属于最难查的一类 bug。写 a[i][j] 时永远在心里念一遍「先行后列」。
要求按「一行一行」的顺序,把每个数读进来。
答:
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] 这种大一点的。
(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,即下。
n = 20 的时候。
答:第 i 行有 i 个数,所以第 20 行有 20 个数。
最大值在中间,是组合数 C(19,9) = 92378,int 完全够用(int 上限约 21 亿)。
但如果题目把 n 开到 100,中间的数会暴涨到 10²⁸ 级别,那时候就必须用高精度了——
所以看题目一定要看 n 的上限。
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]
字符与字符串基础
今天你已经偷偷用到了两个「字符级」的操作:P5730 里的 s[i] - '0',
还有把数字串当 string 读进来。明天要把这套东西系统地学一遍:
cin >> 和 getline 混用时,getline 会读到一个空串另外别忘了:今天的复盘任务(错题重做 + 犯错清单 + 40 分钟自测三题)如果还没做完,明天开新内容之前先补上。