CCF-GESP 等级考试 2025年6月认证C++八级真题解析

1 单选题(每题 2 分,共 30 分)

第1题一间的机房要安排6名同学进行上机考试,座位共2行3列。考虑到在座位上很容易看到同一行的左右两侧的 屏幕,安排中间一列的同学做A卷,左右两列的同学做B卷。请问共有多少种排座位的方案?(   )。

A. 720                                B. 90                                  C. 48                                   D. 15

解析:答案A座位如下图所示:坐②⑤的学生做A卷,坐①④③⑥的学生做B,由于没有指定哪些学生必须做A卷、哪些学生必须做B卷,所以这是个全排列,只要排到②⑤座位的学生就做A卷,其他学生做B卷。‌‌‌‌=6!=720。

也可以这么考虑:6名同学是不同的个体,即同学之间是可区分的。需要计算将6名不同的同学分配到6个座位上,满足题目试卷分配规则的所有可能的排列方式。为了计算满足条件的排列方式,可以按照以下步骤进行:A卷座位:座位②, 座位(共2个);B卷座位:座位①, 座位③, 座位④, 座位(共4个)。从6名同学中选出2名来做A卷,其余4名做B卷。选择A卷同学的方式有= 15 种。分配A卷同学到A卷座位:选出的2A卷同学可以分配到座位2和座位5,有 2! = 2 种排列方式(因为两个A卷同学是不同的,可以交换位置)。分配B卷同学到B卷座位:剩下的4B卷同学需要分配到4B卷座位有 4! = 24 种方式。总方案数 = 选择A卷同学的方式 × 分配A卷同学到座位的方式 × 分配B卷同学到座位的方式=×2!×4! = 15 × 2 × 24 = 720 种。故选A

第2题 又到了毕业季,学长学姐们都在开心地拍毕业照。现在有3位学长、3位学姐希望排成一排拍照,要求男生不相邻、女生不相邻。请问共有多少种拍照方案?(   )。

A. 720                                 B. 72                               C. 36                                     D. 2

解析:答案B3位学长、3位学姐排成一排拍照,要求男生不相邻、女生不相邻,只有如下图所示两种排法:

①“-----②“-----。排法3位学长在3位置上的排列有3! = 6种;3位学姐在3位置上的排列有3! = 6种。因此,该排法的排列数为 3! × 3! = 6 × 6 = 36 种。排法3位学长在3位置上的排列有3! = 6种;3位学姐在3位置上的排列有3! = 6种。因此,该排法的排列数为 3! × 3! = 6 × 6 = 36 种。合计72种。故选B

第3题 下列关于C++类和对象的说法,错误的是(   )。

A. 通过语句 const int x = 5; 定义了一个对象x。

B. 通过语句 std::string t = "12345"; 定义了一个对象t。

C. 通过语句 void (*fp)() = NULL; 定义了一个对象fp。

D. 通过语句 class MyClass; 定义了一个类MyClass。

解析:答案DA. const int x = 5; x 是一个常量对象(尽管是基本类型,但C++允许称其为对象),正确。B. std::string t = "12345"; t  std::string 类的实例,属于对象,正确。C. void (*fp)() = NULL; fp 是一个函数指针,在C++中,指针变量也被视为对象,正确。D. class MyClass; 该语句是前向声明forward declaration),仅声明类名,未定义类体,因此未完成类的定义,错误。故选D

第4题 关于生成树的说法,错误的是(   )。

A. 一个无向连通图,一定有生成树。

B. 𝑛个顶点的无向图,其生成树要么不存在,要么一定包含𝑛-1条边。

C. 𝑛个顶点、𝑛-1条边的无向图,不可能有多颗生成树。

D. 𝑛个顶点、𝑛-1条边的无向图,它本身就是自己的生成树。

解析:答案DA. 无向连通图必然存在生成树。因为生成树的定义要求包含所有顶点且无环,而连通图本身已满足顶点间路径连通性,通过删除环边即可得到生成树,正确。B. 生成树若存在,则必含n−1 条边。这是生成树的基本性质(连通无环的极小图)。C. n个顶点、n−1 条边的无向图若连通则必为树(唯一生成树),但若不连通则无生成树,因此不可能有多颗生成树,正确(最多一棵)。D. n个顶点、n−1 条边的无向图不一定是生成树。反例:当图不连通时(如一个三角形加一个孤立顶点,共4顶点3边),它不符合生成树的连通性要求,所以错误。故选D

第5题 一对夫妻生男生女的概率相同。这对夫妻希望儿女双全。请问这对夫妻生下两个孩子时,实现儿女双全的概率是多少?(   )。

A.                                     B.                                    C.                                    D.

解析:答案C。假设每次生育独立且生男生女概率均为50%,则两个孩子的性别组合共有4种等可能情况:男孩+男孩(概率25%),男孩+女孩(概率25%)、女孩+男孩(概率25%)、女孩+女孩(概率25%),题目条件是"儿女双全",即需至少一个男孩和一个女孩,符合条件的是+女“和+两种组合,总概率为25%+25%=50%,即。故选C

第6题 已定义变量 double a, b; 下列哪个表达式可以用来判断一元二次方程是否有实根?(   )。

A. 4 * b - a * a < 0               B. 4 * b <= a * a               C. a * a - 4 * b                    D. b * 4 - a * a

解析:答案B。一元二次方程的解为,有实根的条件是b²-4ac≥0。对本题方程,a=1b=ac=b,实根的条件a²-4b≥0,即a*a-4*b>=0,也就是a*a>=4*b4*b<=a*a。故选B

第7题 𝑛个结点的二叉树,执行广度优先搜索的平均时间复杂度是(   )。

