循环嵌套的层数等于图形的维数。一维是“打 10 个星”,二维是“打 n 行、每行若干个”,三维(第 14 节的枚举)是“给 n×n 的每个格子再列出 n 种可能”。这一节只解决二维:外层管行,内层管列,换行属于行。
先记住这个判断标准:图形题的判题方式对新手极其友好——跑一遍,形状歪了就是公式错,形状对了答案基本就对。它把“抽象公式 → 循环边界”这条链压缩成肉眼可验证的产物,所以在入门阶段练它最划算。
输入一个整数 n(1 ≤ n ≤ 13),输出一个由数字组成的直角三角形。
第 1 行有 n 个数,第 2 行有 n − 1 个,……,第 n 行有 1 个数;数字从 1 开始依次加 1,每个数固定占 2 个字符,不足两位时在左边补 0。同一行的数之间不加空格。
n ≤ 13 是因为 n = 13 时最大编号 n(n+1)/2 = 91,恰好两位数;再多一位,补零的写法就要换。
这道题的图形有两层结构:行的长度递减,以及编号跨行连续。前者靠内层上界,后者靠一个定义在循环外面的计数器。先把下面两个东西的骨架看清,再谈怎么写。
| 组成 | 含义 / 格式要求 |
|---|---|
外层 for | 控制“第几行”。i 从 1 走到 n,进入内层时 i 固定不动,整轮内层共用同一个 i。 |
内层 for | 控制“这一行的第几个”。外层每走一步,内层都从初值重新跑一整轮,不会接着上一行。 |
内层上界写成 i 的表达式 | 图形题的上界几乎总是含 i 的公式:i、n-i+1、2*i-1、n-i。上界错一个符号,图形立刻歪。 |
变量名不能重复 | 内层变量与外层同名会遮蔽外层变量 i,外层循环失控;g++ 直接报 warning 或 error。 |
总次数要会算 | 矩形是 n×m,三角形是 Σ(n−i+1) = n(n+1)/2。写之前先估这个数,超过 10⁸ 就要警惕。 |
换行的位置 | 换行语句必须与内层 for 平级:一行打完才换。它属于“行”,不属于“格子”。 |
#include <bits/stdc++.h>
using namespace std;
int main() {
// 3 行 5 列的矩形:外层 3 轮、内层每轮 5 个 → 共 15 个 '*'
for (int i = 1; i <= 3; ++i) { // i = 1,2,3
for (int j = 1; j <= 5; ++j) { // 每行都从 j = 1 重新开始
putchar('*');
}
putchar('\n'); // ★ 与内层 for 平级,写进内层图形就散架
}
return 0;
}
// 输出:
// *****
// *****
// *****最常见的 WA:把 putchar('\n') 写进内层 → 每个星号后面都换行,图形变成一条竖线。
内层上界可以写表达式(j <= n - i + 1),因为每进入一次内层都会重新计算上界。
行内计数器 j 每行都归零;跨行累加的计数器必须定义在两层循环之外——这两类变量别混。
嵌套三层以上时,先写清每层“代表什么”,否则下标含义会自己骗自己。
| 组成 | 含义 / 格式要求 |
|---|---|
参数 c | 要输出的字符,用单引号写:'*'、' '、'\n'。形参是 int,实际传的是它的 ASCII 码。 |
返回值 | 成功返回写出的那个字符(转成 int),失败返回 EOF。图形题里基本不用管。 |
'0' + d | 数字字符在 ASCII 里是连续的:'0' = 48,所以 '0' + 7 就是字符 '7'。d 必须在 0~9 之间。 |
'a' + k | 同理,'a' + 2 是 'c',用于循环打一串字母。 |
速度 | 一次一个字节,比 cout << 字符 快 2~5 倍。10⁶ 级字符输出优先用它。 |
必须用单引号 | putchar("*") 传的是字符串指针,类型不符,编译不过。 |
#include <bits/stdc++.h>
using namespace std;
int main() {
putchar('*'); // *
putchar(' '); // 一个空格
putchar('\n'); // 换行
putchar('0' + 7); // 数字 7 → 字符 '7'
putchar('0' + 42 % 10); // 先取个位 2,再变字符 '2'
// putchar("*"); // ✗ 编译错误:双引号是字符串,不能当字符用
return 0;
}putchar 一次只能打一个字符;要打整段文字用 cout 或 puts。
要打多位数,要么自己拆十位个位(本节例题),要么等后面学 printf("%02d", x)。
'0' + x 只在 x ∈ [0,9] 时是数字字符;x = 10 会打出字符 ':',这是“补零写错”最常见的翻车点。
任何字符图形都只有三件事:第 i 行先打几个空格、再打几个什么字符、打完换行。把前两件事写成含 i 的表达式,代码就只剩机械翻译。
例题 P5721 没有空格,只有“两位数”。两位数固定占 2 个字符,所以每行字符数 = 2 × 每行个数:
| 行 i | 空格数 | 数的个数 | 字符数 |
|---|---|---|---|
| 1 | 0 | n | 2n |
| 2 | 0 | n − 1 | 2(n − 1) |
| i | 0 | n − i + 1 | 2(n − i + 1) |
| n | 0 | 1 | 2 |
两列数一写出来,循环边界就定了:外层 i = 1..n,内层 j = 1..n-i+1,内层循环体只干一件事——编号加一,然后打两位。
关键认知:编号 cnt 要跨行连续,所以它定义在所有 for 之外;行内计数器 j 每行归零,跨行计数器 cnt 从不归零。分清这两类变量,多重循环就不会乱。
int cnt = 0; // 跨行累加,必须在两层循环之外
for (int i = 1; i <= n; ++i) { // 外层 = 第几行
for (int j = 1; j <= n - i + 1; ++j) { // 内层 = 这一行第几个数
++cnt;
if (cnt < 10) putchar('0'); // 一位数,补前导 0
if (cnt >= 10) putchar('0' + cnt / 10);
putchar('0' + cnt % 10);
}
putchar('\n'); // 换行属于“行”,所以在外层
}复杂度:内层共执行 Σ(n−i+1) = n(n+1)/2 次,n ≤ 13 时是 91 次;输出字符数 182。这个规模怎么写都不会超时,所以图形题的时间全花在“公式对不对”上,而不是“快不快”上。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
int cnt = 0; // 编号跨行连续,所以放在循环外
for (int i = 1; i <= n; ++i) { // i:第几行
for (int j = 1; j <= n - i + 1; ++j) { // 第 i 行只有 n-i+1 个数
++cnt; // 先加再用:第一个数必须是 1
if (cnt < 10) putchar('0'); // 一位数补前导 0,凑够 2 个字符
if (cnt >= 10) putchar('0' + cnt / 10); // 两位数才打十位
putchar('0' + cnt % 10); // 个位任何情况都要打
}
putchar('\n'); // 换行与内层 for 平级
}
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
#include <bits/stdc++.h> | 万能头,cin/cout 与 putchar 都在里面,竞赛标准写法。 |
int cnt = 0; | 初值必须是 0 且写在双层循环外。写进内层 → 每行都从 1 重数,图形变成“每行 0102…”。 |
for (int i = 1; i <= n; ++i) | 外层走 n 次,一次负责一整行。i 只在这里被修改。 |
j <= n - i + 1 | 内层上界随 i 变:i=1 时打 n 个,i=n 时打 1 个。“每行少一个”全靠这一处。 |
++cnt; | 先自增再输出。cnt 的初值 0 让第一个数是 1。 |
if (cnt < 10) putchar('0'); | 补前导 0。这里不能写 else,因为下面还有两句要执行。 |
if (cnt >= 10) putchar('0' + cnt / 10); | 只有两位数才打十位。n ≤ 13 → cnt ≤ 91,两位一定够。 |
putchar('0' + cnt % 10); | '0' + 数字 = 数字字符;cnt % 10 取个位,永远要打。 |
putchar('\n'); | 位置与内层 for 平级:一行结束才换行。放进内层就变成每个数一行。 |
return 0; | 正常结束。评测机看到 0 才认为程序跑完了。 |
所有基础字符图形都是“先空格、后字符”。把这两列的公式背下来,剩下的都是打字工作:
| 图形 | 第 i 行空格数 | 第 i 行字符数 | 内层怎么写 |
|---|---|---|---|
| n×n 方阵 | 0 | n | 一个内层:1..n |
| 左下三角 | 0 | i | 一个内层:1..i |
| 左上三角 | 0 | n − i + 1 | 一个内层:1..n-i+1 |
| 右下三角 | n − i | i | 两个内层:1..n-i,再 1..i |
| 等腰三角形 | n − i | 2i − 1 | 两个内层:1..n-i,再 1..2*i-1 |
| 倒等腰 | i − 1 | 2(n − i) + 1 | 两个内层:1..i-1,再 1..2*(n-i)+1 |
两列数相加就是这一行实际打出的字符总数——这意味着行末绝不能多打一个空格,公式里没有它。同一份代码打三种形状,只改上界:
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; ++i) { // ① 方阵:每行都 n 个
for (int j = 1; j <= n; ++j) putchar('*');
putchar('\n');
}
putchar('\n'); // 图形之间空一行,方便肉眼对比
for (int i = 1; i <= n; ++i) { // ② 左下三角:每行 i 个
for (int j = 1; j <= i; ++j) putchar('*');
putchar('\n');
}
putchar('\n');
for (int i = 1; i <= n; ++i) { // ③ 等腰:先空格,后星号
for (int j = 1; j <= n - i; ++j) putchar(' '); // 空格让星号居中
for (int j = 1; j <= 2 * i - 1; ++j) putchar('*'); // 每行多 2 个
putchar('\n');
}
return 0;
}n = 4 时第 ③ 段的输出是 3 空格+1 星、2 空格+3 星、1 空格+5 星、0 空格+7 星。星号总数 1+3+5+7 = 16 = 4²,这个等式后面数学补给里会推。
图形题的骨架可以脱离“星号”复用:把内层那一刻要输出的东西换掉,就能打任意二维表格。下面打 n×n 乘法表,每格右对齐占 4 列(取 n ≤ 99,保证 i·j ≤ 9801 放得下)。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; ++i) { // 外层:第 i 行
for (int j = 1; j <= n; ++j) { // 内层:第 j 列,共 n² 格
int v = i * j; // 这一格的值
int w = 1, t = v; // w = v 的十进制位数,至少 1 位
while (t >= 10) { t /= 10; ++w; } // 第三层循环:每除一次 10 少一位
for (int k = 1; k <= 4 - w; ++k) putchar(' '); // 右对齐:先补空格
cout << v; // 再打数字本身
}
putchar('\n'); // 换行只在外层
}
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
int w = 1, t = v; | 位数计数器。任何正整数至少有 1 位,所以从 1 起步。 |
while (t >= 10) { t /= 10; ++w; } | 第三层循环,次数 = 位数 − 1,由数据决定而不是由 n 决定。这是“多重循环”里最轻的一种。 |
for (int k = 1; k <= 4 - w; ++k) putchar(' '); | 右对齐的关键:先补齐 4 − w 个空格再打数。w = 4 时循环 0 次,也是对的。 |
cout << v; | 数字交给 cout,不用自己拆位输出。 |
putchar('\n'); | 与内层 for 平级。写进内层 → 每个格子占一行,n² 行。 |
这段代码里 putchar 与 cout 混着用是安全的,因为默认没有关掉 sync_with_stdio,两套流共享同一个缓冲顺序。第 1 节那句“关了同步就别混用”说的正是这个前提——一旦写了 ios::sync_with_stdio(false),就必须全程只用一套。
总工作量 ≈ 4n²。n = 99 时约 4×10⁴,瞬间跑完;把 n 提到 10⁵ 就是 4×10¹⁰,而且输出本身有几百 GB——所以“先把量级算出来”永远排在“先写循环”前面。
① 手画 n = 4 的目标图形,数出每行的空格数与字符数,写成含 i 的表达式。
② 估总量:总字符数(n²、n(n+1)/2、n(n+1) 这些量级)和总循环次数,确认不超时、不超输出限制。
③ 外层 for 行、内层 for 列,换行必须与内层平级。
④ 用 n = 1、n = 2 和最大数据各跑一次,重点看行末有没有多余空格、末尾有没有多余空行。
换行写进内层。症状:WA,输出变成一列竖线(每个字符一行)。修法:把 putchar('\n') 的缩进与内层 for 对齐,它不属于格子。
内层上界忘了跟 i 变。写成 j <= n 得到方阵;写成 j <= i 得到上小下大的三角形。症状:WA,形状不对但“看着很像”,最容易自我怀疑。
行末多一个空格。公式里没有这一格,多打就是错。症状:逐字节比对的 OJ 判 WA 或提示格式错误,部分 OJ 会放过,但别赌。修法:空格只打在字符前面。
补零只补一半。写成 if (cnt < 10) putchar('0'); putchar('0' + cnt); —— 第二句仍然只打一个字符,cnt = 12 时会打出乱码。症状:n ≥ 5 就 WA,n ≤ 4 反而全对。
putchar 用了双引号。putchar("\n") 症状:CE,报 no matching function for call to 'putchar(const char [2])'。
内层上界写成常量造成 TLE。症状:n ≤ 2000 却跑 3 秒以上。三层各 2000 就是 8×10⁹ 次,必须先算 Σ 再写。
把例题不看页面敲一遍,提交 P5721,输入 5,核对输出是否与题面逐字符一致。
把 n - i + 1 改成 i 再提交:图形变成上小下大,但编号依然连续——因为 cnt 从不归零。想清楚为什么编号还对,只有形状错了。
再把外层改成倒序 for (int i = n; i >= 1; --i),先预测输出再运行。预测错了,说明你还没分清“i 的走向”和“图形的走向”。
最后加第三段:n = 4 时输出金字塔,要求 1、3、5、7 个星号。把内层拆成“先打 n−i 个空格,再打 2i−1 个星”,两个 for 各自独立,不要合在一起。
图形里每一行都在重复同一件事,所以“总量”必然是等差求和。先记住这两张脸:
| 第 i 行有几个元素 | 总元素数 | n = 1000 时 | 常见图形 |
|---|---|---|---|
| n(常数) | n² | 10⁶ | 方阵 |
| i | n(n+1)/2 | 5×10⁵ | 左下三角 |
| 2i − 1 | n² | 10⁶ | 等腰三角形 |
| 2(n − i) + 1 | n² | 10⁶ | 倒等腰 |
第二个公式的来历:Σi=1..n(2i − 1) = 1 + 3 + 5 + … + (2n − 1) = n²。前 n 个奇数之和是平方数——所以金字塔的星号总数是 n²,n = 4 时必须打出 16 个星,少了就是公式漏了一行。
还有一个常用反解:给定项数 N,求最小的 k 使 k(k+1)/2 ≥ N。即 k = ⌈(√(8N+1) − 1) / 2⌉。N = 10⁷ 时 k ≈ 4472,所以挑战题里那个 while 最多转 4472 圈,比开数组既快又省内存。
结论:写图形题前先算“总共要输出多少字符”。超过 10⁷ 个字符的图形,出题人一定不会让你全打出来,他会改成问“第 k 行第 m 个是什么”——那时你需要的就不是循环,是公式。
n - i + 1 换成 i,编号仍然连续。为什么?如果题目改成“每行都从 1 重新编号”,代码里该改哪一行、删哪一个变量?i * j,n、m ≤ 2000。这一格最大是多少?用 int 够不够?(先算 2000×2000,再想 int 上限 2.1×10⁹)if 判断“这个位置在不在边界上”?注意:输出字符变少了,循环次数一个都没少——为什么?cout << '*' 和 putchar('*') 差在哪一步?已经写了 ios::sync_with_stdio(false) 为什么还可能 TLE?putchar('0' + d) 为什么能得到数字字符,以及 d = 10 时会打出什么。把全体正有理数排成下面的表,第 r 行第 c 列写的就是 r/c:
1/1 1/2 1/3 1/4 …
2/1 2/2 2/3 …
3/1 3/2 …
4/1 …
再按 Z 字形给表中每一项编号:第 1 项 1/1,第 2 项 1/2,第 3 项 2/1,第 4 项 3/1,第 5 项 2/2,第 6 项 1/3,第 7 项 1/4,……也就是说,编号沿着对角线来回往返,一条一条扫过整张表。
输入格式:一个整数 N(1 ≤ N ≤ 10⁷)。输出格式:一行,形如 a/b,中间不加空格,表示第 N 项。
不要开二维数组。这张表是“斜着数”的,先想清楚第 N 项落在第几条斜线上。
第 k 条斜线上有几项?从第 1 条到第 k−1 条一共有几项?把这两个数写成 k 的式子。
斜线号的奇偶决定方向:奇数号从上方出发往左下走,偶数号从下方出发往右上走。先手写前 10 项并标上斜线号,规律自己会跳出来。
N 高达 10⁷,但斜线号只有约 4472,一个 while 累加就够,不需要数组也不需要 sqrt。
第 k 条斜线(k = 1, 2, 3, …)含 k 项,且每条斜线上分子 + 分母 = k + 1。前 k 条斜线共 T(k) = k(k+1)/2 项。
于是“第 N 项在哪条斜线”等价于求最小的 k 使 T(k) ≥ N。T(k) 严格递增,从 k = 1 往后 while 累加即可;N ≤ 10⁷ 时 k ≤ 4472,循环量可以忽略。
设 before = (k−1)k/2 为前 k−1 条斜线的项数,则第 N 项在第 k 条斜线里是第 t = N − before 个(从 1 数起)。
方向:k 为奇数时该斜线从 k/1 出发、分子递减,故 a = k − t + 1、b = t;k 为偶数时从 1/k 出发、分子递增,故 a = t、b = k − t + 1。
自检:t 从 1 走到 k 时,a + b 恒为 k + 1,且 a、b 都在 [1, k] 内。用前 10 项逐个对,再提交。
#include <bits/stdc++.h>
using namespace std;
int main() {
long long N;
cin >> N;
// 找最小的 k,使 1+2+...+k >= N:第 N 项落在第 k 条斜线上
long long k = 1;
while (k * (k + 1) / 2 < N) ++k;
long long before = (k - 1) * k / 2; // 前 k-1 条斜线一共多少项
long long t = N - before; // N 在第 k 条斜线里是第 t 个(从 1 数)
long long a, b; // 答案 a/b
if (k % 2 == 1) { // 奇数斜线:从 k/1 出发,分子递减
a = k - t + 1;
b = t;
} else { // 偶数斜线:从 1/k 出发,分子递增
a = t;
b = k - t + 1;
}
cout << a << "/" << b << "\n"; // 输出格式是 a/b,不能带空格
return 0;
}| 片段 | 作用 |
|---|---|
while (k * (k + 1) / 2 < N) ++k; | 用三角数定位斜线。循环次数 = k ≈ 4472,代价可忽略;不用 sqrt 是为了躲开浮点误差。 |
(k - 1) * k / 2 | 前 k−1 条斜线的总项数。先乘后除:写成 (k-1)/2*k 会在 k−1 为奇数时丢余数。 |
t = N - before | 把全局编号 N 换成“斜线内部编号”t ∈ [1, k],后面只需管 t。 |
k % 2 == 1 | 方向开关。奇数斜线在表中是往左下走,所以分子随 t 减小。 |
a = k - t + 1; b = t; | t = 1 时得 k/1,t = k 时得 1/k,两个端点都对得上表。 |
cout << a << "/" << b << "\n"; | 用字符 '/' 连接,不加空格。这是本题唯一的格式要求。 |
sqrt 算 k 再反查,浮点误差会让 k 少 1,N 恰好是三角数时整体错位。a = k - t,t = 1 时会得到 (k−1)/1,整体偏一位。a / b(带空格)或 a÷b,格式不符直接 WA。