









2022 年 CCF 非专业级软件能力认证(CSP-J/S 2022)第一轮认证于 2022 年 9 月 18 日举行。继前两篇单项选择题与阅读程序题解析后,本文为您带来 第三部分:完善程序题(共 2 大题,10 小题,每小题 3 分,共计 30 分) 的全题型深度解析。
本次完善程序题聚焦于两大高频核心考点:
vector 逆序输出配对因数。std::queue 的经典二维网格区域填充算法,考查四方向移动、合法性边界判定与原地状态标记。从小到大打印正整数 $n$ 的所有正因数。 试补全枚举程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> fac;
fac.reserve((int)ceil(sqrt(n)));
int i;
for (i = 1; i * i < n; ++i) {
if (①) {
fac.push_back(i);
}
}
for (int k = 0; k < fac.size(); ++k) {
cout << ② << " ";
}
if (③) {
cout << ④ << " ";
}
for (int k = fac.size() - 1; k >= 0; --k) {
cout << ⑤ << " ";
}
return 0;
}
对于任意正整数 $n$,其正因数必然是成对存在的:
如果暴力从 $1$ 循环枚举到 $n$,时间复杂度为 $\mathcal{O}(n)$。通过利用因数的成对性,我们只需枚举到 $\sqrt{n}$,时间复杂度即可大幅降至 $\mathcal{O}(\sqrt{n})$。
程序分为三个阶段完成从小到大升序输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌────────────────────────────────────────────────────────┐
│ ① 循环枚举 i (1 <= i < sqrt(n)) │
│ 收集所有小于 sqrt(n) 的因数存入 vector fac │
└──────────────────────────┬─────────────────────────────┘
▼
┌────────────────────────────────────────────────────────┐
│ ② 正序遍历 fac │
│ 按升序打印前半部分较小因数 (fac[k]) │
└──────────────────────────┬─────────────────────────────┘
▼
┌────────────────────────────────────────────────────────┐
│ ③ 特判 n 是否为完全平方数 │
│ 若 i * i == n,单独打印中间因数 sqrt(n) (即 i) │
└──────────────────────────┬─────────────────────────────┘
▼
┌────────────────────────────────────────────────────────┐
│ ④ 倒序遍历 fac │
│ 按升序打印后半部分配对因数 (n / fac[k]) │
└────────────────────────────────────────────────────────┘
i * i < n,若 n % i == 0,将较小的因数 $i$ 存入 fac。fac 的因数天然呈从小到大的严格升序排列。fac(for (int k = 0; k < fac.size(); ++k)),输出所有小于 $\sqrt{n}$ 的因数(fac[k])。for (i = 1; i * i < n; ++i) 结束后,变量 $i$ 停留在满足 $i^2 \ge n$ 的第一个整数。若 $i^2 = n$,说明 $n$ 是完全平方数,$i$ 本身就是因数且位于正中央,执行 cout << i << " "; 输出。fac 中保存的因数是从小到大的,那么 $\dfrac{n}{\text{fac}[k]}$ 就是从大到小的。fac(从 fac.size() - 1 递减到 0),这样 $\text{fac}[k]$ 越来越小,计算得到的 $\dfrac{n}{\text{fac}[k]}$ 就会从小到大依次输出。A.
n % i == 0
B.n % i == 1
C.n % (i-1) == 0
D.n % (i-1) == 1
正确答案: A
深度解析:
此处用于判断当前的循环变量 $i$ 是否能够整除 $n$(即 $i$ 是否为 $n$ 的正因数)。在 C++ 中整除判断的表达式为 $n$ 对 $i$ 取模结果为 0,即 n % i == 0。故选 A。
A.
n / fac[k]
B.fac[k]
C.fac[k]-1
D.n / (fac[k]-1)
正确答案: B
深度解析:
该循环 for (int k = 0; k < fac.size(); ++k) 从下标 0 开始正序遍历 fac 容器,用于从小到大输出第一阶段收集的所有小于 $\sqrt{n}$ 的因数。这些较小因数直接保存在 fac[k] 中,因此输出 fac[k]。故选 B。
A.
(i-1)*(i-1)== n
B.(i-1)*i == n
C.i*i == n
D.i*(i-1) == n
正确答案: C
深度解析:
由于前面 for 循环的终止条件是 i * i < n,当循环结束时,经过最后一次 ++i 步进,此时 $i$ 恰好为满足 $i^2 \ge n$ 的最小整数:
因此判断 $n$ 是否为完全平方数只需判断 i * i == n。故选 C。
A.
n-i
B.n-i+1
C.i-1
D.i
正确答案: D
深度解析:
当第 21 行 if (i * i == n) 条件成立时,说明 $n$ 是完全平方数,$i$ 即为 $\sqrt{n}$。这个唯一的中间因数应当被打印出来,因此直接输出 i(cout << i << " ";)。故选 D。
A.
n / fac[k]
B.fac[k]
C.fac[k]-1
D.n / (fac[k]-1)
正确答案: A
深度解析:
后半部分因数与 fac 中的前半部分因数一一成对,其值为 $n / \text{fac}[k]$。 因为循环是从 k = fac.size() - 1 倒序递减到 0,fac[k] 依次减小,所以 $n / \text{fac}[k]$ 依次增大,正好实现所有大于 $\sqrt{n}$ 的因数按升序输出。因此 ⑤ 处应填 n / fac[k]。故选 A。
现有用字符标记像素颜色的 $8 \times 8$ 图像。颜色填充的操作描述如下:给定起始像素的位置待填充的颜色,将起始像素和所有可达的像素(可达的定义:经过一次或多次的向上、下、左、右四个方向移动所能到达且终点和路径上所有像素的颜色都与起始像素颜色相同),替换为给定的颜色。 试补全程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <bits/stdc++.h>
using namespace std;
const int ROWS = 8;
const int COLS = 8;
struct Point {
int r, c;
Point(int r, int c): r(r), c(c) {}
};
bool is_valid(char image[ROWS][COLS], Point pt,
int prev_color, int new_color) {
int r = pt.r;
int c = pt.c;
return (0 <= r && r < ROWS && 0 <= c && c < COLS &&
① && image[r][c] != new_color);
}
void flood_fill(char image[ROWS][COLS], Point cur, int new_color) {
queue<Point> queue;
queue.push(cur);
int prev_color = image[cur.r][cur.c];
②;
while (!queue.empty()) {
Point pt = queue.front();
queue.pop();
Point points[4] = {③, Point(pt.r - 1, pt.c),
Point(pt.r, pt.c + 1), Point(pt.r, pt.c - 1)};
for (auto p : points) {
if (is_valid(image, p, prev_color, new_color)) {
④;
⑤;
}
}
}
}
int main() {
char image[ROWS][COLS] = { {'g', 'g', 'g', 'g', 'g', 'g', 'g', 'g'},
{'g', 'g', 'g', 'g', 'g', 'g', 'r', 'r'},
{'g', 'r', 'r', 'g', 'g', 'r', 'g', 'g'},
{'g', 'b', 'b', 'b', 'b', 'r', 'g', 'r'},
{'g', 'g', 'g', 'b', 'b', 'r', 'g', 'r'},
{'g', 'g', 'g', 'b', 'b', 'b', 'b', 'r'},
{'g', 'g', 'g', 'g', 'g', 'b', 'g', 'g'},
{'g', 'g', 'g', 'g', 'g', 'b', 'b', 'g'} };
Point cur(4, 4);
char new_color = 'y';
flood_fill(image, cur, new_color);
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < COLS; c++) {
cout << image[r][c] << ' ';
}
cout << endl;
}
return 0;
}
洪水填充(Flood Fill) 是计算机图形学中最基础的算法之一(如画图软件中的“油漆桶”上色工具)。
new_color;prev_color = image[r][c];prev_color 的连通区域像素全部替换为 new_color。本题采用标准的 BFS 队列遍历 模型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[起点初始化]
• 起点 cur(4, 4) 入队 queue.push(cur)
• 记录原颜色 prev_color,并将起点染色 image[cur] = new_color
│
▼
[BFS 队列循环 while (!queue.empty())]
┌──▶ 1. 取出队头像素 pt = queue.front(); queue.pop();
│ 2. 生成上下左右 4 个相邻点 points[4]
│ 3. 遍历每个相邻点 p:
│ • 若 is_valid(p) 校验合法(在网格内、颜色等于 prev_color、未被染色):
│ - 立即染色:image[p.r][p.c] = new_color
│ - 节点入队:queue.push(p)
└─── 4. 重复循环直至队列为空
│
▼
[填充完毕,输出图像结果]
is_valid:image[r][c] == prev_color;image[r][c] != new_color。pt 的四个相邻点分别为:Point(pt.r - 1, pt.c)Point(pt.r + 1, pt.c)Point(pt.r, pt.c - 1)Point(pt.r, pt.c + 1)new_color;image[r][c] == prev_color(且 image[r][c] == new_color),天然起到了 visited 数组防重入的作用,防止死循环和重复搜索。A.
image[r][c] == prev_color
B.image[r][c] != prev_color
C.image[r][c] == new_color
D.image[r][c] != new_color
正确答案: A
深度解析:
is_valid 函数用于判断坐标 pt 是否属于待填充的连通块。根据题意,可达像素要求“终点和路径上所有像素的颜色都与起始像素颜色相同”。因此该位置当前的颜色必须等于起始像素的原颜色 prev_color,即 image[r][c] == prev_color。故选 A。
A.
image[cur.r+1][cur.c] = new_color
B.image[cur.r][cur.c] = new_color
C.image[cur.r][cur.c+1] = new_color
D.image[cur.r][cur.c] = prev_color
正确答案: B
深度解析:
在 flood_fill 开始时,起始点 cur 已经通过 queue.push(cur) 加入队列,同时记录了原颜色 prev_color。 为了防止起始点在后续邻居向回搜索时被重复访问入队,必须在入队时立即将起始像素的颜色修改为新颜色,即 image[cur.r][cur.c] = new_color。故选 B。
A.
Point(pt.r, pt.c)
B.Point(pt.r, pt.c+1)
C.Point(pt.r+1, pt.c)
D.Point(pt.r+1, pt.c+1)
正确答案: C
深度解析:
代码中定义了包含 4 个相邻方向的数组 points[4],已经明确列出了:
Point(pt.r - 1, pt.c)Point(pt.r, pt.c + 1)Point(pt.r, pt.c - 1)四连通移动中唯一缺少的是向下移动的坐标,即行号加 1(pt.r + 1),列号不变(pt.c),因此 ③ 处应填 Point(pt.r + 1, pt.c)。故选 C。
A.
prev_color = image[p.r][p.c]
B.new_color = image[p.r][p.c]
C.image[p.r][p.c] = prev_color
D.image[p.r][p.c] = new_color
正确答案: D
深度解析:
当相邻像素点 $p$ 经 is_valid 校验合法后,需要将该像素染成目标颜色 new_color,因此执行赋值语句 image[p.r][p.c] = new_color。故选 D。
A.
queue.push(p)
B.queue.push(pt)
C.queue.push(cur)
D.queue.push(Point(ROWS, COLS))
正确答案: A
深度解析:
在 BFS 搜索过程中,对合法的相邻节点 $p$ 完成染色后,必须将该相邻节点 $p$ 压入队列中,以便后续以此点为基准继续向四周扩展搜索。因此 ⑤ 处执行 queue.push(p)。故选 A。
| 大题 | 小题 | 题型 / 考点 | 正确答案 |
|---|---|---|---|
| 三、(1)枚举因数 | 1 | 条件判断(整除判断) | A |
| 2 | 数组访问(升序输出前半部分因数) | B | |
| 3 | 条件判断(完全平方数特判) | C | |
| 4 | 变量输出(输出中间平方根因数) | D | |
| 5 | 配对因数计算(倒序输出后半部分因数) | A | |
| 三、(2)洪水填充 | 1 | 合法性判断(颜色匹配) | A |
| 2 | 起点染色初始化 | B | |
| 3 | 方向构造(向下移动) | C | |
| 4 | 邻居节点染色 | D | |
| 5 | 邻居节点入队 | A |
vector)正序存储前半部分,倒序遍历计算配对项,是无需额外排序即可实现 $\mathcal{O}(\sqrt{n})$ 升序输出的标准模板。{-1, 0}, {1, 0}, {0, -1}, {0, 1})以及边界合法性判断的完整性。所有代码已上传至Github:https://github.com/lihongzheshuai/yummy-code
GESP 学习专题站:GESP WIKI
"luogu-"系列题目可在洛谷题库进行在线评测。
"bcqm-"系列题目可在编程启蒙题库进行在线评测。
欢迎加入:Java、C++、Python技术交流QQ群(982860385),大佬免费带队,有问必答
欢迎加入:C++ GESP/CSP认证学习QQ频道,考试资源总结汇总
欢迎加入:C++ GESP/CSP学习交流QQ群(688906745),考试认证学员交流,互帮互助
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。