A. O(log n)                            B. O(n log n)                     C. O(n)                                       D. O(2)

解析:答案C。广度优先搜索(BFS)会逐层访问所有节点,每个节点仅被访问一次,因此时间复杂度与节点数𝑛成正比。无论是平衡二叉树、普通二叉树还是退化的链表结构,BFS都需要遍历所有𝑛个节点,时间复杂度恒为O(n)‌A. O(log n)适用于平衡二叉搜索树的查找操作,不适用于BFS,错误。‌B. O(n log n)常见于排序算法,与BFS无关,错误。‌D. O(2)仅适用于满二叉树的节点总数计算(如𝑛层满二叉树有2−1个节点),与时间复杂度无关,错误。故选C

第8题 以下关于动态规划的说法中,错误的是(   )。

A. 动态规划方法通常能够列出递推公式。

B. 动态规划方法的时间复杂度通常为状态的个数。

C. 动态规划方法有递推和递归两种实现形式。

D. 对很多问题,递推实现和递归实现动态规划方法的时间复杂度相当。

解析:答案B。动态规划的时间复杂度不仅取决于状态个数,还与状态转移的计算复杂度相关。例如,状态转移涉及多层循环时,时间复杂度可能是状态数的多项式倍数,所以B.错误。A.动态规划的核心是通过递推公式描述子问题与原问题的关系,正确。C.动态规划可通过递归(记忆化搜索)或递推(迭代)实现,正确。‌D.对于多数问题,两种实现的时间复杂度相同,但递归可能因函数调用产生额外开销,正确。故选C

第9题 下面的sum_digit函数试图求出从1到n(包含1和n)的数中,包含数字d的个数。该函数的时间复杂度为(   )。

  1. #include <string>
  2. int count_digit(int n, char d) {
  3.     int cnt = 0;
  4.     std::string s = std::to_string(n);
  5.     for (int i = 0; i < s.length(); i++)
  6.         if (s[i] == d)
  7.             cnt++;
  8.     return cnt;
  9. }
  10. int sum_digit(int n, char d) {
  11.     int sum = 0;
  12.     for (int i = 1; i <= n; i++)
  13.         sum += count_digit(i, d);
  14.    return sum;
  15. }

A. O(n log n)                       B. O(n)                             C. O(log n)                            D. O(n²)

解析:答案A。先分析count_digit函数的时间复杂度:将整数n转换为字符串的时间复杂度为O(log₁₀(n)),因为数字的位数与log₁₀(n)成正比,因为log(n)=log(n)/log(m)log₁₀(n)= log₂(n)/ log₂(10),因为log₂(10)为常数,O(log₁₀(n))O(log₂(n))相当,也与O(log(n))相当。遍历字符串的每个字符进行比较(n的位数为log₁₀n),时间复杂度为O(log n),因此count_digit函数的总时间复杂度为O(log n)‌sum_digit函数的时间复杂度:外层循环执行n次,每次调用count_digit函数,每次count_digit调用时间为O(log i),最坏情况下i=n时为O(log n),因此总时间复杂度为n × O(log n) = O(n log n)‌,所以该函数的时间复杂度为O(n log n)‌。这种复杂度属于线性对数阶,介于线性阶O(n)和平方阶O(n²)之间。故选A

第10题 下面程序的输出为(   )。

  1. #include <iostream>
  2. const int N = 10;
  3. int ch[N][N][N];
  4. int main() {
  5.     for (int x = 0; x < N; x++)
  6.         for (int y = 0; y < N; y++)
  7.             for (int z = 0; z < N; z++)
  8.                 if (x == 0 && y == 0 && z == 0)
  9.                     ch[x][y][z] = 1;
  10.                 else {
  11.                     if (x > 0)
  12.                         ch[x][y][z] += ch[x - 1][y][z];
  13.                     if (y > 0)
  14.                         ch[x][y][z] += ch[x][y - 1][z];
  15.                     if (z > 0)
  16.                         ch[x][y][z] += ch[x][y][z - 1];
  17.                 }
  18.      std::cout << ch[1][2][3] << std::endl;
  19.      return 0;
  20. }

A. 60                                      B. 20                                   C. 15                                           D. 10

解析:答案A。程序分析:这段代码的核心逻辑是填充一个三维数组 ch[N][N][N],然后输出 ch[1][2][3] 的值。ch[x][y][z] 的计算方式:

初始条件ch[0][0][0] = 1(当 x = 0, y = 0, z = 0 时)

递推公式:对于其他 (x, y, z)ch[x][y][z] 的值由其三个方向的前驱状态相加:

if (x > 0) ch[x][y][z] += ch[x-1][y][z]

if (y > 0) ch[x][y][z] += ch[x][y-1][z]

if (z > 0) ch[x][y][z] += ch[x][y][z-1]

数学意义:这个递推关系实际上是三维路径计数,类似于组合数学中的多重组合数ch[x][y][z] 表示从 (0, 0, 0) 走到 (x, y, z) 的路径数,每次只能沿 x或 z 方向移动一步。其数学表达式为:ch[x][y][z]=(x+y+z)!/(x!y!z!),即多项式系数Multinomial Coefficient)。‌计算 ch[1][2][3]‌,将代入 x = 1, y = 2, z = 3,得ch[1][2][3]=(1+2+3)!/(1!2!3!)=720/12=60。

也可以手动计算得到结果(计算量=2*3*4=24(其中ch[0][0][0] = 1已知)):

ch[0][0][0] = 1

ch[0][0][1] = ch[0][0][0] = 1

ch[0][0][2] = ch[0][0][1] = 1

