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卷座位:选出的2名A卷同学可以分配到座位2和座位5,有 2! = 2 种排列方式(因为两个A卷同学是不同的,可以交换位置)。分配B卷同学到B卷座位:剩下的4名B卷同学需要分配到4个B卷座位有 4! = 24 种方式。总方案数 = 选择A卷同学的方式 × 分配A卷同学到座位的方式 × 分配B卷同学到座位的方式=
×2!×4! = 15 × 2 × 24 = 720 种。故选A。
第2题 又到了毕业季,学长学姐们都在开心地拍毕业照。现在有3位学长、3位学姐希望排成一排拍照,要求男生不相邻、女生不相邻。请问共有多少种拍照方案?( )。
A. 720 B. 72 C. 36 D. 2
解析:答案B。3位学长、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。
解析:答案D。A. 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条边的无向图,它本身就是自己的生成树。
解析:答案D。A. 无向连通图必然存在生成树。因为生成树的定义要求包含所有顶点且无环,而连通图本身已满足顶点间路径连通性,通过删除环边即可得到生成树,正确。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=1,b=a,c=b,实根的条件a²-4b≥0,即a*a-4*b>=0,也就是a*a>=4*b或4*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的个数。该函数的时间复杂度为( )。
- #include <string>
- int count_digit(int n, char d) {
- int cnt = 0;
- std::string s = std::to_string(n);
- for (int i = 0; i < s.length(); i++)
- if (s[i] == d)
- cnt++;
- return cnt;
- }
- int sum_digit(int n, char d) {
- int sum = 0;
- for (int i = 1; i <= n; i++)
- sum += count_digit(i, d);
- return sum;
- }
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题 下面程序的输出为( )。
- #include <iostream>
- const int N = 10;
- int ch[N][N][N];
- int main() {
- for (int x = 0; x < N; x++)
- for (int y = 0; y < N; y++)
- for (int z = 0; z < N; z++)
- if (x == 0 && y == 0 && z == 0)
- ch[x][y][z] = 1;
- else {
- 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];
- }
- std::cout << ch[1][2][3] << std::endl;
- return 0;
- }
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、y 或 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 函数的时间复杂度为( )。
- int gcd(int a, int b) {
- if (a == 0)
- return b;
- return gcd(b % a, a);
- }
- int count_triple(int n) {
- int cnt = 0;
- for (int v = 1; v * v * 4 <= n; v++)
- for (int u = v + 1; u * (u + v) * 2 <= n; u += 2)
- if (gcd(u, v) == 1) {
- int a = u * u - v * v;
- int b = u * v * 2;
- int c = u * u + v * v;
- cnt += n / (a + b + c);
- }
- return cnt;
- }
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 函数试图实现快速排序算法,两处横线处分别应该填入的是( )。
- void swap(int & a, int & b) {
- int temp = a; a = b; b = temp;
- }
- int partition(int a[], int l, int r) {
- int pivot = a[l], i = l + 1, j = r;
- while (i <= j) {
- while (i <= j && a[j] >= pivot)
- j--;
- while (i <= j && a[i] <= pivot)
- i++;
- if (i < j)
- swap(a[i], a[j]);
- }
- ________; // 在此处填入选项
- return ________; // 在此处填入选项
- }
- void quick_sort(int a[], int l, int r) {
- if (l < r) {
- int pivot = partition(a, l, r);
- quick_sort(a, l, pivot - 1);
- quick_sort(a, pivot + 1, r);
- }
- }
A. |
|
B. |
|
C. |
|
D. |
|
解析:答案D。这是一个快速排序函数,两处横线都在求基准值(pivot)选择子函数partition()之中。基准选择:以 a[l] 为基准值(pivot),初始化指针 i = l + 1(从左向右扫描)、j = r(从右向左扫描)。j 从右向左移动,找到第一个小于 pivot 的元素;i 从左向右移动,找到第一个大于 pivot 的元素。若 i < j,交换 a[i] 和 a[j],确保左侧元素 ≤ pivot,右侧元素 ≥ pivot。
终止条件是当 i > j 时,循环结束。此时 j 指向最后一个≤ pivot 的元素,i 指向第一个> 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 函数试图求出最长上升子序列的长度,横线处应该填入的是( )。
- int max(int a, int b) {
- return (a > b) ? a : b;
- }
- int LIS(vector & nums) {
- int n = nums.size();
- if (n == 0)
- return 0;
- vector dp(n, 1);
- int maxLen = 1;
- for (int i = 1; i < n; i++) {
- for (int j = 0; j < i; j++)
- if (nums[j] < nums[i])
- ________; // 在此处填入选项
- maxLen = max(maxLen, dp[i]);
- }
- return maxLen;
- }
A. |
|
B. |
|
C. |
|
D. |
|
解析:答案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 函数试图求出最长上升子序列的长度,其时间复杂度为( )。
- #define INT_MIN (-1000)
- int LIS(vector & nums) {
- int n = nums.size();
- vector tail;
- tail.push_back(INT_MIN);
- for (int i = 0; i < n; i++) {
- int x = nums[i], l = 0, r = tail.size();
- while (l < r) {
- int mid = (l + r) / 2;
- if (tail[mid] < x)
- l = mid + 1;
- else
- r = mid;
- }
- if (r == tail.size())
- tail.push_back(x);
- else
- tail[r] = x;
- }
- return tail.size() - 1;
- }
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 → 1:w=5,1 → 3:w=7,总权重:5+7=12
路径 0 → 2 → 3:0 → 2:w=8,2 → 3:w=3,总权重:8+3=11
路径 0 → 1 → 2 → 3:0 → 1:w=5,1 → 2:w=1,2 → 3:w=3,总权重:5+1+3=9
路径 0 → 2 → 1 → 3:0 → 2:w=8,2 → 1:w=1,1 → 3:w=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²−2ab⋅cosθ,即。
其中:a、b为已知两边长度,θ为两边的夹角。
周长为:。
所以题目给出的“三角形的周长可以通过表达式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
- 5 4
- 1 1 2 2
- 3 3
- 1 -1 -1
- 2 5
- 1 -1 1 -1 1
- 5 8
- 1 1 1 -1 -1 -1 -1 -1
- 5 3
- -1 -1 1
3.1.4.2 输出样例1
- 4
- 1
- 4
- 2
3.1.4.3 输入样例2
- 8 3
- 5 4 2 1 3 6 6
- 8 1
- 8
- 8 2
- 8 -8
- 8 3
- 8 -8 8
3.1.4.4 输出样例2
- 1
- 7
- 1
3.1.5 数据范围
对于所有测试点,保证1 ≤ 𝑛 ≤ 10⁵,1 ≤ 𝑞 ≤ 10⁴,1 ≤ 𝑝ᵢ ≤ 𝑛,1 ≤ 𝑠ᵢ ≤ 𝑛,𝑘ᵢ ≥ 1且Σ𝑘ᵢ ≤ 10⁵,1 ≤ | 𝑎ᵢ,ⱼ | ≤ 𝑛。
3.1.6编写程序思路
分析:题目要求在一棵有𝑛个结点的有根树上进行𝑞次旅行。在第𝑖次旅行中,小A首先会选定结点𝑠ᵢ作为起点,并移动若干次。移动分为以下两种:(1) 移动至当前结点的父结点。(2) 移动至当前结点的所有子结点中编号最小的结点。对于第 𝑖 次旅行,旅行中的移动将以𝑘ᵢ个不为零的整数构成的序列表示。若𝑎ᵢ,ⱼ > 0则代表进行𝑎ᵢ,ⱼ次第一种移动;若𝑎ᵢ,ⱼ < 0则代表进行- 𝑎ᵢ,ⱼ次第二种移动。给定每次旅行的起点与移动序列,求出旅行终点的结点编号。
对样例1:5个结点,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次旅行终点的结点编号分别为4、1、4、2。
方法一:
数据结构设计:使用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
- 4
- 1 2
- 2 3
- 3 4
3.2.4.2 输出样例1
- 6
3.2.4.3 输入样例2
- 8
- 1 2
- 1 3
- 1 4
- 2 5
- 2 6
- 3 7
- 3 8
3.2.4.4 输出样例2
- 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;
}