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

解析:直接按公式算:存储空间 = 像素总数 × 每像素的位数。

  1. 像素总数 = 2048 × 1024 = 2^11 × 2^10 = 2^21 个像素。
  2. "32 位真彩色"意味着每个像素占 32 bit(比特)= 4 字节(1 字节 = 8 bit,32÷8=4)。
  3. 总字节数 = 2^21 × 4 = 2^23 字节。
  4. 换算成 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

解析:这是"捆绑法"(相邻问题)计数:

  1. 把双胞胎当作一个整体:因为要求两人必须相邻,可以先把这"两个人"捆成一个"大块"。于是整体来看,要排列的是 4 个对象(其他 3 个小朋友 + 1 个双胞胎整体)。4 个对象的排列数是 4! = 24。
  2. 双胞胎内部还有顺序:这个"整体"里的两个双胞胎可以交换位置(比如哥哥在左、弟弟在右,或反过来),有 2 种排法。
  3. 由乘法原理:总方法数 = 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 只来自两副不同的颜色且不能是同副)。

分两步:

  1. 选出凑成完整两副的颜色:5 副中选 2 副,记 C(5, 2) = 10 种。
  2. 再从剩下的 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;
}

理解程序在做什么:

  1. encoder 初始只有 3 个有值字符:'C'、'S'、'P'(第 3 个是 0),所以第一段循环得到 k = 3。
  2. 第二个 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 的逆映射。
  4. 输入字符串 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]"的贪心问题。标准做法:

  1. 把所有区间按左端点 a 从小到大排序(左端点相同的无所谓)。
  2. 去掉会被别的区间"完全覆盖"的冗余区间(用一个技巧:如果当前区间 aᵢ 比前一个有效区间的 b 还小且 bᵢ 也不超过前一个的 b,那它就没必要保留——不过这里代码处理的是"如果 A[i].b > A[p-1].b 才保留",即只保留"右端点单调递增"的有效区间,去除那些 b 被前面覆盖掉的区间)。
  3. 贪心扩展:每次从当前能覆盖到的位置 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 数据源),解析均经过程序模拟与手工推导双重核验。

posted @ 2026-08-25 14:37  surprise_ying  阅读(125)  评论(0)    收藏  举报