ch[0][0][3] = ch[0][0][2] = 1

ch[0][1][0] = ch[0][0][0] = 1

ch[0][1][1] = ch[0][0][1] + ch[0][1][0] = 2

ch[0][1][2] = ch[0][0][2] + ch[0][1][1] = 3

ch[0][1][3] = ch[0][0][3] + ch[0][1][2] = 4

ch[0][2][0] = ch[0][1][0] = 1

ch[0][2][1] = ch[0][1][1] + ch[0][2][0] = 3

ch[0][2][2] = ch[0][1][2] + ch[0][2][1] = 6

ch[0][2][3] = ch[0][1][3] + ch[0][2][2] = 10

ch[1][0][0] = ch[0][0][0] = 1

ch[1][0][1] = ch[0][0][1] + ch[1][0][0] = 2

ch[1][0][2] = ch[0][0][2] + ch[1][0][1] = 3

ch[1][0][3] = ch[0][0][3] + ch[1][0][2] = 4

ch[1][1][0] = ch[0][1][0] + ch[1][0][0] = 2

ch[1][1][1] = ch[0][1][1] + ch[1][0][1] + ch[1][1][0] = 6

ch[1][1][2] = ch[0][1][2] + ch[1][0][2] + ch[1][1][1] = 12

ch[1][1][3] = ch[0][1][3] + ch[1][0][3] + ch[1][1][2] = 20

ch[1][2][0] = ch[0][2][0] + ch[1][1][0] = 3

ch[1][2][1] = ch[0][2][1] + ch[1][1][1] + ch[1][2][0] = 12

ch[1][2][2] = ch[0][2][2] + ch[1][1][2] + ch[1][2][1] = 30

ch[1][2][3] = ch[0][2][3] + ch[1][1][3] + ch[1][2][2] = 60

故选A

第11题 下面 count_triple 函数的时间复杂度为(   )。

  1. int gcd(int a, int b) {
  2.     if (a == 0)
  3.         return b;
  4.     return gcd(b % a, a);
  5. }
  6. int count_triple(int n) {
  7.     int cnt = 0;
  8.     for (int v = 1; v * v * 4 <= n; v++)
  9.         for (int u = v + 1; u * (u + v) * 2 <= n; u += 2)
  10.             if (gcd(u, v) == 1) {
  11.                 int a = u * u - v * v;
  12.                 int b = u * v * 2;
  13.                 int c = u * u + v * v;
  14.                 cnt += n / (a + b + c);
  15.             }
  16.     return cnt;
  17. }

A. O(n)                             B. O(n²)                           C. O(n log n)                       D. O(n²log n)

解析:答案C(1) gcd 函数使用欧几里得算法计算两个数的最大公约数,其时间复杂度为O(log min(a, b)),在最坏情况下,递归深度为O(log n)‌‌(‌2) count_triple 函数为双循环结构外层循环‌ for (int v = 1; v * v * 4 <= n; v++),循环次数由 v² ≤ n/4 决定,即 v 最多增长到,因此循环次数为‌‌;内层循环‌ for (int u = v + 1; u * (u + v) * 2 <= n; u += 2),由于 u  v+1 开始,且 u 的增长受 u(u + v) ≤ n 限制,最坏情况下循环次数为u * (u +1) * 2 <= n/2(因为u+=2,次数减半) u² < n/4,循环次数也约为‌‌。每次内层循环都会调用 gcd(u, v),总时间复杂度为:=O(n log n)。故选C

第12题 下面 quick_sort 函数试图实现快速排序算法,两处横线处分别应该填入的是(   )。

  1. void swap(int & a, int & b) {
  2.     int temp = a; a = b; b = temp;
  3. }
  4. int partition(int a[], int l, int r) {
  5.     int pivot = a[l], i = l + 1, j = r;
  6.     while (i <= j) {
  7.         while (i <= j && a[j] >= pivot)
  8.             j--;
  9.         while (i <= j && a[i] <= pivot)
  10.             i++;
  11.         if (i < j)
  12.             swap(a[i], a[j]);
  13.     }
  14.     ________; // 在此处填入选项
  15.     return ________; // 在此处填入选项
  16. }
  17. void quick_sort(int a[], int l, int r) {
  18.     if (l < r) {
  19.         int pivot = partition(a, l, r);
  20.         quick_sort(a, l, pivot - 1);
  21.         quick_sort(a, pivot + 1, r);
  22.     }
  23. }

A.

  1. swap(a[l], a[i])
  2. i

B.

  1. swap(a[l], a[j])
  2. i

C.

  1. swap(a[l], a[i])
  2. j

D.

  1. swap(a[l], a[j])
  2. j

解析:答案D。这是一个快速排序函数,两处横线都在求基准值(pivot)选择子函数partition()之中。基准选择:以 a[l] 为基准值(pivot),初始化指针 i = l + 1(从左向右扫描)、j = r(从右向左扫描)。从右向左移动,找到第一个小于 pivot 的元素;从左向右移动,找到第一个大于 pivot 的元素。若 i < j,交换 a[i]  a[j],确保左侧元素 ≤ pivot,右侧元素 ≥ pivot

终止条件是当 i > j 时,循环结束。此时 j 指向最后一个≤ pivot 的元素,指向第一个> pivot 的元素。基准归位:需将基准值 a[l]  a[j] 交换,使 pivot 位于正确位置(即左侧均 ≤ pivot,右侧均 ≥ pivot),第一处填空(第14行)填swap(a[l], a[j])。如填swap(a[l], a[i]) 会导致基准值可能与> pivot 的元素交换,破坏分区正确性。

