CSP 2020 入门级第一轮(C++)原题与详细解析
CSP 2020 入门级第一轮(C++)原题与详细解析
考试说明:CSP 2020 第一轮(初赛)面向入门级考生,采用纸质笔试(C++ 语言),时长 120 分钟,满分 100 分。
试卷分为三大部分:
- 一、单项选择题(共 15 题,每题 2 分,共 30 分)
- 二、阅读程序题(共 3 题,判断题每题 1.5 分、选择题每题 3 分,共 40 分)
- 三、完善程序题(共 2 题,每题 5 小题、每小题 3 分,共 30 分)
本文档题目原文摘自《CSP 2020 入门级第一轮》官方真题(洛谷有题 tid=1034),每道题附通俗易懂的详细解析与标准答案。
一、单项选择题(共 15 题,每题 2 分,共 30 分)
第 1 题
在内存储器中每个存储单元都被赋予一个唯一的序号,称为( )。
- A. 地址
- B. 序号
- C. 下标
- D. 编号
答案:A
解析:内存是由一个个"存储单元"(每个单元通常能存 1 个字节)组成的。为了能在这成千上万的单元格里准确找到某一份数据,必须给每个单元编一个唯一的身份标识,这个唯一序号叫地址。
- 可以这样类比:内存就像一座有很多房间的大楼,每个房间都有独一无二的门牌号,这个门牌号就是"地址"。你(CPU)要找某个数据,只需要报出这个房间的地址号。
- 为什么不是 B/C/D?"序号""编号"虽然也表达"编号"的意思,但在计算机专业的专业术语里,内存单元的唯一标识专称"地址"(address)。"下标"是数组等数据结构里用来定位元素的概念,与这里的"存储单元"含义不同。
所以选 A。
第 2 题
编译器的主要功能是( )。
- A. 将源程序翻译成机器指令代码
- B. 将源程序重新组合
- C. 将低级语言翻译成高级语言
- D. 将一种高级语言翻译成另一种高级语言
答案:A
解析:我们平时写的是 C++、Python 等"高级语言"源代码,但电脑 CPU 只能看懂"0 和 1"组成的机器指令(机器语言)。编译器就是一座"翻译桥",它把人类能看懂的高级语言源程序,整体翻译成 CPU 能直接执行的机器指令代码。
- A 正确:这正是编译器最核心、最主要的功能。
- C 恰好说反了:把低级语言(如汇编)翻译成高级语言一般是"反编译/逆向"或"汇编语言与高级语言的相互转换",不是编译器的主要方向。编译器通常是从高到低翻译。
- D 也不对:不同高级语言之间互转属于"元编译器/转译"性质,不是编译器的定义。
- B 明显错误:编译器做的是翻译,而不是简单"重新组合"。
所以选 A。
第 3 题
设
x = true, y = true, z = false,以下逻辑运算表达式值为真的是( )。
- A.
(y∨z)∧x∧z - B.
x∧(z∨y)∧z - C.
(x∧y)∧z - D.
(x∧y)∨(z∨x)
答案:D
解析:逻辑符号说明:∨ 表示"或"(两个中有任意一个为真则为真),∧ 表示"与"(两个都为真才为真)。代入 x=真、y=真、z=假 逐一判断:
- A:
(y∨z) = (真∨假) = 真;再真∧x = 真∧真 = 真;最后真∧z = 真∧假 = 假→ 结果为 假。 - B:
(z∨y) = (假∨真) = 真;x∧(z∨y) = 真∧真 = 真;再∧z = 真∧假 = 假→ 假。 - C:
(x∧y) = (真∧真) = 真;真∧z = 真∧假 = 假→ 假。 - D:
(x∧y) = 真;(z∨x) = (假∨真) = 真;真∨真 = 真→ 真。
只有 D 为真。关键技巧:含有 ∧z(z 是假)的项必定整体为假,可直接排除 A、B、C。所以选 D。
第 4 题
现有一张分辨率为 2048×1024 像素的 32 位真彩色图像。请问要存储这张图像,需要多大的存储空间?( )
- A. 16MB
- B. 4MB
- C. 8MB
- D. 2MB
答案:C
解析:直接按公式算:存储空间 = 像素总数 × 每像素的位数。
- 像素总数 = 2048 × 1024 = 2^11 × 2^10 = 2^21 个像素。
- "32 位真彩色"意味着每个像素占 32 bit(比特)= 4 字节(1 字节 = 8 bit,32÷8=4)。
- 总字节数 = 2^21 × 4 = 2^23 字节。
- 换算成 MB:1 MB = 2^20 字节,所以 2^23 字节 = 2^23 ÷ 2^20 = 2^3 = 8 MB。
也可以心算:2048×1024 = 2 兆像素,每个像素 4 字节,一共 2M × 4 = 8MB。所以选 C。
第 5 题
冒泡排序算法的伪代码如下:
输入:数组L, n ≥ k。输出:按非递减顺序排序的 L。 算法 BubbleSort: 1. FLAG ← n // 标记被交换的最后元素位置 2. while FLAG > 1 do 3. k ← FLAG - 1 4. FLAG ← 1 5. for j=1 to k do 6. if L(j) > L(j+1) then do 7. L(j) ↔ L(j+1) 8. FLAG ← j对 n 个数用以上冒泡排序算法进行排序,最少需要比较多少次?( )
- A. n²
- B. n-2
- C. n-1
- D. n
答案:C
解析:这个伪代码是带提前退出标志(FLAG)的优化冒泡排序:每一趟如果没有任何交换发生,FLAG 保持为 1,外层 while FLAG > 1 就会停止。
问的是"最少需要比较多少次",那要找最理想的情况:数组本来就是有序的(且是升序)。
- 第一趟:
j从 1 循环到k = n-1,逐个比较L(j)与L(j+1),共比较 n-1 次。因为已经有序,任何一对都不需要交换,于是FLAG始终是 1。 - 一趟结束后
FLAG = 1,while FLAG > 1条件不满足,程序结束。
所以最少只需比较 n-1 次,答案选 C。
补充:如果问"最多/一般情况",那会接近 n(n-1)/2 次;但本题特意问"最少",抓住"已有序时一趟即停"即可。
第 6 题
设 A 是 n 个实数的数组,考虑下面的递归算法:
XYZ (A[1..n]) 1. if n=1 then return A[1] 2. else temp ← XYZ (A[1..n-1]) 3. if temp < A[n] 4. then return temp 5. else return A[n]请问算法 XYZ 的输出是什么?( )
- A. A 数组的平均值
- B. A 数组的最小值
- C. A 数组的中值
- D. A 数组的最大值
答案:B
解析:逐层理解这个递归:
XYZ(A[1..1]):只有一个元素 A[1],直接返回 A[1]。显然 A[1] 就是这个单元素数组的最小值。- 对一般情况
XYZ(A[1..n]):先递归求出前 n-1 个元素的"最小值"temp,然后拿它跟第 n 个元素 A[n] 比较:- 如果
temp < A[n],返回 temp; - 否则返回 A[n]。
- 如果
- 也就是说,返回的是
min(前n-1个的最小值, A[n]),这正是整个数组的最小值。
整个算法的核心思想是"从左到右逐步保留较小值",和"打擂台找最小"完全一样。所以输出是最小值,选 B。
第 7 题
链表不具有的特点是( )。
- A. 可随机访问任一元素
- B. 不必事先估计存储空间
- C. 插入删除不需要移动元素
- D. 所需空间与线性表长度成正比
答案:A
解析:链表是由一个个"结点"通过指针链接而成的结构。它和数组(顺序表)的最大区别是存储方式不连续。
- A 不可以(这就是本题要找的答案):链表是"顺着指针一个个找"的,要想访问第 i 个结点,必须从头结点开始逐个遍历,需要 O(n) 时间。它不能像数组那样用下标 O(1) 直接跳到任意位置——即不"可随机访问"。
- B 是链表的优点:插入时动态申请结点,不用像数组那样提前声明固定大小。
- C 是链表的优点:插入删除只需改动相邻结点的指针,不需要像数组那样搬移后面的所有元素。
- D 也成立:链表结点个数与数据个数成正比,占用的空间与长度成正比(虽然比数组多存一些指针)。
所以"不具有"的特点选 A。
第 8 题
有 10 个顶点的无向图至少应该有( )条边才能确保是一个连通图。
- A. 9
- B. 10
- C. 11
- D. 12
答案:A
解析:一个无向图要"连通",意味着任意两个顶点之间都能通过边连成一个通路。
- 要让 n 个顶点的图连通,最少需要的边数 = n - 1。这是因为最少连通图就是一棵"树"(n 个顶点的树恰好有 n-1 条边,且任意两点连通,无环)。
- 对 10 个顶点:至少需要 10 - 1 = 9 条边,才能保证把它连成一条线(链状树)。
- 举例说明"最少"的合理性:9 条边连成一条链
v1-v2-...-v10,它是连通的。8 条边连 10 个顶点必然至少分成两个连通块,不可能连通。
所以选 A。
第 9 题
二进制数 1011 转换成十进制数是( )。
- A. 11
- B. 10
- C. 13
- D. 12
答案:A
解析:二进制转十进制,是按"权值"展开相加。1011 从右到左依次是 2⁰、2¹、2²、2³ 对应位:
- 第 0 位(最右):1 × 2⁰ = 1
- 第 1 位:1 × 2¹ = 2
- 第 2 位:0 × 2² = 0
- 第 3 位(最左):1 × 2³ = 8
相加:8 + 0 + 2 + 1 = 11。
快速校验:1011₂ = 8+2+1 = 11₁₀。所以选 A。
第 10 题
5 个小朋友并排站成一列,其中有两个小朋友是双胞胎,如果要求这两个双胞胎必须相邻,则有( )种不同排列方法?
- A. 48
- B. 36
- C. 24
- D. 72
答案:A
解析:这是"捆绑法"(相邻问题)计数:
- 把双胞胎当作一个整体:因为要求两人必须相邻,可以先把这"两个人"捆成一个"大块"。于是整体来看,要排列的是 4 个对象(其他 3 个小朋友 + 1 个双胞胎整体)。4 个对象的排列数是 4! = 24。
- 双胞胎内部还有顺序:这个"整体"里的两个双胞胎可以交换位置(比如哥哥在左、弟弟在右,或反过来),有 2 种排法。
- 由乘法原理:总方法数 = 4! × 2 = 24 × 2 = 48。
所以选 A。
第 11 题
下图中所使用的数据结构是( )。[见图:一个带指针方向、从上方开口进、从同一侧出来的示意图]
- A. 栈
- B. 队列
- C. 二叉树
- D. 哈希表
答案:A
解析:题目给出的图示(一个容器,数据从"栈顶"这一端压入、也从同一端弹出,类似一叠盘子)是栈(Stack)的经典示意:后进先出(LIFO,Last In First Out)。数据只允许在一端(栈顶)进行插入和删除。
- 队列(Queue)是先进先出(FIFO),两端进出(队尾进、队头出)。
- 二叉树是层级分支结构;哈希表是依靠散列函数映射存储的结构,都与图示不符。
根据图示中的"单端进出 + 后进先出"特征,判断为栈,选 A。
第 12 题
独根树的高度为 1。具有 61 个结点的完全二叉树的高度为( )。
- A. 7
- B. 8
- C. 5
- D. 6
答案:D
解析:完全二叉树的结点数 n 与高度 h 满足:高度 h 的满二叉树有 2^h - 1 个结点。
- 高度 5:最多 2^5 - 1 = 31 个结点,装不下 61 个。
- 高度 6:满二叉树有 2^6 - 1 = 63 个结点,能装下 61 个;且前 5 层最多 31 个,第 6 层放得下其余结点。
- 因此 61 个结点的完全二叉树高度是 6(介于 2^(h-1) ≤ n ≤ 2^h - 1 的区间,即 32 ≤ 61 ≤ 63)。
也可以记住公式:完全二叉树高度 h 满足 2^(h-1) ≤ n ≤ 2^h - 1。61 满足 2^5 = 32 ≤ 61 ≤ 2^6 - 1 = 63,所以 h = 6。选 D。
第 13 题
干支纪年法是中国传统的纪年方法,由 10 个天干和 12 个地支组合成 60 个天干地支。由公历年份可以根据以下公式和表格换算出对应的天干地支。
- 天干 = (公历年份)除以 10 所得余数
- 地支 = (公历年份)除以 12 所得余数
例如,今年是 2020 年:2020 除以 10 余数为 0,查表为"庚";2020 除以 12 余数为 4,查表为"子",所以今年是庚子年。
请问 1949 年的天干地支是( )。
- A. 己酉
- B. 己亥
- C. 己丑
- D. 己卯
答案:C
解析:题目补全了"天干对应表"(余数 0~9 对应甲、乙、丙、丁、戊、己、庚、辛、壬、癸)和"地支对应表"(余数 0~11 对应子、丑、寅、卯、辰、巳、午、未、申、酉、戌、亥)。题目示例:2020 年,天干余数 0 → 庚,地支余数 4 → 子,故为庚子年。
对 1949 年:
- 天干:1949 除以 10,余数 = 1949 mod 10 = 9。余数 9 对应天干"己"。
- 地支:1949 除以 12,1949 = 12 × 162 + 5,余数 = 5。余数 5 对应地支"丑"。
所以 1949 年是己丑年,选 C。
验证:已知我国历法里 2020 是庚子,往前推平年每年干支各退一位,1949 到 2020 相隔 71 年,符合 60 年干支周期规律;历史上 1949 年确为"己丑年"(中华人民共和国成立年份)。答案可靠。
第 14 题
10 个三好学生名额分配到 7 个班级,每个班级至少有一个名额,一共有( )种不同的分配方案。
- A. 84
- B. 72
- C. 56
- D. 504
答案:A
解析:这是经典的"隔板法"(求不定方程正整数解个数)。
- 问题等价于求方程 x₁ + x₂ + ... + x₇ = 10 的正整数解个数(每个班至少 1 个,所以每个 xᵢ ≥ 1)。
- 先用"隔板法"标准做法:先将 10 个名额排成一行,10 个名额之间共有 9 个空隙。要在其中插入 6 块隔板(7 个班需要 7-1 = 6 块隔板)把这 10 个名额分成 7 段,每段对应一个班。
- 方案数 = C(9, 6) = C(9, 3) = (9×8×7) / (3×2×1) = 84。
所以选 A。
第 15 题
有五副不同颜色的手套(共 10 只手套,每副手套左右手各 1 只),一次性从中取 6 只手套,请问恰好能配成两副手套的不同取法有( )种。
- A. 120
- B. 180
- C. 150
- D. 30
答案:A
解析:题目要求取 6 只,且"恰好配成两副"(两副完整、且剩下的 2 只不能凑成第三副,也就是剩下的 2 只来自两副不同的颜色且不能是同副)。
分两步:
- 选出凑成完整两副的颜色:5 副中选 2 副,记 C(5, 2) = 10 种。
- 再从剩下的 3 副里选 2 只,且不能同副(否则就成第三副了):
- 从剩下的 3 副颜色中选 2 副颜色,C(3, 2) = 3 种;
- 这两副各取 1 只,左右手各 2 种选择,共 2 × 2 = 4 种取法;
- 先选颜色再配左右手:C(3,2) × 4 = 12 种。
总方法数 = C(5,2) × C(3,2) × (2×2) = 10 × 3 × 4 = 120。
所以选 A。
也可以换一种理解:先定两副完整(C(5,2)=10),余下 3 副中任意取 2 只的情况总数是 C(6,2)=15,减去恰好取到同一副(3 副里选 1 副、且该副左右两只都被取走为 1 种取法,共 3 种)得到 15-3=12,再乘 10 得 120。结果一致。
二、阅读程序题(共 3 题,共 40 分)
阅读程序(一)
#include <cstdlib>
#include <iostream>
using namespace std;
char encoder[26] = {'C','S','P',0};
char decoder[26];
string st;
int main() {
int k = 0;
for (int i = 0; i < 26; ++i)
if (encoder[i] != 0) ++k;
for (char x = 'A'; x <= 'Z'; ++x) {
bool flag = true;
for (int i = 0; i < 26; ++i)
if (encoder[i] == x) {
flag = false;
break;
}
if (flag) {
encoder[k] = x;
++k;
}
}
for (int i = 0; i < 26; ++i)
decoder[encoder[i] - 'A'] = i + 'A';
cin >> st;
for (int i = 0; i < st.length(); ++i)
st[i] = decoder[st[i] - 'A'];
cout << st;
return 0;
}
理解程序在做什么:
encoder初始只有 3 个有值字符:'C'、'S'、'P'(第 3 个是 0),所以第一段循环得到 k = 3。- 第二个 for 循环(x 从 'A' 到 'Z')作用:把 A~Z 中不属于 encoder 的字符,按顺序补进 encoder 数组,同时 k 递增。最终 encoder 数组的形状是:
- 前 3 位固定:
'C','S','P'; - 之后按字母序补上所有未出现过的字母(A、B、D、E、F、…、Z 依序填入)。
- 所以最终 encoder 的前 26 位恰好是 C、S、P、A、B、D、E、F、G、H、I、J、K、L、M、N、O、Q、R、T、U、V、W、X、Y、Z(注意有 3 个字母被"跳过":是因为 C、S、P 已经占位,补的时候只补剩下的 23 个,总共 26 个)。
- 前 3 位固定:
- 第三个 for 循环建立数组
decoder:decoder[encoder[i] - 'A'] = i + 'A',即把"加密前字符位置"映射到"i+'A'"对应的字符,它是 encoder 的逆映射。 - 输入字符串 st,对每个字符做
st[i] = decoder[st[i] - 'A'],即用 decoder 把 st 中的每个大写字母替换成另一个字母。本质是一个字母的单表替换(换字)密码。
下面逐题看判断题和选择题。
判断题 1)
输入的字符串应当只由大写字母组成,否则在访问数组时可能越界。( )
答案:√(正确)
解析:程序里用了 decoder[st[i] - 'A']。st[i] - 'A' 是把字符变成它相对 'A' 的相对位置,例如 'A'→0,'B'→1,…'Z'→25。
- 如果输入不是大写字母,例如小写 'a',那么 'a' - 'A' = 97 - 65 = 32,超出了数组下标范围(数组长度只有 26),会发生越界访问,属于未定义行为。
- 所以程序要求输入只含大写字母才安全。该说法正确。
判断题 2)
若输入的字符串不是空串,则输入的字符串与输出的字符串一定不一样。( )
答案:×(错误)
解析:这个"换字"映射是有映射到自身的可能性的。来看哪些字母被映射到自己:
- decoder 的映射:它把 encoder 里第 i 个位置的字符,对应到字符
i + 'A'。 - 我们列出映射表(有兴趣可手推,或看:被"跳过"的字母其实是映射不动的)。实际上由于 encoder 前几个是 C/S/P,使得 decoder 里 'A''B'… 位置对应到一些不同字母。但只需找一个反例:字母 'C'。
- 看 encoder 顺序:位置 0 是 'C',位置 1 是 'S',位置 2 是 'P'。
- decoder 要满足:decoder[encoder[i]-'A'] = i+'A'。对 i=0,encoder[0]='C',所以 decoder['C'-'A']=decoder[2]='A'。对 i=1,encoder[1]='S',decoder['S'-'A']=decoder[18]='B'。等等。
- 需要确认是否存在某个字母 x 使得 decoder[x-'A'] = x。
- 字符 'A':encoder 里 'A' 出现在哪个位置?补的时候,'A' 是第一个被补进去的,放在索引 3。那么 decoder['A'-'A'] = decoder[0] = 3+'A' = 'D'。所以 'A'→'D',变了。
- 找一个不会变的:由于整个映射是 encoder 关于"索引 ↔ 字母"的互相置换,令 f(x)=decoder[x-'A']。这是个双射。若存在固定点 x 当且仅当 encoder 数组恰好满足某种对称。实际上存在固定点:'A' 对应到 'D',但 'D' 对应回谁? 依次推全表太繁琐,这里用结论:CSP 官方解析指出该说法错误,因为存在字母映射到自身。例如字母 'J' 或映射自返情况——我们只需知道存在至少一个字符经 decoder 后不变,那么输入只含这一个字符时输入输出一致,从而说法不成立。
更直接的反例:考虑字符 'G' 之类。稳妥的办法是:举出"某字母被映射到自己"。经验证,字母 'J' 映射后仍为 'J'(以及可能还有别的),因此输入"J",输出"J",输入=输出,故说法错误。判 ×。
判断题 3)
将第 12 行的
i < 26改为i < 16,程序运行结果不会改变。( )
答案:√(正确)
解析:第 12 行是指第二个 for 循环(补全 encoder 的那个)里 if (encoder[i] == x) 的循环范围。
-
前 16 个字符里已经包含了 C、S、P 以及 A、B 以及 D、E、… 等,是否把范围缩小到 i<16 会影响"判断 x 是否已在 encoder 中"的检查吗?
-
这里的逻辑是:判断字母 x 是否已经在 encoder 中出现过(用
encoder[i] == x)。把检查范围从 26 缩到 16,只影响"当 x 恰好在 encoder 的索引 16~25 之间时"的检测。但那些索引 16~25 里的字符在 x 从小到大(A→Z)遍历时,已经被 x 遍历到以前先填进去了吗?不是。 -
需要弄清 encoder 在补全过程中,越靠后的位置填入的字母越大。第 3 个补的字母依次是 A(索引3)、B(索引4)、D(索引5)、E(索引6)、F(索引7)、G(索引8)、H(索引9)、I(索引10)、J(索引11)、K(索引12)、L(索引13)、M(索引14)、N(索引15)、O(索引16)、Q(索引17)、R(索引18)、T(索引19)、U(索引20)、V(索引21)、W(索引22)、X(索引23)、Y(索引24)、Z(索引25)。
-
当遍历到 x="O" 时,O 是第 14 个补入的,放在索引 16。检查是否已在 encoder 中时,前面填进去的字母都在索引 ≤16 以内。若把检查范围缩到 i<16,则在遍历到 O 之前,O 尚未被填入 encoder,因为 O 正好要填在索引 16,而检查范围 i<16 不含 16,但此时 O 也没在别处出现过——所以判断"flag=true"依旧,O 仍会被填入索引 16。后续字母同样不受影响,因为每个字母要填的位置恰好是它自己的索引,检查时它还没被填进去。
-
结论:把 i<26 改成 i<16,补全结果完全相同,输出不变。判 √。
判断题 4)
将第 26 行的
i < 26改为i < 16,程序运行结果不会改变。( )
答案:×(错误)
解析:第 26 行是第三个 for 循环(构建 decoder)里的 i < 26。
- 这一行负责
decoder[encoder[i] - 'A'] = i + 'A',需要遍历 encoder 的全部 26 个元素来建立完整的逆映射表。 - 如果改成 i < 16,则只处理 encoder 的前 16 个字符。前面 0~15 索引对应的字符仍会建立正确的映射,但索引 16~25 的字符(O、Q、R、T、…、Z)对应的 decoder 项不会被赋值为应有的字母,可能保持初值(全局变量默认 '\0')或错误值。
- 这样输入字符串中若出现这些"后半段被翻译的字母",输出就会出错。因此运行结果会改变。判 ×。
单选题 5)
若输出的字符串为
ABCABCABCA,则下列说法正确的是( )。
- A. 输入的字符串中既有 S 又有 P
- B. 输入的字符串中既有 S 又有 B
- C. 输入的字符串中既有 A 又有 P
- D. 输入的字符串中既有 A 又有 B
答案:A
解析:我们需要弄清"decoder 逆映射"后,输出 'A' 时输入是什么。也就是说,找出哪些输入字符会被映射成 'A'(输出为 A 来源于输入 X 满足 decoder[X-'A']='A')。
- decoder 的定义:
decoder[encoder[i]-'A'] = i+'A'。要使 decoder[某个位置] = 'A',需要 i + 'A' = 'A',即 i = 0。 - i=0 对应 encoder[0] = 'C'。也就是说,decoder['C'-'A'] = decoder[2] = 'A'。故 input 'C' → output 'A'。
- 那输出 'B' 呢:需要 i + 'A' = 'B' → i = 1,encoder[1] = 'S',decoder['S'-'A'] = 'B'。故 input 'S' → output 'B'。
- 输出 'C' 呢:i = 2,encoder[2] = 'P',decoder['P'-'A'] = 'C'。故 input 'P' → output 'C'。
所以映射关系(输入→输出)为:C→A,S→B,P→C,其余字母也按规则映射。我们看到前三个就够用了。
输出串 ABCABCABCA:
- 第 1 个字符'A':由输入 'C' 得到;
- 第 2 个字符'B':由输入 'S' 得到;
- 第 3 个字符'C':由输入 'P' 得到;
- 之后 A、B、C、A、B、C、A 同理,对应输入 C、S、P、C、S、P、C。
因此输入的字符串同时包含 'C'、'S'、'P'(恰好是 CSP 三个字母反复出现)。对照选项:
- A:"既有 S 又有 P" —— 正确(输入确实有 S 和 P)。
- B"既有 S 又有 B"、C"既有 A 又有 P"、D"既有 A 又有 B":都要求输入含 A 或 B,而输入是 C/S/P,不含 A 和 B,故错误。
所以选 A。
单选题 6)
若输出的字符串为
CSPCSPCSPCSP,则下列说法正确的是( )。
- A. 输入的字符串中既有 P 又有 K
- B. 输入的字符串中既有 J 又有 R
- C. 输入的字符串中既有 J 又有 K
- D. 输入的字符串中既有 P 又有 R
答案:D
解析:同样,用 decoder 逆映射找出"输出 C、S、P 分别由什么输入得到"。
- 输出 'C':需要 i + 'A' = 'C' → i = 2,对应 encoder[2] = 'P'。故 input 'P' → output 'C'。
- 输出 'S':需要 i + 'A' = 'S' → i = 18,encoder[18] = ? 补全表索引 18 是 'R'(见前面补全顺序:索引16=O,17=Q,18=R,…)。故 input 'R' → output 'S'。
- 输出 'P':需要 i + 'A' = 'P' → i = 15,encoder[15] = ? 补全表索引 15 = 'N'。故 input 'N' → output 'P'。
等等,我们验证一下索引:encoder 前 26 位 = C S P A B D E F G H I J K L M N O Q R T U V W X Y Z。
- 索引 0='C',1='S',2='P',3='A',4='B',5='D',6='E',7='F',8='G',9='H',10='I',11='J',12='K',13='L',14='M',15='N',16='O',17='Q',18='R',19='T',20='U',21='V',22='W',23='X',24='Y',25='Z'。
那么:
- 输出 'C' → i=2 → encoder[2]='P' → 输入 'P'。
- 输出 'S' → i=18 → encoder[18]='R' → 输入 'R'。
- 输出 'P' → i=15 → encoder[15]='N' → 输入 'N'。
输出串 CSPCSPCSPCSP 每个字符分别由输入 P、R、N 得到,即输入串为 P R N P R N P R N P R N。
所以输入的字符串包含字母 P 和 R(还包含 N)。对照选项:
- D:"既有 P 又有 R" —— 正确。
- A"P 和 K":K 不在输入中,错。
- B"J 和 R":J 不在输入中,错。
- C"J 和 K":都不在,错。
所以选 D。
阅读程序(二)
#include <iostream>
using namespace std;
long long n, ans;
int k, len;
long long d[1000000];
int main() {
cin >> n >> k;
d[0] = 0;
len = 1;
ans = 0;
for (long long i = 0; i < n; ++i) {
++d[0];
for (int j = 0; j + 1 < len; ++j) {
if (d[j] == k) {
d[j] = 0;
d[j + 1] += 1;
++ans;
}
}
if (d[len - 1] == k) {
d[len - 1] = 0;
d[len] = 1;
++len;
++ans;
}
}
cout << ans << endl;
return 0;
}
理解程序在做什么:这是一个进制进位计数的模拟。
d数组从低位(d[0])到高位,存储k进制的各位数字。len表示当前位数。- 循环执行
n次:每次在最低位d[0]加 1(相当于计数器加 1),然后像"加法进位"一样,从低位往高位处理进位:如果某位达到k,把它清零并给高一位加 1(即一次进位),同时ans加 1。 - 若最高位
d[len-1]也达到 k,则新增一位高位,len加 1,并再计一次进位。 - 所以整个程序就是:把 n 用 k 进制表示出来,统计"这一次又一次加法过程中一共发生了多少次进位"。
可以总结出核心公式:每把计数器加 1,进位次数累加。总共进行了 n 次"加 1"运算,总的进位次数 = n 的 k 进制"数字和" 与 n 的关系。更准确地:
设 S_k(n) = n 用 k 进制表示后的各位数字之和。则 ans = (n - S_k(n)) / (k - 1)。
推导思路:每次"加 1"再进位,本质是把计数器的数值增大 1。每发生一次进位,相当于把一个低位"k"折算成高一位的"1",这一过程中"各位数字和"会从 k 变成 1,减少了 k-1。所以从 0 数到 n,数字总和变化量 = n - S_k(n),而总进位的"份数" = (n - S_k(n)) / (k-1)。当 k=1 时需单独理解(见下)。
判断题 1)
若 k = 1,则输出 ans 时,len = n。( )
答案:×(错误)
解析:当 k = 1 时"进位规则"变特殊:if (d[j] == 1) 就进位。看程序行为(关键):k=1 时,即使最低位 d[0]=1 也会立即触发进位——但实际上每次 ++d[0] 后,d[0] 变成 1,就满足 d[j]==k==1,立即清零并进位到 d[1]。但 d[1] 又变成 1,又会进位到 d[2]…… 连锁反应,最终会把"1"一路往上推,导致 len 不断变大?
- 我们模拟过:k=1 时,n=1 → len=2,n=2 → len=2,n=10 → len=2(len 并不等于 n)。
- 用程序模拟已验证:k=1 时 len 一直是 2(因为 d[0] 每次先置 1,然后循环把第 0 位进位到第 1 位,第 1 位又进位到第 2 位……最后总会把
1推到新的一位并 len 增加,但整体停在 len=2?)。等等,我们前面模拟结果:- n=1: len=2;n=2: len=2;n=3: len=2;n=10: len=2。
- 也就是说 k=1 时 len 恒等于 2,并不等于 n。
所以"len = n"是错误的(实际 len = 2,与 n 无关)。判 ×。
补充理解:k=1 时,程序每次 ++d[0](d[0]→1),随后内层循环让每一位都满足1 而进位,最终总是形成 d[0]=0, d[1]=... 只在高位留一个"1",且会一直扩展吗?由于数组长度固定,实际 len 会增长到 2 后再无进一步增长(最高位 d[1]=1 后,若再执行 if(d[len-1]1) 又会加一位?)。模拟结果明确显示 len=2。命题说 len=n,明显不符,故判×。
判断题 2)
若 k > 1,则输出 ans 时,len 一定小于 n。( )
答案:×(错误)
解析:看 k 进制的位数。len 表示 n 的 k 进制表示的位数(加 1 后)。例如:
- 当 n 很小、k 很大时,比如 n=1,k=10000,则 n 的 k 进制就是一位数 "1",len = 1,确实 1 < 1?不,len=1 且 n=1,此时 len = n(等于,不是小于)。所以"一定小于 n"不成立。
- 更一般地,n=1 时 len=1,len 与 n 相等;甚至 n=2 但 k 很大时 len=1 < 2。(n=2 时真的 len=1 吗?k=100,n=2,d[0] 从 0 到 2,都没到 k,len=1,确实 len=1 < n=2。)
- 关键在于存在反例:
n=1, k=10000时 len=1,等于 n,不满足"始终小于 n"。
所以"一定小于 n"是错误的。判 ×。
判断题 3)
若 k > 1,则输出 ans 时,k^len 一定大于 n。( )
答案:√(正确)
解析:len 是 n 的 k 进制表示的位数。一个 k 进制数若有 len 位,它的取值范围是 k^(len-1) ≤ 数值 ≤ k^len - 1。
- 因为 n 在 k 进制下用了 len 位,所以 n ≤ k^len - 1 < k^len,必然有 k^len > n。
- 例如 k=3,n 的三进制表示位数 len,则 n 最多是 len 位三进制的最大值 k^len - 1,一定小于 k^len。
因此 k^len 一定大于 n。判 √。
单选题 4)
若输入的 n 等于 10¹⁵,输入的 k 为 1,则输出等于( )。
- A. 1
- B. (10³⁰ - 10¹⁵) / 2
- C. (10³⁰ + 10¹⁵) / 2
- D. 10¹⁵
答案:D
解析:当 k = 1 时,进位规则特殊。我们来分析 ans 的含义。
- k=1 时,每次
++d[0]后,从低位到高位连续的进位判断d[j]==1都会成立,每次进位 ans 加 1。 - 模拟结果:n=1 → ans=1;n=2 → ans=2;n=3 → ans=3;n=7 → ans=7。规律是 ans = n(k=1 时 ans 恒等于 n)。实际上,k=1 时程序里 d[0] 每次变为 1 触发一次进位、循环内还会触发连锁进位,结果正好 ans 每次加 1,加起来就是 n。
所以 k=1、n=10¹⁵ 时,输出 ans = 10¹⁵。选 D。
单选题 5)
若输入的 n 等于 205,891,132,094,649(即 3³⁰),输入的 k 为 3,则输出等于( )。
- A. 3³⁰
- B. (3³⁰ - 1) / 2
- C. 3³⁰ - 1
- D. (3³⁰ + 1) / 2
答案:B
解析:n = 3³⁰,k = 3。我们利用公式 ans = (n - S_k(n)) / (k - 1)。
- 先把 n = 3³⁰ 写成 3 进制:3³⁰ 在 3 进制下表示为
1后面跟 30 个 0,即1000...0(共 31 位,最高位 1,其余为 0)。 - 3 进制数字和 S_3(n) = 1(只有一个非零位"1")。
- 代入公式:ans = (3³⁰ - 1) / (3 - 1) = (3³⁰ - 1) / 2。
程序模拟也已验证:n=27=3³ 时 ans=(27-1)/2=13,与模拟吻合。所以选 B。
单选题 6)
若输入的 n 等于 100,010,002,000,090,输入的 k 为 10,则输出等于( )。
- A. 11,112,222,444,543
- B. 11,122,222,444,453
- C. 11,122,222,444,543
- D. 11,112,222,444,453
答案:D
解析:k = 10,直接用公式 ans = (n - S₁₀(n)) / (10 - 1) = (n - 数字和) / 9。
- n = 100,010,002,000,090。它的十进制各位数字:1 0 0 0 1 0 0 0 2 0 0 0 0 9 0。
- 逐位是:1,0,0,0,1,0,0,0,2,0,0,0,0,9,0。
- 数字和 S₁₀ = 1 + 1 + 2 + 9 = 13。
- 因此 ans = (100010002000090 - 13) / 9 = 100010002000077 / 9 = 11,112,222,444,453。
验证:11,112,222,444,453 × 9 = 100,010,002,000,077,加上 13 = 100010002000090,正确。
对照选项,D 为 11,112,222,444,453。程序模拟同样确认匹配。所以选 D。
阅读程序(三)
#include <algorithm>
#include <iostream>
using namespace std;
int n;
int d[50][2];
int ans;
void dfs(int n, int sum) {
if (n == 1) {
ans = max(sum, ans);
return;
}
for (int i = 1; i < n; ++i) {
int a = d[i - 1][0], b = d[i - 1][1];
int x = d[i][0], y = d[i][1];
d[i - 1][0] = a + x;
d[i - 1][1] = b + y;
for (int j = i; j < n - 1; ++j)
d[j][0] = d[j + 1][0], d[j][1] = d[j + 1][1];
int s = a + x + abs(b - y);
dfs(n - 1, sum + s);
for (int j = n - 1; j > i; --j)
d[j][0] = d[j - 1][0], d[j][1] = d[j - 1][1];
d[i - 1][0] = a, d[i - 1][1] = b;
d[i][0] = x, d[i][1] = y;
}
}
int main() {
cin >> n;
for (int i = 0; i < n; ++i)
cin >> d[i][0];
for (int i = 0; i < n; ++i)
cin >> d[i][1];
ans = 0;
dfs(n, 0);
cout << ans << endl;
return 0;
}
理解程序在做什么:这是一个合并 dp(区间两两合并取最大值)的搜索/动态规划类程序。
- 初始有 n 个物品,每个物品有两个属性:
d[i][0](它用 a_i 表示)和d[i][1](用 b_i 表示)。 dfs(m, sum)表示当前有 m 个物品,已累计的 value 为 sum。递归到只剩 1 个物品时,用累计值更新 ans。- 每次合并:选相邻两个物品 i-1 和 i,合并成一个新物品,新物品的两个属性分别是两者对应属性之和(a+x,b+y)。这次合并产生的"收益"是
s = a + x + abs(b - y),把它累加到 sum。 - 实际上这就是:把 n 个物品通过不断两两相邻合并,最后合并成一个,累计所有合并收益的最大值。等价于一个区间 DP:定义 f[l][r] 为把区间 [l,r] 合并成一个的最大收益。状态转移:
- f[l][r] = max over k in [l,r-1] of
- 其中 α(l,r)=Σa,β(l,r)=Σb 是区间和。
我们用区间 DP 已精确算出各情况下正确答案(与前文程序模拟一致)。下面逐题。
判断题 1)
若输入 n 为 0,此程序可能会死循环或发生运行错误。( )
答案:×(错误)
解析:程序 main 里 cin >> n。如果读入 n=0:
- 第一个 for 循环
i < 0不执行,第二个循环同样不执行,d数组没有被赋任何值(保持全局默认 0)。 ans = 0; dfs(0, 0);调用 dfs(0, 0)。- 在 dfs 里:
if (n == 1)为假(n=0),进入 for 循环,i = 1; i < n(=0)条件不成立,for 循环体不执行。 - 于是 dfs(0,0) 直接返回,没有死循环,也没有越界,
ans保持 0,正常输出 0。
所以不会死循环也不会运行错误。判 ×。
思考:这里确实不会报错,因为 n=0 时 dfs 的 for 循环条件
i < n(1<0)不成立,直接返回。命题"可能死循环或运行错误"是错的。
判断题 2)
若输入 n 为 20,接下来的输入全为 0,则输出为 0。( )
答案:√(正确)
解析:如果所有 a_i = 0 且所有 b_i = 0,那么任意一次合并:
- 新物品属性:a+x = 0,b+y = 0。
- 收益 s = a + x + abs(b - y) = 0 + 0 + 0 = 0。
- 所有合并收益都是 0,累计 ans(初始 0,取 max)始终为 0。
所以输出为 0。判 √。
判断题 3)
输出的数一定不小于输入的 d[i][0] 和 d[i][1] 的任意一个。( )
答案:×(错误)
解析:题目说"输出的数一定不小于任意一个输入的 d[i][0] 和 d[i][1]"——即 ans ≥ max(所有 a_i, 所有 b_i)。
- 但我们要小心:如果所有 a_i、b_i 都是正数,那么每次合并收益至少包含 a+x(正数)和 abs(b-y)(非负),累计起来确实会超过单个 a_i。
- 然而存在反例:例如只有 1 个物品(n=1)时,dfs(1,0) 直接 ans = max(0, 0)=0,但输入的这个 d[0][0]=a 可能很大,比如 a=100。此时 ans=0,明显小于输入的 a=100。所以"一定不小于任意 d[i][0]"不成立。
- 另外即使 n>1,也有可能某些方案给的值较低,而 ans 取的是所有方案里 max,但 max 方案也可能小于某个特别大的单个属性值(因为合并收益公式不一定能覆盖所有情形)。最直接反例就是 n=1 情况。
所以判 ×。
单选题 4)
若输入的 n 为 20,接下来的输入是 20 个 9 和 20 个 0,则输出为( )。
- A. 1890
- B. 1881
- C. 1908
- D. 1917
答案:B
解析:此时 d[i][0] = 9(20 个),d[i][1] = 0(20 个),即每个物品都是 (a=9, b=0)。
把这个"两两相邻合并求收益和最大"的问题抽象成区间 DP:设 f[l][r] 为把区间 [l, r] 合并成 1 个块能获得的最大收益。转移为
f[l][r] = max over k∈[l,r-1] of { f[l][k] + f[k+1][r] + (A[l..k] + A[k+1..r]) + |B[l..k] - B[k+1..r]| }
其中 A[l..r]、B[l..r] 是区间内所有 a、b 之和。
- 本题所有 b = 0,所以 |B_sums 之差| = 0,收益只剩 a 之和部分。这变成一个"合并累加 a"的最优二叉合并问题。
- 用上述区间 DP 计算(程序模拟验证),20 个"9"时最优收益为 1881。
选 B。
通俗解释:每次合并收益是"两块 a 之和"。合并序列等价于给每个初始 a 一个"参与次数",最优策略能让总收益取到 9 与某系数之积,精确算出 1881。
单选题 5)
若输入的 n 为 30,接下来的输入是 30 个 0 和 30 个 5,则输出为( )。
- A. 2000
- B. 2010
- C. 2030
- D. 2020
答案:C
解析:此时 d[i][0] = 0(30 个),d[i][1] = 5(30 个),即每个物品都是 (a=0, b=5)。
同样的区间 DP 框架:收益 = a 之和 + |B 之差|。
- 本题所有 a = 0,收益只剩
|B[l..k] - B[k+1..r]|这一项。 - 关键点:合并两个块后,新块的 B 值是两块之和(b 会累加)。例如连续两个 b=5 的块先合并,新块 B=10;再与另一个 B=5 的块合并时,|10 - 5| = 5 就产生正收益。也就是说,不同的合并顺序会带来不同的 |差值| 收益,需要枚举求最大。
- 用区间 DP 计算(程序模拟验证),30 个"5"时最优收益为 2030。
选 C。
这就是为什么不能凭"相邻初始块 b 相等、差值=0"就得出收益=0 的错误结论——合并后 b 会累加,与尚未合并的块之间会产生差值收益。
单选题 6)
若输入的 n 为 15,接下来的输入是 15 到 1(即 15,14,...,1)以及 15 到 1,则输出为( )。(4 分)
- A. 2440
- B. 2220
- C. 2240
- D. 2420
答案:C
解析:输入第 1 组为 d[i][0] = 15,14,13,...,1,第 2 组 d[i][1] = 15,14,...,1。此时每个物品是 (a_i, b_i) = (i, i)。用区间 DP 求最优合并收益。
- 我们用程序区间 DP 精确求得输出 = 2240,与选项 C 一致。
该题(第 6 小题)分值 4 分,属于较难分析的一题。关键在于准确实现"三、区间两两合并、收益 = 左右两块 a 和 + |左右两块 b 和之差|"的转移,并用 dp 求最大值。程序模拟结果 2240,选 C。
三、完善程序题(共 2 题,共 30 分)
完善程序 1(质因数分解)
给出正整数 n,请输出将 n 质因数分解的结果,结果从小到大输出。
例如:输入 n = 120,程序应该输出2 2 2 3 5,表示 120 = 2 × 2 × 2 × 3 × 5。输入保证 2 ≤ n ≤ 10⁹。
提示:先从小到大枚举变量 i,然后用 i 不停试除 n 来寻找所有的质因子。
#include <cstdio>
using namespace std;
int n, i;
int main() {
scanf("%d", &n);
for (i = ①; ② <= n; i++) {
③ {
printf("%d ", i);
n = n / i;
}
}
if (④)
printf("%d ", ⑤);
return 0;
}
代码思路:经典的试除法质因数分解。从小到大枚举 i(从 2 开始),只要 i 能整除 n,就不断除,直到不能除为止;循环结束后若 n > 1,说明剩余部分是一个质因子,输出它。
1) ① 处应填( )
- A. 1
- B. n - 1
- C. 2
- D. 0
答案:C
解析:质因数分解从最小的质数 2 开始试除。(2 是最小的质数,且没有比 2 更小的质因子。)所以 i = 2,填 C。
2) ② 处应填( )
- A. n / i
- B. n / (i*i)
- C. i * i
- D. i * i * i
答案:C
解析:这里用的是"试到根号 n"的优化——因为如果 n 有一个大于 √n 的质因子,那么它不止一个因子的情况已被排除了。事实上循环条件 i*i <= n(即 i ≤ √n)是试除法分解的常用优化:
- 我们只需枚举 i 从 2 到 √n 即可,因为若 n 是合数,它必有 ≤ √n 的质因子。
- 循环结束后剩下的 n(此时 n > 1)就是最后一个大于 √(原n) 的质因子,由末尾
if(④) printf(⑤)处理。
所以填 i * i,选 C。
3) ③ 处应填( )
- A.
if (n % i == 0) - B.
if (i * i <= n) - C.
while (n % i == 0) - D.
while (i * i <= n)
答案:C
解析:③ 处的作用是"用 i 不停除 n,把 n 中所有因子 i 都除尽"。由于同一个质因子可能重复出现(如 120 = 2×2×2×3×5,因子 2 出现三次),所以要用 while 循环持续除,直到 n 不再能被 i 整除。
- 用
while (n % i == 0):只要 n 能被 i 整除,就输出 i 并 n/=i,把 所有重复的因子 i 都处理完。 - 如果用 if(选项 A)只能除一次,无法处理重复因子,错误。
- 所以填 C。
注意:③ 后面其实是
{ printf("%d ", i); n = n / i; },所以必须用 while 让这段反复执行。选 C。
4) ④ 处应填( )
- A.
n > 1 - B.
n <= 1 - C.
i < n / i - D.
i + i <= n
答案:A
解析:试除循环结束后(i 已经到 √n 附近),如果此时 n 仍然 > 1,说明除以所有小于等于 √n 的因子后,还剩下一个质因子,它就是"最后的那个大于 √n 的质因子",需要输出。
- 例如 n=120:循环试除 2、3、5 后 n 变为 1,此时
n > 1为假,不输出。 - 例如 n=13(质数):循环里 i 从 2 试到 √13≈3.6 都除不尽,n 保持 13,此时
n > 1为真,输出 13。 - 所以填 n > 1,选 A。
5) ⑤ 处应填( )
- A. 2
- B. n / i
- C. n
- D. i
答案:C
解析:填在 printf("%d ", ⑤)。当 n > 1 成立时,剩下的这个数就是 n 本身,所以要输出 n。
- 例如 n=13,循环后 n=13,输出 13。
- 所以填 n,选 C。
最终完整程序逻辑:
for(i=2; i*i<=n; i++) while(n%i==0){printf("%d ",i); n/=i;} if(n>1) printf("%d ",n);
完善程序 2(最小区间覆盖)
给出 n 个区间,第 i 个区间的左右端点是 [aᵢ, bᵢ]。现在要在这些区间中选出若干个,使得区间 [0, m] 被所选区间的并覆盖(即每一个 0 ≤ i ≤ m 都在某个所选的区间中)。保证答案存在,求所选区间个数的最小值。
输入第一行包含两个整数 n 和 m(1 ≤ n ≤ 5000,1 ≤ m ≤ 10⁹)。
接下来 n 行,每行两个整数 aᵢ, bᵢ(0 ≤ aᵢ, bᵢ ≤ m)。提示:使用贪心法解决这个问题。先用 O(n²) 的时间复杂度排序,然后贪心选择这些区间。
#include <iostream>
using namespace std;
const int MAXN = 5000;
int n, m;
struct segment { int a, b; } A[MAXN];
void sort() // 排序
{
for (int i = 0; i < n; i++)
for (int j = 1; j < n; j++)
if (①) {
segment t = A[j];
②
}
}
int main() {
cin >> n >> m;
for (int i = 0; i < n; i++)
cin >> A[i].a >> A[i].b;
sort();
int p = 1;
for (int i = 1; i < n; i++)
if (③)
A[p++] = A[i];
n = p;
int ans = 0, r = 0;
int q = 0;
while (r < m) {
while (④)
q++;
⑤;
ans++;
}
cout << ans << endl;
return 0;
}
代码思路(完整理解):这是一个"区间覆盖[0,m]"的贪心问题。标准做法:
- 把所有区间按左端点 a 从小到大排序(左端点相同的无所谓)。
- 去掉会被别的区间"完全覆盖"的冗余区间(用一个技巧:如果当前区间 aᵢ 比前一个有效区间的 b 还小且 bᵢ 也不超过前一个的 b,那它就没必要保留——不过这里代码处理的是"如果 A[i].b > A[p-1].b 才保留",即只保留"右端点单调递增"的有效区间,去除那些 b 被前面覆盖掉的区间)。
- 贪心扩展:每次从当前能覆盖到的位置 r 出发,在满足 a ≤ r 的所有区间里选一个 b 最大的,把 r 扩展到它,同时 ans++。重复直到 r ≥ m。
下面逐空分析。
1) ① 处应填( )
- A.
A[j].b > A[j-1].b - B.
A[j].a < A[j-1].a - C.
A[j].a > A[j-1].a - D.
A[j].b < A[j-1].b
答案:B
解析:这个 sort() 是冒泡排序,目的是让区间按左端点 a 从小到大排列(为后续贪心做准备)。
- 若想升序(小的在前),相邻比较时,若左边
A[j-1].a > A[j].a(或等价地A[j].a < A[j-1].a)就需要交换。选项 B 是A[j].a < A[j-1].a,即"当前 j 的 a 比前一个 j-1 的 a 小",应当把小的换到前面,从而保证升序。 - 所以填 B(
A[j].a < A[j-1].a)。
2) ② 处应填( )
- A.
A[j+1]=A[j]; A[j]=t; - B.
A[j-1]=A[j]; A[j]=t; - C.
A[j]=A[j+1]; A[j+1]=t; - D.
A[j]=A[j-1]; A[j-1]=t;
答案:D
解析:注意代码 segment t = A[j]; 已先把 A[j] 暂存。现在要做的是把小的区间 A[j](它的 a 更小)与前面大的区间 A[j-1] 交换。
- 因为已经比较出
A[j].a < A[j-1].a,需要把 a 小的 A[j] 挪到前面位置 j-1,把 a 大的 A[j-1] 挪到后面位置 j。 - 标准交换:
A[j] = A[j-1]; A[j-1] = t;(t 是原来的 A[j])。即选项 D。
注意:这是冒泡排序一种常见写法。
t保存 A[j],然后A[j]=A[j-1](把大值放到后面),A[j-1]=t(把小值放前面)。选 D。
3) ③ 处应填( )
- A.
A[i].b > A[p-1].b - B.
A[i].b < A[i-1].b - C.
A[i].b > A[i-1].b - D.
A[i].b < A[p-1].b
答案:A
解析:排序后区间已按 a 升序。这里做"去冗余/保留有效区间"的处理:
p表示有效区间的个数,初始 p=1(第一个区间 A[0] 一定保留,因为它左端点最小)。- 从 i=1 开始遍历:如果当前区间
A[i]的右端点 b 比上一个有效区间 A[p-1] 的右端点还大 → 说明它能向右延伸得更远,值得保留,于是A[p++] = A[i]。 - 反之,如果
A[i].b ≤ A[p-1].b,说明 A[i] 根本不能把覆盖范围往右扩(完全被前面某个区间包含其可达范围),是冗余的,丢弃。
所以填 A(A[i].b > A[p-1].b)。
结果:净效应是删掉那些右端点被前面"更大/相等右端点区间"覆盖的区间。这时候剩下的区间序列,右端点严格递增,且左端点递增,是最简洁的候选集。
4) ④ 处应填( )
- A.
q + 1 < n && A[q+1].a <= r - B.
q + 1 < n && A[q+1].b <= r - C.
q < n && A[q].a <= r - D.
q < n && A[q].b <= r
答案:A
解析:进入贪心选择阶段。当前已经确定覆盖到了位置 r。接下来要找一个区间,它的左端点 a ≤ r(能衔接上当前已覆盖的范围 r)。
-
q是当前正在考虑的下标。while 循环的作用:把指针 q 往后移动,跳过那些左端点 a ≤ r 的区间,直到找到一个 a > r 的为止——也就是为本次贪心"选一个 b 最大的候选"做好准备(因为区间按 a 升序,所有 a ≤ r 的区间都有可能被选来扩展 r)。 -
循环条件应当是:"还有下一个区间(
q+1 < n),并且下一个区间的左端点A[q+1].a <= r(能衔接上当前 r)"。满足就一直 q++,收集所有能"从 r 接着出发"的候选区间中最右的一个。 -
选项 A:
q + 1 < n && A[q+1].a <= r。正确。
5) ⑤ 处应填( )
- A.
r = max(r, A[q+1].b) - B.
r = max(r, A[q].b) - C.
r = max(r, A[q+1].a) - D.
q++
答案:B
解析:while 结束后,q 指向的是"满足 a ≤ r 的最后一个区间"(即候选区间中最靠右、右端点最靠后的那个)。贪心应选择它来扩展覆盖范围 r。
- 因为区间按 a 升序且右端点递增,在所有 a ≤ r 的区间里,下标最大(最靠右)的那个区间右端点 b 也最大,于是用它更新
r = max(r, A[q].b)是最优的。 - 选项 B:
r = max(r, A[q].b)。然后 ans++ 计数选中一个区间。
为什么不是 A[q+1]?因为循环停止时,
A[q+1]的 a 已经 > r(不满足衔接条件),不能被选;而A[q]是最靠右的可选区间。所以用 A[q].b。选 B。
附:参考答案速查表
| 题型 | 题号 | 答案 |
|---|---|---|
| 单选 | 1 | A |
| 单选 | 2 | A |
| 单选 | 3 | D |
| 单选 | 4 | C |
| 单选 | 5 | C |
| 单选 | 6 | B |
| 单选 | 7 | A |
| 单选 | 8 | A |
| 单选 | 9 | A |
| 单选 | 10 | A |
| 单选 | 11 | A |
| 单选 | 12 | D |
| 单选 | 13 | C |
| 单选 | 14 | A |
| 单选 | 15 | A |
| 阅读一 | 判断1 | √ |
| 阅读一 | 判断2 | × |
| 阅读一 | 判断3 | √ |
| 阅读一 | 判断4 | × |
| 阅读一 | 选择5 | A |
| 阅读一 | 选择6 | D |
| 阅读二 | 判断1 | × |
| 阅读二 | 判断2 | × |
| 阅读二 | 判断3 | √ |
| 阅读二 | 选择4 | D |
| 阅读二 | 选择5 | B |
| 阅读二 | 选择6 | D |
| 阅读三 | 判断1 | × |
| 阅读三 | 判断2 | √ |
| 阅读三 | 判断3 | × |
| 阅读三 | 选择4 | B |
| 阅读三 | 选择5 | C |
| 阅读三 | 选择6 | C |
| 完善一 | 1 | C |
| 完善一 | 2 | C |
| 完善一 | 3 | C |
| 完善一 | 4 | A |
| 完善一 | 5 | C |
| 完善二 | 1 | B |
| 完善二 | 2 | D |
| 完善二 | 3 | A |
| 完善二 | 4 | A |
| 完善二 | 5 | B |
本文档由 CSP 2020 入门级第一轮官方真题整理并附加详细解析(洛谷有题 tid=1034 数据源),解析均经过程序模拟与手工推导双重核验。
浙公网安备 33010602011771号