GESP C++四级 必背编程真题
GESP C++ 四级上机固定为「客观题 40 分 + 2 道编程大题各 25 分」。编程大题的高频母题集中在 结构体与排序、二维数组与网格、递推、字符串与数位模拟、函数封装,偶尔涉及指针、文件重定向与异常处理。
本章收录 7 道近年真实真题(洛谷 B 系列),每道题按「题目描述 → 输入输出格式 → 样例 → 数据范围 → 参考答案与解析(折叠)」组织,答案与解析紧跟题目、一一对应、默认折叠,背熟即可覆盖绝大多数变形题。文末附选择题/判断题高频坑(选项单独成行)与保命技巧。
41.1 四级编程题高频考点速览
| 考点 | 代表真题 | 必会程度 |
|---|---|---|
| 结构体 + 多关键字排序 | B4502 礼盒排序 | 必背 |
| 二维数组 / 子矩阵 / 网格枚举 | B4360 画布裁剪、B4501 山之谷 | 必背 |
| 字符串 + 进制 / 数位模拟 | B3869 进制转换、B3850 幸运数 | 必背 |
| 递推 + 判重 + 排序 | B4068 Recamán 数列 | 必背 |
| 字符串扫描 + 映射 + 函数 | B3927 小杨的字典 | 必背 |
| 指针 / 文件重定向 / 异常 | 核心模板速查 | 看懂会用 |
41.2 核心模板速查(客观题与小题常考)
指针定义与解引用:
int a = 10;
int* p = &a; // p 保存 a 的地址
cout << *p; // 解引用,输出 10
*p = 20; // 通过指针改 a
指针传递交换(与值传递对比,四级常考):
void swap(int* x, int* y) {
int t = *x; *x = *y; *y = t;
}
// 调用:swap(&a, &b);
二维数组与八方向数组:
int g[105][105];
int dx[8] = {-1, -1, -1, 0, 0, 1, 1, 1};
int dy[8] = {-1, 0, 1, -1, 1, -1, 0, 1};
文件重定向(四级大纲明确要求):
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
// 之后 cin / cout 自动读写文件
异常处理(按类型精确匹配):
try {
throw runtime_error("err");
} catch (const runtime_error& e) {
cout << e.what();
}
三大排序稳定性速记:冒泡稳定、插入稳定、选择不稳定。四级大题一律用 sort 配合 cmp,不要手写冒泡。
41.3 真题实战
真题1 礼盒排序(洛谷 B4502 · 结构体多关键字排序)
题目描述
商店推出许多礼盒,每个礼盒包含 k 件商品,每件商品有一个价格。需要对礼盒排序:
- 先按礼盒总价格从小到大;
- 总价相同,按最贵商品价格从小到大;
- 仍相同,按最便宜商品价格从小到大;
- 仍相同,按礼盒编号从小到大。
输出排序后的礼盒编号(编号从 1 开始)。
输入格式
第一行两个整数 n、k;接下来 n 行,每行 k 个整数(商品价格)。
输出格式
一行 n 个整数,为排序后的礼盒编号,空格分隔。
样例
输入:
4 3
3 5 2
4 1 5
2 2 4
3 4 3
输出:
3 4 2 1
数据范围
1 ≤ n ≤ 1000,1 ≤ k ≤ 10,1 ≤ 商品价格 ≤ 10000。
参考答案与解析(点击展开)
参考代码如下:
#include <iostream>
#include <algorithm>
using namespace std;
struct Box {
int id;
int total;
int mx;
int mi;
} b[1005];
bool cmp(Box a, Box b) {
if (a.total != b.total) return a.total < b.total;
if (a.mx != b.mx) return a.mx < b.mx;
if (a.mi != b.mi) return a.mi < b.mi;
return a.id < b.id;
}
int main() {
int n, k;
cin >> n >> k;
for (int i = 1; i <= n; i++) {
b[i].id = i;
b[i].total = 0;
b[i].mx = 0;
b[i].mi = 100000000;
for (int j = 1; j <= k; j++) {
int x;
cin >> x;
b[i].total += x;
if (x > b[i].mx) b[i].mx = x;
if (x < b[i].mi) b[i].mi = x;
}
}
sort(b + 1, b + n + 1, cmp);
for (int i = 1; i <= n; i++) {
cout << b[i].id;
if (i < n) cout << " ";
}
cout << endl;
return 0;
}
解析要点:
- 把礼盒的四个特征(编号、总价、最贵、最便宜)打包进结构体,用
sort配合自定义cmp。 cmp写法口诀「层层阻击」:当前优先级不相等就立刻用该项决出大小,相等才落到下一项,不要写反顺序。- 最小值初始化要设足够大的数(如 1e8),不要设成 0,否则最小值永远为 0。
- 编号从 1 开始,读入时
b[i].id = i。
真题2 画布裁剪(洛谷 B4360 · 二维数组 / 子矩阵)
题目描述
画布可视为 h 行 w 列的字符矩阵。只保留由第 x1 行到第 x2 行、第 y1 列到第 y2 列构成的子矩阵并输出。
输入格式
第一行两个正整数 h、w;第二行四个正整数 x1、x2、y1、y2;接下来 h 行,每行一个长度为 w 的字符串。
输出格式
共 x2 − x1 + 1 行,每行一个长度为 y2 − y1 + 1 的字符串。
样例
输入:
3 5
2 2 2 4
.....
.>_>
abcde
输出:
>_>
数据范围
行列范围较小,直接二维字符数组存储即可。
参考答案与解析(点击展开)
参考代码如下:
#include <iostream>
#include <string>
using namespace std;
int main() {
int h, w;
cin >> h >> w;
int x1, x2, y1, y2;
cin >> x1 >> x2 >> y1 >> y2;
string a[1005];
for (int i = 1; i <= h; i++) {
cin >> a[i];
}
for (int i = x1; i <= x2; i++) {
for (int j = y1; j <= y2; j++) {
cout << a[i][j - 1]; // 字符串下标从 0 开始
}
cout << endl;
}
return 0;
}
解析要点:
- 字符串下标从 0 开始,题目给的是 1-based 行列号,访问字符要用
a[i][j - 1]。 - 只输出子矩阵范围内的字符,不要输出多余空格或整行。
- 二维数组是四级最核心方向之一,重点练熟「读入 → 下标转换 → 局部输出」。
真题3 进制转换(洛谷 B3869 · 字符串 / 进制 / 递推)
题目描述
给定一个 k 进制数(2 ≤ k ≤ 16,可能含大写字母 A–F),将其转换为十进制并输出。
输入格式
第一行一个整数 k;第二行一个字符串,表示 k 进制数(大写字母 A–F 表示 10–15)。
输出格式
一行一个十进制整数。
样例
输入:
16
FF
输出:
255
数据范围
结果可能较大,用 long long 保存。
参考答案与解析(点击展开)
参考代码如下:
#include <iostream>
#include <string>
using namespace std;
int main() {
int k;
string s;
cin >> k >> s;
long long ans = 0;
for (char c : s) {
int v;
if (c >= '0' && c <= '9') v = c - '0';
else v = c - 'A' + 10;
ans = ans * k + v;
}
cout << ans << endl;
return 0;
}
解析要点:
- 从左到右递推:
ans = ans * k + 当前位值,这是进制转换的通用骨架。 - 字符转数值:数字用
c - '0',字母用c - 'A' + 10。 - 不能把 k 进制数直接当十进制整数读入,必须按字符串逐位处理。
- 结果用
long long,避免溢出。
真题4 幸运数(洛谷 B3850 思路 · 数位模拟 / 函数)
题目描述
输入一个正整数 n(n ≤ 10^9),把它的奇数位(从低位第 1 位起算)上的数字加 1(若加 1 后等于 10 则变为 0),得到新数。判断新数各位数字之和是否能被 7 整除,能则输出 yes,否则输出 no。
说明:具体变换与判倍规则以官方题面为准,本题提供「数位模拟」通用模板,核心是逐位取模、按位修改、再求和判断。
输入格式
一行一个正整数 n。
输出格式
一行 yes 或 no。
样例
输入:
12345
输出:
no
数据范围
n ≤ 10^9,用 long long 或字符串处理。
参考答案与解析(点击展开)
参考代码如下:
#include <iostream>
using namespace std;
int main() {
long long n;
cin >> n;
long long t = n;
long long w = 1; // 当前位权(从低位第 1 位起)
long long sum = 0;
while (t > 0) {
int d = t % 10;
if (w % 2 == 1) { // 奇数位变换
d = d + 1;
if (d == 10) d = 0;
}
sum += d;
t /= 10;
w++;
}
if (sum % 7 == 0) cout << "yes" << endl;
else cout << "no" << endl;
return 0;
}
解析要点:
- 数位模拟三步走:取个位
t % 10→ 处理该位 →t /= 10去掉个位,循环到t == 0。 - 用
w记录当前是第几位,奇偶位用w % 2判断。 - 大数用
long long,不要图省事用int。 - 多次「各位数求和」可封装成函数复用。
真题5 Recamán 数列(洛谷 B4068 · 递推 / 判重 / 排序)
题目描述
Recamán 数列定义:a(0) = 0;对 i ≥ 1,若 a(i-1) - i > 0 且该值未在数列中出现过,则 a(i) = a(i-1) - i,否则 a(i) = a(i-1) + i。输入 n,生成前 n 项并升序输出。
输入格式
一行一个整数 n。
输出格式
一行 n 个整数,为升序排列后的前 n 项,空格分隔。
样例
输入:
5
输出:
0 1 2 3 6
数据范围
n 较小,判重可用布尔数组,最后 sort 输出。
参考答案与解析(点击展开)
参考代码如下:
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
int n;
cin >> n;
int a[1005];
bool used[1000005] = {false};
a[0] = 0;
used[0] = true;
for (int i = 1; i < n; i++) {
int back = a[i - 1] - i;
if (back > 0 && !used[back]) {
a[i] = back;
} else {
a[i] = a[i - 1] + i;
}
used[a[i]] = true;
}
sort(a, a + n);
for (int i = 0; i < n; i++) {
cout << a[i];
if (i < n - 1) cout << " ";
}
cout << endl;
return 0;
}
解析要点:
- 递推不是套公式,而是「用前一项推出后一项」,注意
back必须同时满足「大于 0」和「未出现过」才走减法分支。 - 判重用布尔数组标记已出现的值,避免重复。
- 生成后统一
sort输出,不要边生成边排序。
真题6 山之谷(洛谷 B4501 · 八方向枚举 / 边界)
题目描述
给定一个字符网格,若某个格子「不高于」其八个方向相邻格子(即高度 ≤ 所有在界内的相邻格子),则称其为山谷。统计山谷的数量(边界格子的相邻方向不足八个,按实际在界内的相邻格子比较)。
输入格式
第一行两个整数 h、w;接下来 h 行,每行一个长度为 w 的字符串。
输出格式
一行一个整数,为山谷数量。
样例
输入:
3 3
abc
def
ghi
输出:
1
数据范围
网格规模较小,直接枚举每个格子与其八方向邻居。
参考答案与解析(点击展开)
参考代码如下:
#include <iostream>
#include <string>
using namespace std;
int main() {
int h, w;
cin >> h >> w;
string g[1005];
for (int i = 0; i < h; i++) cin >> g[i];
int dx[8] = {-1, -1, -1, 0, 0, 1, 1, 1};
int dy[8] = {-1, 0, 1, -1, 1, -1, 0, 1};
int cnt = 0;
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
bool valley = true;
for (int k = 0; k < 8; k++) {
int ni = i + dx[k];
int nj = j + dy[k];
if (ni < 0 || ni >= h || nj < 0 || nj >= w) continue;
if (g[i][j] > g[ni][nj]) { valley = false; break; }
}
if (valley) cnt++;
}
}
cout << cnt << endl;
return 0;
}
解析要点:
- 八方向数组
dx/dy是网格题标配,枚举邻居先判断是否越界(ni/nj在[0, h)、[0, w)内)再比较。 - 「不高于」包含相等,所以只要存在比它高的邻居就判非山谷。
- 边界格子的邻居不足八个,越界的直接
continue跳过即可。
真题7 小杨的字典(洛谷 B3927 · 字符串扫描 / 映射 / 函数)
题目描述
先给出一个词典(N 行,每行「英文 中文」),再给出一段由小写单词和标点符号组成的文章。把文章中的每个单词替换为词典中的中文翻译,词典中没有的单词替换为 UNK,非字母字符原样输出。
输入格式
第一行一个整数 N;接下来 N 行每行两个字符串(英文、中文);最后一行是文章(可能含空格)。
输出格式
一行,为替换后的文章。
样例
输入:
2
hello 你好
world 世界
hello, world!
输出:
你好, 世界!
数据范围
单词均由小写字母组成,长度有限。
参考答案与解析(点击展开)
参考代码如下:
#include <iostream>
#include <string>
#include <map>
using namespace std;
int main() {
int n;
cin >> n;
map<string, string> dict;
for (int i = 0; i < n; i++) {
string en, zh;
cin >> en >> zh;
dict[en] = zh;
}
string article;
getline(cin, article); // 读掉词典后的换行
getline(cin, article); // 读文章(可能含空格)
string word = "", out = "";
for (char c : article) {
if (c >= 'a' && c <= 'z') {
word += c;
} else {
if (!word.empty()) {
out += dict.count(word) ? dict[word] : "UNK";
word = "";
}
out += c;
}
}
if (!word.empty()) out += dict.count(word) ? dict[word] : "UNK";
cout << out << endl;
return 0;
}
解析要点:
- 用
map存词典,dict.count(word)判断是否命中,未命中输出UNK。 - 扫描文章时,连续小写字母拼成一个单词,遇到非字母字符先处理已拼好的单词再原样输出该字符。
- 读文章前用一次
getline吃掉词典后的换行,否则会读到一个空行。 - 这是「字符串扫描 + 映射」的标准骨架,函数封装后可读性更好。
41.4 选择题 / 判断题高频坑(选项单独成行,答案折叠)
下面每题的选项各自成行,避免挤在一行;答案与解析折叠在题后。
题目1:下列关于 C++ 指针的说法,正确的是?
- A. 指针变量只能存放 int 类型数据的地址
- B. 对空指针解引用是安全的
- C. 指针保存的是内存地址
- D. 指针不能参与算术运算
答案与解析
答案:C
解析:指针可以存放任意类型数据的地址(A 错);对空指针解引用会导致运行时错误(B 错);指针加减整数按指向类型的大小移动(D 错)。指针本质就是保存内存地址的变量。
题目2:下列关于结构体与排序的说法,正确的是?
- A. 结构体只能保存同一种类型的数据
- B. 多关键字排序必须手写冒泡排序
- C. 可以用
sort配合自定义cmp实现多关键字排序 - D. 结构体不能作为函数参数
答案与解析
答案:C
解析:结构体可包含多种类型成员(A 错);四级大题直接用 sort + cmp 即可,不必手写冒泡(B 错);结构体可作函数参数,建议用 const 引用传参(D 错)。
题目3:下列关于排序算法稳定性的说法,正确的是?
- A. 选择排序是稳定的
- B. 冒泡排序是不稳定的
- C. 插入排序是稳定的
- D. 稳定性指排序速度是否一致
答案与解析
答案:C
解析:稳定性指「相等元素排序后相对次序不变」。冒泡稳定、插入稳定、选择不稳定。口诀:冒泡稳、插入稳、选择不稳。
题目4:下列关于变量作用域的说法,正确的是?
- A. 在函数内定义的局部变量在函数外也能直接使用
- B. 全局变量在整个程序中都可访问
- C. 局部变量会覆盖同名的全局变量,但全局变量的值会被改变
- D. 函数内不能访问全局变量
答案与解析
答案:B
解析:局部变量只在函数内有效(A 错);全局变量在整个程序中可访问(B 对);局部变量与全局变量同名时,函数内优先使用局部变量,且不会改变全局变量的值(C 错);函数内可以访问全局变量(D 错)。
41.5 四级编程题保命技巧
- 下标从 0 还是从 1:题目编号从 1 开始时,结构体里存
id = i,字符串/数组下标从 0 开始,访问第 y 列字符用s[y - 1]。 - 字符与数字互转:数字转字符
c = d + '0';字符转数字d = c - '0';字母进制c - 'A' + 10。 - 多关键字排序:
cmp严格按题目优先级「层层阻击」,顺序写反就全错。 - 数组越界:循环边界用
<或<=要一致,网格枚举先判断是否越界再访问邻居。 - 大数用
long long:数位、进制、阶乘类题目,数据稍大就溢出int。 - 指针传参才能改原值:想在函数里修改外部变量,用指针或引用传参,值传递只改副本。
- 选择排序不稳定:四级只考概念,大题一律用
sort+cmp。 - 复杂度估算:
n ≤ 1000可O(n^2)暴力;n ≤ 10^5需用O(n log n)的sort或哈希;多项式 vs 指数复杂度要会判断。
背熟上面 7 道真题的骨架,遇到四级编程大题基本都是「换输入输出 + 微调判定」,25 分大题稳拿。