分区完成后,基准值的最终位置为 j,因此应返回 j 作为后续递归的划分点,所以第二处填空(第15行)填j。如返回 i 会导致递归区间错误(如 pivot + 1 可能包含 ≤ pivot 的元素)。

故选D

第 13 题 下面 LIS 函数试图求出最长上升子序列的长度,横线处应该填入的是(   )。

  1. int max(int a, int b) {
  2.     return (a > b) ? a : b;
  3. }
  4. int LIS(vector & nums) {
  5.     int n = nums.size();
  6.     if (n == 0)
  7.         return 0;
  8.     vector dp(n, 1);
  9.     int maxLen = 1;
  10.     for (int i = 1; i < n; i++) {
  11.         for (int j = 0; j < i; j++)
  12.             if (nums[j] < nums[i])
  13.                 ________; // 在此处填入选项
  14.         maxLen = max(maxLen, dp[i]);
  15.     }
  16.     return maxLen;
  17. }

A.

  1. dp[j] = max(dp[j] + 1, dp[i])

B.

  1. dp[j] = max(dp[j], dp[i] + 1)

C.

  1. dp[i] = max(dp[i] + 1, dp[j])

D.

  1. dp[i] = max(dp[i], dp[j] + 1)

解析:答案D。该函数实现的是最长上升子序列(LIS的动态规划解法,dp[i] 表示以 nums[i] 结尾的最长上升子序列的长度。外层循环遍历每个元素 nums[i],内层循环遍历 nums[j]j < i),如果 nums[j] < nums[i],则更新 dp[i]横线处应填入的‌dp[i] 的更新方式

如果 nums[j] < nums[i],说明 nums[i] 可以接在 nums[j] 后面,形成更长的上升子序列。因此,dp[i] 应更新为 max(dp[i], dp[j] + 1),即:如果 dp[j] + 1 比当前 dp[i] 大,则更新 dp[i],否则保持 dp[i] 不变。所以应该填dp[i] = max(dp[i], dp[j] + 1)D.正确。故选D

第14题 下面 LIS 函数试图求出最长上升子序列的长度,其时间复杂度为(   )。

  1. #define INT_MIN (-1000)
  2. int LIS(vector & nums) {
  3.     int n = nums.size();
  4.     vector tail;
  5.     tail.push_back(INT_MIN);
  6.     for (int i = 0; i < n; i++) {
  7.         int x = nums[i], l = 0, r = tail.size();
  8.         while (l < r) {
  9.             int mid = (l + r) / 2;
  10.             if (tail[mid] < x)
  11.                 l = mid + 1;
  12.             else
  13.                 r = mid;
  14.         }
  15.         if (r == tail.size())
  16.             tail.push_back(x);
  17.         else
  18.             tail[r] = x;
  19.     }
  20.     return tail.size() - 1;
  21. }

A. O(log n)                         B. O(n)                            C. O(n log n)                               D. O(n²)

解析:答案C。该函数实现的是最长上升子序列(LIS)的解法,本题给出的算法通过维护一个单调递增的 tail 数组,利用二分查找替代动态规划中的线性扫描,算法逻辑维护单调序列‌tail 数组用于存储长度为 i+1 的上升子序列的最小末尾值,始终保持严格递增。对于每个元素 x,通过二分查找确定其在 tail 中的插入位置(替换第一个 ≥ x 的元素或扩展序列)。

外层循环:遍历 nums 的每个元素,共 n 次,时间复杂度为O(n)‌内层二分查找:每次查找范围为 tail 的当前长度(最大为 n),时间复杂度为O(log n)‌两者嵌套,总复杂度‌O(n log n)。故选C

第15题 下面的程序使用邻接矩阵表达的带权无向图,则从顶点0到顶点3的最短距离为(   )。

int weight[4][4] = {

    { 0, 5, 8, 10},

    { 5, 0, 1, 7},

    { 8, 1, 0, 3},

    {10, 7, 3, 0}

};

A. 9                                    B. 10                                C. 11                                     D. 12

解析:答案A。由于图较小(仅4个顶点),可以手动计算所有可能的路径,并找出最短路径。根据所给邻接矩阵可绘制出如下对应图:

直接观察邻接矩阵‌(或对应图),尝试找出所有可能的路径,并计算它们的总权重。

使用 Dijkstra 算法(适用于带权无负边图的最短路径)。枚举所有可能的路径(适用于小规模图)从顶点0顶点3的所有可能路径及其权重:

直接路径 0 → 3‌w[0][3] = 10

路径 0 → 1 → 3‌0 → 1w=51 → 3w=7总权重5+7=12

路径 0 → 2 → 3‌0 → 2w=82 → 3w=3总权重8+3=11

路径 0 → 1 → 2 → 30 → 1w=51 → 2w=12 → 3w=3总权重5+1+3=9

路径 0 → 2 → 1 → 3‌0 → 2w=82 → 1w=11 → 3w=7总权重8 + 1 + 7 = 16

所以最短路径 0 → 1 → 2 → 3,权重为9。故选A

2 判断题(每题 2 分,共 20 分)

第1题 C++语言中,表达式 9 | 12 的结果类型为int、值为13。(   )

解析:答案正确。C++语言中,”|“按位或运算符,9 | 12 = 0b1001 | 0b1100 = 0b1101 = 13。故正确。

第2题 C++语言中,访问数据发生下标越界时,总是会产生运行时错误,从而使程序异常退出。(   )

解析:答案错误。C++语言中,不检测访问数据越界问题,越界访问不一定会导致程序崩溃(可能只是读取/修改错误数据),所以不一定会产生运行时错误。故错误。

