2024年12月真题
一、单选题(每题2分,共30分)
正确答案:D
解析:面向对象编程(OOP)的重要特性:抽象、封装、继承
抽象是将现实世界中的事物简化为计算机可以理解和处理的模型。
封装是将数据和操作数据的方法组合在一起,并且对外部隐藏对象的内部细节。
继承允许创建新的类(子类),子类可以继承父类的属性和方法。
模块化是一种程序设计方法,它将程序分解为独立的模块,每个模块有自己的功能,模块之间通过接口进行通信。虽然模块化在程序设计中很重要,但它不是面向对象编程特有的特性。过程式编程等其他编程范式也强调模块化。
正确答案:C
解析:
选择A:在 C++ 中,类中定义的成员变量和成员函数默认是 private(私有)访问权限,而不是 public(公有)
选项B:构造函数是一种特殊的成员函数,它用于初始化类的对象。构造函数没有返回类型,包括void也不能写。
选项D:同一个类的实例有各自的成员数据,但是它们共享成员函数的代码。
选项C:在 C++ 中,类的数据一般设置为私有(private)。这样可以隐藏数据的具体实现细节,提高数据的安全性。公有成员函数(public member functions)提供了访问私有数据的唯一途径。
正确答案:C
解析:
选项A:在 C++ 中,NULL可以用于指针初始化。nullptr是 C++11 引入的更安全的空指针常量,但NULL并没有不能用于指针初始化的说法,所以 A 错误。
选项B:obj定义为指针类型是可以的,这里的问题不在于它是指针类型,而在于它没有指向有效的对象,所以 B 错误。
选项D:由于obj是NULL,程序在执行到obj->display()时会出现运行时错误,根本不会调用display函数,所以 D 错误。
选项C:因为obj是NULL,调用obj->display()会导致空指针访问错误。应该将obj初始化为一个有效的对象,C 正确。
正确答案:B
解析:栈进出特点:先进后出,后进先出;队列进出特点:先进先出,后进后出;
因此栈的输出顺序为:5 4 3 2 1,队列的输出顺序为:1 2 3 4 5。B正确
正确答案:B
解析:双向循环链是一种数据结构,每个节点有两个指针,分别指向前一个节点和后一个节点,并且头节点和尾节点相互连接形成一个环。
在双向循环链中,没有索引等辅助结构来快速定位节点。要查找一个节点,最坏的情况是遍历整个链表,需要遍历个节点。平均情况下,也需要遍历大约一半的节点,时间复杂度仍然是
O
(
n
)
O(n)
O(n)。
正确答案:B
解析:
选项A:度为0的结点称为叶子结点
选项C:所有结点的度之和,就是所有结点的度之和。叶子结点的度为0。
选项D:先序遍历:访问根结点——>先序遍历左子树——>先序遍历右子树;中序遍历:中序遍历左子树——>访问根结点——>中序遍历右子树;只有所有结点都不存在左子树的情况下,先序遍历和中序遍历的结果才相同。
选项B:满二叉树每一层的结点数为
2
层数
−
1
2^{层数-1}
2层数−1,高度为 h 的满二叉树的结点数为:
2
h
−
1
2^h-1
2h−1。
正确答案:A
解析:哈夫曼编码(Huffman Coding)是一种用于数据无损压缩的编码方法。哈夫曼编码基于这样一个原则:出现频率高的字符(数据元素)用较短的编码表示,出现频率低的字符用较长的编码表示。这样可以使得编码后的字符串总长度尽可能短,从而达到数据压缩的目的。
哈夫曼编码的实现依赖于哈夫曼树(Huffman Tree)的构建。哈夫曼树是一种二叉树,它的叶子节点代表要编码的字符,每个叶子节点都有一个权值,这个权值通常是字符出现的频率。
构建过程是一个不断合并节点的过程。首先,将所有字符(节点)按照其频率从小到大排序,然后选择频率最小的两个节点合并为一个新节点。新节点的频率是被合并的两个节点频率之和。这个过程不断重复,直到所有节点都被合并成一棵树。
答案为A
正确答案:C
解析:一旦哈夫曼树构建完成,就可以为每个字符生成编码。从根节点开始,向左分支标记为 0,向右分支标记为 1(或者反之)。每个字符的编码就是从根节点到该字符所在叶子节点的路径上的标记序列。答案选C
正确答案:A
解析:格雷码(Gray Code)是一种特殊的二进制编码方式。它的特点是任意两个相邻的代码只有一位二进制数不同。答案为A
二进制码转格雷码:
设二进制码为:
B
=
B
n
−
1
B
n
−
2
.
.
.
B
1
B
0
B=B_{n-1}B_{n-2}...B_1B_0
B=Bn−1Bn−2...B1B0,对应的格雷码为:
G
=
G
n
−
1
G
n
−
2
.
.
.
G
1
G
0
G=G_{n-1}G_{n-2}...G_1G_0
G=Gn−1Gn−2...G1G0,转换规则是:
G
n
−
1
=
B
n
−
1
G_{n-1}=B_{n-1}
Gn−1=Bn−1,
G
i
=
B
i
+
1
⊕
B
i
(
i
=
0
,
1
,
.
.
.
,
n
−
2
)
G_i=B_{i+1} \oplus B_i(i=0,1,...,n-2)
Gi=Bi+1⊕Bi(i=0,1,...,n−2),其中
⊕
\oplus
⊕表示异或运算。
格雷码转二进制码:
设格雷码为:
G
=
G
n
−
1
G
n
−
2
.
.
.
G
1
G
0
G=G_{n-1}G_{n-2}...G_1G_0
G=Gn−1Gn−2...G1G0,对应的二进制码为:
B
=
B
n
−
1
B
n
−
2
.
.
.
B
1
B
0
B=B_{n-1}B_{n-2}...B_1B_0
B=Bn−1Bn−2...B1B0,转换规则是:
B
n
−
1
=
G
n
−
1
B_{n-1}=G_{n-1}
Bn−1=Gn−1,
B
i
=
B
i
+
1
⊕
G
i
(
i
=
0
,
1
,
.
.
.
,
n
−
2
)
B_i=B_{i+1} \oplus G_i(i=0,1,...,n-2)
Bi=Bi+1⊕Gi(i=0,1,...,n−2),其中
⊕
\oplus
⊕表示异或运算。
正确答案:B
解析:search函数代码分析:
首先,函数输出当前节点的值。
然后判断根节点是否为空或者根节点的值等于要搜索的值,如果满足条件则返回根节点。
如果要搜索的值小于根节点的值,则递归调用search函数在左子树中搜索。
如果要搜索的值大于根节点的值,则递归调用search函数在右子树中搜索。
代码功能:输出值为val的结点的所有祖先结点,包括值为val的结点。
正确答案:B
解析:dfs函数代码分析
函数dfs接受一个二叉树节点指针root作为参数。
如果root为空指针(nullptr),则直接返回。
然后创建了一个栈s来辅助实现深度优先搜索。将根节点root压入栈s中。
进入while循环,只要栈s不为空,就执行循环体。
在循环体中,需要从栈顶取出节点进行访问(目前这部分代码缺失,需要在横线上填写)。
访问完节点后,如果该节点有右子节点,则将右子节点压入栈;如果有左子节点,也将左子节点压入栈。
缺失代码部分要做的事:取出栈顶的元素。B选项正确。
正确答案:D
解析:这段代码的目的是实现二叉树的广度优先搜索(BFS)。
首先,函数bfs接受一个二叉树节点指针root作为参数。如果root为空(NULL),则直接返回,不进行搜索。
然后,定义了一个queue(队列)q来存储待访问的节点。将根节点root压入队列q中。
接着,进入while循环,只要队列q不为空,就会循环执行。在每次循环中,需要从队列中取出一个节点进行处理(这部分代码需要补全),然后将该节点的左子节点(如果存在)和右子节点(如果存在)依次压入队列。
缺失代码部分要做的事:取出队首的元素。D选项正确。
正确答案:C
解析:宽度优先搜索算法遍历树,即按层进行遍历,答案为C
正确答案:B
解析:
A 选项:动态规划的关键在于处理重叠子问题,通过保存子问题的解来避免重复计算。
C 选项:. 动态规划一般有两种实现方式
自顶向下(带记忆化的递归):从原始问题开始,递归地分解问题为子问题。在这个过程中,使用一个数据结构(如数组或哈希表)来存储已经计算过的子问题的解。当需要计算一个子问题时,先检查是否已经计算过,如果是,则直接返回存储的解,否则进行计算并存储结果。
自底向上(迭代):从最基础的子问题开始求解,逐步构建出更复杂的子问题的解,直到得到原始问题的解。通常使用循环来实现。
D 选项:贪心算法是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。它的决策过程是局部最优的,每一步都只考虑当前的最佳选择,而不考虑整体的最优解是如何由子问题的最优解构成的。
具有重叠子问题的题目也可以使用贪心算法,但它不会像动态规划那样去记录和利用子问题的解来避免重复计算。
以活动安排问题为例,假设有一系列活动,每个活动都有开始时间和结束时间。目标是选择尽可能多的相互兼容的活动。贪心算法可以按照活动结束时间的先后顺序对活动进行排序,然后依次选择活动。在这个过程中,会有重叠子问题,比如在判断某个活动是否与已选活动兼容时,可能会多次比较相同的活动。但是贪心算法不会像动态规划那样去记录之前比较的结果,它只是每次根据当前的排序规则(结束时间先后)做出选择。
B 选项:动态规划要求问题具有最优子结构和无后效性。最优子结构意味着问题的最优解可以由子问题的最优解构造出来,无后效性意味着当前的决策不会影响到之前的状态。
正确答案:C
解析:动态规划典型题目:0/1 背包问题。
定义状态:设
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 表示前
i
i
i 个物品放入容量为
j
j
j 的背包的最大价值。
状态转移方程:
d
p
[
i
]
[
j
]
=
{
d
p
[
i
−
1
]
[
j
]
,
w
i
>
j
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
w
i
]
+
v
i
)
,
w
i
<
=
j
dp[i][j]=\begin{cases} dp[i-1][j],w_i>j \\max(dp[i-1][j], dp[i-1][j-w_i]+v_i),w_i<=j \end{cases}
dp[i][j]={dp[i−1][j],wi>jmax(dp[i−1][j],dp[i−1][j−wi]+vi),wi<=j
边界条件:当没有物品时
i
=
=
0
i==0
i==0 或者背包容量为
0
0
0 时
j
=
=
0
j==0
j==0,最大价值为
0
0
0,即
d
p
[
0
]
[
j
]
=
0
dp[0][j]=0
dp[0][j]=0,
d
p
[
i
]
[
0
]
=
0
dp[i][0]=0
dp[i][0]=0。
本题计算结果为C。
二、判断题(每题2分,共20分)
正确答案:正确,错误,正确
解析:
第1题正确:构造函数是一种特殊的成员函数,用于初始化对象。它的名字必须与类名相同。函数重载是指在同一作用域内,可以有一组具有相同函数名但参数列表不同(参数个数不同、参数类型不同或者参数顺序不同)的函数。在类中,可以通过函数重载创建多个构造函数,用于不同的初始化场景。
第2题错误:类的静态成员函数只能访问类的静态数据成员,不能访问非静态数据成员。因为静态成员函数是属于类的,而不是属于某个具体对象的。非静态数据成员是与对象相关联的,在没有具体对象的情况下,静态成员函数不知道要访问哪个对象的非静态数据成员。
第3题正确:栈是一种后进先出(LIFO)的数据结构,元素的插入(入栈)和删除(出栈)操作都在栈的顶端进行。单向链表很适合实现栈,因为可以将链表的头部作为栈顶。入栈操作相当于在链表头部插入一个新节点,出栈操作相当于删除链表头部的节点。
正确答案:正确
解析:这段 C++ 代码定义了一个二叉树节点结构TreeNode和一个函数buildCompleteBinaryTree来构建一棵二叉树。
构建了一棵完全二叉树,按层序遍历为:1 2 3 4 5 6
正确答案:错误,错误
解析:
第4题错误:在二叉排序树(二叉搜索树)中,左子树所有节点的值都小于根节点的值,右子树所有节点的值都大于根节点的值。这是二叉排序树的基本性质。
第5题错误:在生成一个派生类的对象时,首先会调用基类的构造函数,然后再调用派生类的构造函数。这是因为派生类继承了基类的成员,在初始化派生类对象时,需要先确保基类部分得到正确的初始化。
正确答案:正确
解析:先访问根节点输出其值——>先序访问左子树——>先序访问右子树。正是二叉树的先序(前序)遍历
正确答案:正确
解析:宽度优先搜索算法是从根节点一层一层遍历的,因此可以保证结点在最短路径下被访问。
正确答案:正确
解析:以上正是0-1背包问题的动态转移方程
正确答案:错误
解析:参见第3题。
三、编程题(每题25分,共50分)
方案1:这么简单吗,直接写。得了40分都算对如此轻视六级编程题的仁慈。WA了60分。
#include<bits/stdc++.h> //万能头文件
using namespace std;
int main() {
long long n, s;
string str;
cin>>n>>s>>str;
for(int i=0; i<n; i++){
if(str[i]=='U' && s/2>=1){
s = s/2;
} else if(str[i]=='L') s = 2*s;
else if(str[i]=='R') s = 2*s+1;
}
cout<<s;
return 0;
}
方案2:回来审题:“数据保证最后的所处的节点编号不超过 1 0 12 10^{12} 1012”,也就是中间操作的过程中结点编号有可能超过 1 0 12 10^{12} 1012,至于超多少, 不知道,题目中还有一句话“小杨有一棵包含无穷节点的二叉树”,也就是说结点的编号会超过long long的范畴,超long long那就高精度吧。还是40分,60分TLE。高精度应对六级是有些不尊重了。
#include<bits/stdc++.h> //万能头文件
using namespace std;
string add(string a, int num){
int tmp, carry=num;
for(int i=a.size()-1; i>=0; i--){
tmp = a[i]-'0'+carry;
carry = tmp/10;
tmp %= 10; a[i] = tmp+'0';
if(carry == 0) break;
}
if(carry > 0) a = char(carry+'0') + a;
return a;
}
string mul(string a, int num){
int tmp, carry=0;
for(int i=a.size()-1; i>=0; i--){
tmp = (a[i]-'0')*num+carry;
carry = tmp/10;
tmp %= 10; a[i] = tmp+'0';
}
if(carry > 0) a = char(carry+'0') + a;
return a;
}
string div(string a, int num){
string res="";
int tmp, r=0;
for(int i=0; i<a.size(); i++){
r = r*10+(a[i]-'0');
tmp = r / num; res += char(tmp+'0');
r %= num;
}
int i=0;
while(i<res.size()-1 && res[i]=='0') i++;
return res.substr(i);
}
int main() {
int n;
long long s;
string str;
cin>>n>>s>>str;
string res = to_string(s);
for(int i=0; i<n; i++){
if(str[i]=='U'){
if(res == "1") continue;
else res = div(res, 2);
} else{
res = mul(res, 2);
if(str[i] == 'R'){
res = add(res, 1);
}
}
}
cout<<res;
return 0;
}
方案3:既然 ‘U’ 表示往父亲节点方向走,结点编号变小, ‘L’,‘R’ 表示往左儿子、右儿子方向走,结点编号变大。而数据范围越界只发生在编号变大的情况下,考虑当编号范围超过 1 0 12 10^{12} 1012 时,不继续变大,而是使用栈进行暂存,一层压一层,相当于往儿子结点方向走,这样下一次遇见编号变小的情况栈弹出(如果栈内有元素),相当于往父亲结点方向走。
#include<bits/stdc++.h> //万能头文件
using namespace std;
#define ll long long
const ll INF = 1e12;
int main() {
int n;
ll s;
string str;
cin>>n>>s>>str;
stack<char> st;
for(int i=0; i<n; i++){
if(str[i]=='U'){
if(s == 1) continue;
if(st.size()){ //栈内有元素,弹出一个相等于往父亲结点方向移一下
st.pop();
continue;
}
s >>= 1; //使用位运算符,比算术运算符快
} else if(str[i]=='L') {
if((s<<1) > INF){ //如果超过1^12,编号不在增大,使用压栈代替往下移动
st.push('L');
continue;
}
s <<= 1; //使用位运算符,比算术运算符快
}else{
if((s<<1|1) > INF){ //如果超过1^12,编号不在增大,使用压栈代替往下移动
st.push('R');
continue;
}
s = s<<1 | 1; //使用位运算符,比算术运算符快
}
}
cout<<s;
return 0;
}
-
对于单个货车 j j j 来说,假定其在站点 i i i,则其行驶路程为: 2 p i a j + 2 ( x − p i ) b j 2p_ia_j + 2(x-p_i)b_j 2piaj+2(x−pi)bj = 2 x b j + 2 p i ( a j − b j ) 2xb_j+2p_i(a_j-b_j) 2xbj+2pi(aj−bj),其中 2 x b j 2xb_j 2xbj 的大小是固定的,则要想货车 j j j 的行驶路程最短,只需要 2 p i ( a j − b j ) 2p_i(a_j-b_j) 2pi(aj−bj) 最小。当 a j > b j a_j>b_j aj>bj, p i p_i pi越小越好;当 a j < b j a_j<b_j aj<bj, p i p_i pi越大越好;当 a j = b j a_j=b_j aj=bj,货车 j j j 的行驶路程固定为 2 x a j 2xa_j 2xaj 或者 2 x b j 2xb_j 2xbj。
-
本题中每个站点可容纳车辆数是有限制的,因此并不一定能保证每个货车都可以达成最短行驶路程,也因此直接累加每个货车的最短行驶路程并不一定能求得所有货车每天的最短总行驶路程。
那到底给某个站点安排那些货车呢?
现有两辆货车,向a、b点运送物资次数为 a j , b j , j = 1 , 2 a_j, b_j, j=1,2 aj,bj,j=1,2,有两个站点都只能安排一辆货车,站点位置 p i , i = 1 , 2 p_i, i=1,2 pi,i=1,2
考虑货车 1 1 1 安排给 站点 1 1 1,考虑货车 2 2 2 安排给 站点 2 2 2,行驶路程为: c 1 = 2 x b 1 + 2 p 1 ( a 1 − b 1 ) + 2 x b 2 + 2 p 2 ( a 2 − b 2 ) c1 = 2xb_1+2p_1(a_1-b_1) + 2xb_2+2p_2(a_2-b_2) c1=2xb1+2p1(a1−b1)+2xb2+2p2(a2−b2)
考虑货车 2 2 2 安排给 站点 1 1 1,考虑货车 1 1 1 安排给 站点 2 2 2,行驶路程为: c 2 = 2 x b 2 + 2 p 1 ( a 2 − b 2 ) + 2 x b 1 + 2 p 2 ( a 1 − b 1 ) c2 = 2xb_2+2p_1(a_2-b_2) + 2xb_1+2p_2(a_1-b_1) c2=2xb2+2p1(a2−b2)+2xb1+2p2(a1−b1)
c 1 − c 2 = 2 ( p 1 − p 2 ) ( ( a 1 − b 1 ) − ( a 2 − b 2 ) ) c1-c2=2(p_1-p_2)((a_1-b_1)-(a_2-b_2)) c1−c2=2(p1−p2)((a1−b1)−(a2−b2))
要想 c 1 c1 c1 小于 c 2 c2 c2,当 p 1 < p 2 p_1<p_2 p1<p2,要使得 ( a 1 − b 1 ) > ( a 2 − b 2 ) (a_1-b_1)>(a_2-b_2) (a1−b1)>(a2−b2);当 p 1 > p 2 p_1>p_2 p1>p2,要使得 ( a 1 − b 1 ) < ( a 2 − b 2 ) (a_1-b_1)<(a_2-b_2) (a1−b1)<(a2−b2) -
结论:当a>b时,贪心地为a−b较大的货车选择p较小的站点;当a<b时,贪心地为a-b较小的货车选择p较大的站点;
#include<bits/stdc++.h> //万能头文件
using namespace std;
#define ll long long
const ll N=1e5+10;
struct node {
ll x, y;
} station[N], truck1[N], truck2[N];
//按运输站点升序排序
bool cmp1(node a, node b) {
return a.x<b.x;
}
//将货车按照 a-b 进行降序排序,a、b为向a、b市运输的次数
bool cmp2(node a, node b) {
return a.x-a.y > b.x-b.y;
}
int main() {
ll n, m, x, a, b;
cin>>n>>m>>x;
for(int i=1; i<=n; i++) {
cin>>station[i].x>>station[i].y;
}
sort(station+1, station+n+1, cmp1);
int cnt1=0, cnt2=0; //cnt1,cnt2分别表示满足a>=b, a<b的货车数量
ll totalCost=0;
for(int i=1; i<=m; i++) {
cin>>a>>b;
if(a>=b) {
cnt1++;
truck1[cnt1].x=a;
truck1[cnt1].y=b;
} else if(a<b) {
cnt2++;
truck2[cnt2].x=a;
truck2[cnt2].y=b;
}
totalCost += x*b;
}
sort(truck1+1, truck1+cnt1+1, cmp2);
sort(truck2+1, truck2+cnt2+1, cmp2);
ll l=1, r=n;
//当a>=b时,贪心地为a-b较大的货车选择p较小的站点
for(int i=1; i<=cnt1; i++){
while(l<=n && station[l].y==0) l++;
totalCost += station[l].x*(truck1[i].x-truck1[i].y);
station[l].y--;
}
//当a<b时,贪心地为a-b较小的货车选择p较大的站点
for(int i=cnt2; i>=1; i--){
while(r>=1 && station[r].y==0) r--;
totalCost += station[r].x*(truck2[i].x-truck2[i].y);
station[r].y--;
}
cout<<totalCost*2;
return 0;
}