第3题 对𝑛个元素的数组进行归并排序,最差情况的时间复杂度为𝘖(𝑛 log 𝑛)。(   )

解析:答案正确。归并排序数组被递归地分成两半,直到子数组长度为 1,共需‌log₂𝑛层分解。每层合并需要遍历所有元素𝑂(𝑛)),总共有log₂𝑛层,因此总时间复杂度为𝑂(𝑛 log 𝑛)。无论输入数据是否有序,归并排序的分层和合并步骤均保持相同操作量,因此最好、最坏、平均时间复杂度均为𝑂(𝑛 log 𝑛)。故正确。

第4题 5个相同的红球和4个相同的蓝球排成一排,要求每个蓝球的两侧都必须至少有一个红球,则一共有15种排列方案。(   )

解析:答案错误。根据题意:每个蓝球()的左右两侧都必须至少有一个红球(),即不能出现在开头或结尾,且不能有两个相邻(否则中间的会有一侧没有)。因此,蓝球()必须被红球()隔开,即只能出现在之间的间隔中。

先间隔排蓝球():_____,总共只有5个间隔位置,且每个间隔位置至少放一个红球(),只有5个红球()放5个间隔位置,只有1种方案:

由于满足条件的排列方案只有1(即 ),而不是题目中给出的15种,故错误‌‌

第5题 使用 math.h 或 cmath 头文件中的函数,表达式log(8)的结果类型为double、值约为3。(   )

解析:答案错误。 math.h  cmath 中定义log() 函数原型为 double log(double x),结果类型为 double,正确。log(8) 计算的是自然对数(以 e 为底,e ≈ 2.71828),log(8) ≈‌ 2.07944‌,而非题目中的3,所以错误。故错误。

第6题 C++是一种面向对象编程语言,C则不是。继承是面向对象三大特性之一,因此,使用C语言无法实现继承。(   )

解析:答案正确。‌C++支持面向对象编程(OOP),包括封装继承多态三大特性(也有称四大特性,增加抽象特性)‌C语言是面向过程 的编程语言,缺乏类、继承等OOP机制。C语言无原生语法支持继承,所以C语言无法实现继承,说法正确。故正确。

第7题 𝑛个顶点的无向完全图,有𝑛⁻²棵生成树。(   )

解析:答案正确。包含𝑛个顶点的无向完全图(记作𝐾ₙ)是指任意两个顶点之间都有一条边相连的图,其边数为𝑛(𝑛−1)/2。生成树是原图的极小连通子图,包含所有𝑛个顶点和𝑛−1条边,且不包含环。对于𝑛个顶点的无向完全图,生成树的数量由‌Cayley公式给出,即𝑛ⁿ⁻²,题目中的说法正确。故正确。

第8题 已知三个 double 类型的变量a、b和theta分别表示一个三角形的两条边长及二者的夹角(弧度),则三角形的周长可以通过表达式sqrt(a * a + b * b - 2 * a * b * cos(theta))求得。(   )

解析:答案错误。已知三角形的两边长及夹角,第三边的长度可通过 余弦定理求得:

余弦定理求第三边长度公式为:c²=a²+b²−2abcosθ,即

其中:ab为已知两边长度,θ为两边的夹角。

周长为:

所以题目给出的“三角形的周长可以通过表达式sqrt(a * a + b * b - 2 * a * b * cos(theta))求得说法错误,故错误。

第9题 有𝑉个顶点、𝐸条边的图的深度优先搜索遍历时间复杂度为𝘖(𝑉+𝐸)。(   )

解析:答案正确。深度优先搜索(DFS)的时间复杂度分析:邻接表存储时:每个顶点被访问一次,时间复杂度为O(V),每条边会被遍历一次(无向图每条边处理两次,但仍为线性关系),时间复杂度为O(E),总时间复杂度为O(V + E)。邻接矩阵存储时:需要检查每个顶点与其他所有顶点的连接情况,时间复杂度为O(V²),与边数E无关。题目中未说明存储方式,但默认情况下时间复杂度分析通常基于邻接表实现,所以O(V+E)的表述是正确的。故正确。

第10题 从32名学生中选出4人分别担任班长、副班长、学习委员和组织委员,老师要求班级综合成绩排名最后的4 名学生不得参选班长或学习委员(仍可以参选副班长和组织委员),则共有𝑃(30,4)种不同的选法。(   )

解析:答案正确。‌题目涉及职位分配,不同职位代表不同的顺序,属于排列问题。排列数公式:𝑃(𝑛,𝑘)=𝑛!/(𝑛𝑘)!,表示从𝑛个元素中选𝑘个有序排列。‌‌班长和学习委员:只能由非最后4名的28名学生担任(即前28名)。副班长和组织委员:可由全部32名学生担任(包括最后4名)。

班长和学习委员的选择:需从前28名学生中选2人,并分配到班长和学习委员两个职位,排列数为𝑃(28,2)=28×27

副班长和组织委员的选择:剩余30名学生(因班长和学习委员已占用2人,无论是否来自最后4名)中选2人,分配到副班长和组织委员,排列数为𝑃(30,2)=30×29

总排列数:总选法为𝑃(28,2)×𝑃(30,2)=28×27×30×29,与𝑃(30,4)=30×29×28×27一致。

故正确。

3 编程题(每题 25 分,共 50 分)

3.1 编程题1

  • 试题名称:树上旅行
  • 时间限制:1.0 s
  • 内存限制:512.0 MB

3.1.1 题目描述

给定一棵有𝑛个结点的有根树,结点依次以1,2,...,编号,其中根结点的编号为1。

小A计划在这棵有根树上进行𝑞次旅行。在第𝑖次旅行中,小A首先会选定结点𝑠作为起点,并移动若干次。移动分为以下两种:

1. 移动至当前结点的父结点。特殊地,如果当前位于根结点,则不进行移动。

2. 移动至当前结点的所有子结点中编号最小的结点。特殊地,如果当前位于叶子结点,则不进行移动。

由于移动次数可能很大,对于第 𝑖 次旅行,旅行中的移动将以𝑘个不为零的整数构成的序列𝑎,₁, 𝑎,₂,..., 𝑎,表示。对于𝑎,,若𝑎, > 0则代表进行𝑎,次第一种移动;若𝑎, < 0则代表进行- 𝑎,次第二种移动。根据给出的序列从左至右完成所有移动后,小A所在的结点即是旅行的终点。

给定每次旅行的起点与移动序列,请你求出旅行终点的结点编号。

3.1.2 输入格式

第一行,两个正整数𝑛, 𝑞,分别表示有根树的结点数量,以及旅行次数。

第二行,𝑛-1个整数 𝑝₂, 𝑝₃, ..., 𝑝,其中𝑝表示结点 𝑖 的父结点编号。

接下来 2𝑞 行中的第2𝑖-1行(1 ≤ 𝑖 ≤ 𝑞)包含两个正整数𝑠, 𝑘,分别表示第 𝑖 次旅行的起点编号,以及移动序列的长度。第 2𝑖 行包含 𝑘 个整数𝑎,₁, 𝑎,₂,..., 𝑎,,表示移动序列。

3.1.3 输出格式

输出共 𝑞 行,第 𝑖 行包含一个整数,表示第 𝑖 次旅行终点的结点编号。

3.1.4 样例

3.1.4.1 输入样例1

  1. 5 4
  2. 1 1 2 2
  3. 3 3
  4. 1 -1 -1
  5. 2 5
  6. 1 -1 1 -1 1
  7. 5 8
  8. 1 1 1 -1 -1 -1 -1 -1
  9. 5 3
  10. -1 -1 1

3.1.4.2 输出样例1

  1. 4
  2. 1
  3. 4
  4. 2

3.1.4.3 输入样例2

  1. 8 3
  2. 5 4 2 1 3 6 6
  3. 8 1
  4. 8
  5. 8 2
  6. 8 -8
  7. 8 3
  8. 8 -8 8

3.1.4.4 输出样例2

  1. 1
  2. 7
  3. 1

3.1.5 数据范围

对于所有测试点,保证1 ≤ 𝑛 ≤ 10⁵,1 ≤ 𝑞 ≤ 10⁴,1 ≤ 𝑝 ≤ 𝑛,1 ≤ 𝑠 ≤ 𝑛,𝑘 ≥ 1且Σ𝑘 ≤ 10⁵,1 ≤ | 𝑎, | ≤ 𝑛。

3.1.6编写程序思路

分析:题目要求在一棵有𝑛个结点的有根树上进行𝑞次旅行。在第𝑖次旅行中,小A首先会选定结点𝑠作为起点,并移动若干次。移动分为以下两种:(1) 移动至当前结点的父结点。(2) 移动至当前结点的所有子结点中编号最小的结点。对于第 𝑖 次旅行,旅行中的移动将以𝑘个不为零的整数构成的序列表示。若𝑎, > 0则代表进行𝑎,次第一种移动;若𝑎, < 0则代表进行- 𝑎,次第二种移动。给定每次旅行的起点与移动序列,求出旅行终点的结点编号。

对样例15个结点,4次旅行,为根结点,的父结点为的父结点为的父结点为的父结点为,对应的树如下图所示:

4次旅行:第1次旅行从开始,移动长度为3,第1次移动1为第1种移动(移向父结点),移1步到2次移动-1为第2种移动(移向其编号最小结点),移1步到②(②),第3次移动-1为第2种移动(移向其编号最小结点),移1步到④(④);第2次旅行从开始,移动长度为5,第1次移动1为第1种移动,移1步到2次移动-1为第2种移动,移1步到,第3次移动1为第1种移动,移1步到4次移动-1为第2种移动,移1步到,第5次移动1为第1种移动,移1步到3次旅行从开始,移动长度为8,第1次移动1为第1种移动,移1步到2次移动1为第1种移动,移1步到,第3次移动1为第1种移动,已到根结点,不能再上移;4次移动-1为第2种移动,移1步到,第5次移动-1为第2种移动,移1步到,已到叶子结点,不能再向下移(2种移动);第4次旅行从开始,移动长度为3,第1次移动-1为第2种移动,为叶子结点不能再下移2次移动-1为第2种移动,不能下移,第3次移动1为第1种移动,移1步到1~4次旅行终点的结点编号分别为4142

方法一:

数据结构设计:使用vector存储父结点关系(parent数组)

使用vector<vector<int>>存储子结点关系(children数组)

预处理时对每个结点的子结点按编号排序。

移动处理逻辑:向上移动:直接访问父结点,直到根结点或移动次数用完;向下移动:访问已排序子结点列表的第一个元素(最小编号),直到叶子结点或移动步数用完。

使用快速输入输出(ios::sync_with_stdio),提高输入、输出速度。预处理子结点排序,保证访问最小子结点时间复杂度为O(1)。时间复杂度为O(n log n + qk),适用于大规模数据输入。

对本题𝑛10⁵𝑞≤2×10⁴𝑘≤2×10⁴𝑞𝑘=Σ𝑘O(n log n + qk)= O(10×16.61 + 10)≈1761000,在10⁶级,不会超时。完整参考程序代码如下:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int n, q;
    cin >> n >> q;
    
    vector<int> parent(n+1);
    vector<vector<int>> children(n+1);
    
    for(int i=2; i<=n; ++i) {  //构建树结构
        cin >> parent[i];
        children[parent[i]].push_back(i);
    }
    
    for(int i=1; i<=n; ++i) {  //预处理:对子节点排序
        sort(children[i].begin(), children[i].end());
    }
    
    while(q--) {      //处理每个查询
        int s, k;
        cin >> s >> k;
        
        vector<int> moves(k);
        for(int i=0; i<k; ++i) {
            cin >> moves[i];
        }
        
        int current = s;
        for(int move : moves) {
            if(move > 0) { //向上移动
                int steps = move;
                while(steps-- && current != 1) {
                    current = parent[current];
                }
            } else {       //向下移动
                int steps = -move;
                while(steps-- && !children[current].empty()) {
                    current = children[current][0];
                }
            }
        }
        cout << current << '\n';
    }

    return 0;
}

方法二:

数据结构设计:使用一维数组存储父结点关系(par数组),使用二维数组存储子结点关系(chd数组),一般树一个结点的子结点数不大于𝐿=log₂(10⁵)~log₂(𝑛)

预处理时对每个结点的子结点找最小编号子结点,时间复杂度为𝘖(𝐿)。整个过程时间复杂度为𝘖(𝑛×𝐿) ~ 𝘖(𝑛×log(𝑛))

移动处理逻辑:向上移动:直接访问父结点(子结节只有一个父结点),直到移动次数用完或到根结点;向下移动:访问子结点列表的下标为1的元素(最小编号),直到移动步数用完或到叶子结点(子结点列表的下标为0的元素为0)。因预处理子结点,保证访问最小子结点时间复杂度为O(1)。时间复杂度为O(n log n + qk),对本题𝑛10⁵𝑞≤2×10⁴𝑘≤2×10⁴𝑞𝑘=Σ𝑘,则O(n log n + qk)= O(10×16.61 + 10)≈1761000,在10⁶级,不会超时。完整参考程序代码如下:

#include <iostream>
using namespace std;

const int Max_N = 1e5+5; //𝑛≤ 10⁵
const int L = 18; //log₂𝑛≈ 16.61 < 17
int par[Max_N], chd[Max_N][L];

int main() {
         int n, q;
         cin >> n >> q;


         for (int i = 2; i <= n; ++i) { //构建树结构
                   cin >> par[i];
                   chd[par[i]][0] += 1;
                   chd[par[i]][chd[par[i]][0]] = i;
         } //chd[i][0]存储结点i的子结点数

         for (int i = 1; i <= n; ++i) { //预处理:找最小编号子节点
                   int idx = 1;
                   for (int j = 2; j <= chd[i][0]; j++)
                            if (chd[i][j] < chd[i][idx]) idx = j;
                   if (idx != 1) {
                            int tmp = chd[i][1];
                            chd[i][1] = chd[i][idx];
                            chd[i][idx] = tmp;
                   }
         } //chd[i][1]为结点i的最小编号子结点

         while (q--) {  //处理每个查询
                   int s, k;
                   cin >> s >> k;
                   int cur = s, m;
                   for (int i = 0; i < k; ++i) {
                            cin >> m;
                            if (m > 0) { //向上移动
                                     int steps = m;
                                     while (steps-- && cur != 1)
                                               cur = par[cur];
                            } else {     //向下移动
                                     int steps = -m;
                                     while (steps-- && !(chd[cur][0] == 0)) //chd[cur][0]=0为叶子结点(子结点为0)
                                               cur = chd[cur][1]; //结点cur的最小编号子结点
                            }
                   }
                   cout << cur << '\n';
         }

         return 0;
}

3.2 编程题2

  • 试题名称:遍历计数
  • 时间限制:1.0 s
  • 内存限制:512.0 MB

3.2.1 题目描述

给定一棵有𝑛个结点的树𝑇,结点依次以1,2,...,标号。树𝑇的深度优先遍历序可由以下过程得到:

1. 选定深度优先遍历的起点𝑠(1 ≤ 𝑠 ≤ 𝑛),当前所在结点即是起点。

2. 若当前结点存在未被遍历的相邻结点𝑢则遍历𝑢,也即令当前所在结点为𝑢并重复这一步;否则回溯。

3. 按照遍历结点的顺序依次写下结点编号,即可得到一组深度优先遍历序。

第一步中起点的选择是任意的,并且第二步中遍历相邻结点的顺序是任意的,因此对于同一棵树𝑇可能有多组不同的深度优先遍历序。请你求出树𝑇有多少组不同的深度优先遍历序。由于答案可能很大,你只需要求出答案对10⁹取模之后的结果。

3.2.2 输入格式 第一行,一个整数 𝑛,表示树 𝑇 的结点数。

接下来 𝑛-1 行,每行两个正整数𝑢, 𝑣,表示树 𝑇 中的一条连接结点𝑢, 𝑣的边。

3.2.3 输出格式

输出一行,一个整数,表示树 𝑇 的不同的深度优先遍历序数量对10⁹取模的结果。

3.2.4 样例

3.2.4.1 输入样例1

  1. 4
  2. 1 2
  3. 2 3
  4. 3 4

3.2.4.2 输出样例1

  1. 6

3.2.4.3 输入样例2

  1. 8
  2. 1 2
  3. 1 3
  4. 1 4
  5. 2 5
  6. 2 6
  7. 3 7
  8. 3 8

3.2.4.4 输出样例2

  1. 112

3.2.5 数据范围

对于 40% 的测试点,保证 1 ≤ 𝑛 ≤ 8。

对于另外 20% 的测试点,保证给定的树是一条链。

对于所有测试点,保证 1 ≤ 𝑛 ≤ 10⁵。

3.2.6编写程序思路

分析:DFS序的数量取决于两个因素:(1)‌起点选择𝑛个结点共有 𝑛 种可能的起点(𝑠[1, 𝑛])。

(2)‌子树的遍历顺序:对于每个非叶子节点,其子树的遍历顺序是任意的,若节点有𝑘个子树,则贡献𝑘DFS序列。如样例1:给定一棵有𝑛个结点的树𝑇已退化为为链表,如下图(a)所示。如选为起点,DFS遍历序各只有一种(下图(a)(b)) ;选为起点,DFS遍历序有两种(下图(c)(d));选为起点,DFS遍历序也有两种(下图(e)(f))。合计共6种。

如样例2:按所给数据得树如下图所示。

为起点,DFS遍历序为3!*2!*2! =24种;为起点,DFS遍历序为3!*2!*2!=24种;为起点,DFS遍历序为3!*2!*2!=24种;为起点,DFS遍历序为1!*2!*2!*2!=8种;为起点,DFS遍历序为1!*2!*2!*2!=8种;为起点,DFS遍历序为1!*2!*2!*2!=8种;为起点,DFS遍历序为1!*2!*2!*2!=8种;为起点,DFS遍历序为1!*2!*2!*2!=8种。合计:24+24+24+8+8+8+8+8=112种。

方法一:

树的DFS遍历序数量取决于两个因素:(1)‌起点选择:共有n种可能的起点(𝑠[1,𝑛])。(2)‌子树的遍历顺序:对于每个非叶子节点,其子树的遍历顺序是任意的,若节点有𝑘 个子树,则贡献𝑘!种排列方式。

其中,degree(𝑢)为结点𝑢的度数。根节点的子树数为 degree(𝑢),其他节点为 degree(𝑢)−1(因为父节点已访问)

时间复杂度为𝑂(𝑛²)。对于 40% 的测试点,保证 1 ≤ 𝑛 ≤ 8𝑂(𝑛²)=𝑂(0.4×8²)≈26,可忽略;对于另外60%的测试点,保证 1 ≤ 𝑛 ≤ 10⁵𝑂(𝑛²)=𝑂(0.6×(10⁵)²)=6×10⁹。会超时。完整程序代码如下:

#include <iostream>
using namespace std;

const int MOD = 1e9;
const int N = 1e5 + 5;
int deg[N];
long long fac[N], res;

int main() {
    int n;
    cin >> n;
    for (int i = 1; i < n; i++) {
        int u, v;
        cin >> u >> v;
        deg[u] += 1;
        deg[v] += 1;
    }
    fac[0] = 1; //预计算n!,时间复杂度O(n),查询O(1)
    for (int i = 1; i <= n; i++)
        fac[i] = (fac[i - 1] * i) % MOD;
    res = 0;
    for (int i = 1; i <= n; i++) {
        long long res1 = 1;
        for (int j = 1; j <= n; j++)
            if (i == j)
                res1 *= fac[deg[j]] % MOD;
            else
                res1 *= fac[(deg[j] - 1)] % MOD;
        res += res1 % MOD;
    }
    cout << res << endl;
    return 0;
}

方法二:

为解决超时问题,可解决如下的双重循环:

    res = 0;
    for (int i = 1; i <= n; i++) {
        long long res1 = 1;
        for (int j = 1; j <= n; j++)
            if (i == j)
                res1 *= fac[deg[j]] % MOD;
            else
                res1 *= fac[(deg[j] - 1)] % MOD;
        res += res1 % MOD;
    }

先简化方法一的公式

因为为常数(可用一重循环求得),这样双循环就降为两个并列一重循环的结果乘积。

    long long res1 = 1, res = 0;
    for (int j = 1; j <= n; j++) //每个结点扣除父结点的∏((deg(u)-1)!)
        res1 = (res1 * fac[(deg[j] - 1)]) % MOD;
    for (int i = 1; i <= n; i++) //根结点没有父结点,补乘deg[i]=deg(u)!/(deg(u)-1)!
        res = (res + res1 * deg[i]) % MOD;

𝑛=1时,deg[1]=0,求fac[deg[1]]=1正确,求deg[1]*fac[deg[1]-1]时,求fac[deg[1]-1]= fac[-1]会越界,乘deg[1]会变0,而不是1。所以𝑛=1要单独处理,直接返回1

时间复杂度为𝑂(𝑛),不会超时。完整程序代码如下:

#include <iostream>
using namespace std;

const int MOD = 1e9;
const int N = 1e5 + 5;
int deg[N], fac[N];

int main() {
    int n;
    cin >> n;
    if (n == 1) { //n=1时直接返回1
        cout << 1 << endl;
        return 0;
    }
    for (int i = 1; i < n; i++) { //统计结点连接边数
        int u, v;
        cin >> u >> v;
        deg[u] += 1;
        deg[v] += 1;
    }
    fac[0] = 1; //预计算n!,时间复杂度O(n),查询O(1)
    for (int i = 1; i <= n; i++)
        fac[i] = (1ll * fac[i - 1] * i) % MOD; //1ll保证计算不溢出
    long long res1 = 1, res = 0;
    for (int j = 1; j <= n; j++) //每个结点扣除父结点的∏((deg(u)-1)!)
        res1 = (res1 * fac[(deg[j] - 1)]) % MOD;
    for (int i = 1; i <= n; i++) //根结点没有父结点,补乘deg[i]=deg(u)!/(deg(u)-1)!
        res = (res + res1 * deg[i]) % MOD;
    cout << res << endl;
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值