CCF-GESP 等级考试 2025年6月认证C++五级真题解析
1 单选题(每题 2 分,共 30 分)
第1题 与数组相比,链表在( )操作上通常具有更高的效率。
A. 随机访问元素 B. 查找指定元素
C. 在已知位置插入或删除节点 D. 遍历所有元素
解析:答案C。与数组相比,链表在插入和删除操作上通常具有更高的效率。这是因为链表采用非连续的链式存储结构,在插入或删除元素时仅需调整相邻节点的指针,无需移动大量元素;而数组基于连续内存空间,插入或删除需移动后续元素以维持连续性,导致更高时间复杂度。故选C。
第2题 下面C++代码实现双向链表。函数 is_empty() 判断链表是否为空,如链表为空返回 true ,否则返回 false 。横线处不能填写( )。
- // 节点结构体
- struct Node {
- int data;
- Node* prev;
- Node* next;
- };
- // 双向链表结构体
- struct DoubleLink {
- Node* head;
- Node* tail;
- int size;
- DoubleLink() {
- head = nullptr;
- tail = nullptr;
- size = 0;
- }
- ~DoubleLink() {
- Node* curr = head;
- while (curr) {
- Node* next = curr->next;
- delete curr;
- curr = next;
- }
- }
- // 判断链表是否为空
- bool is_empty() const {
- _______________________
- }
- };
A. return head == nullptr; B. return tail == nullptr;
C. return head.data == 0; D. return size == 0;
解析:答案C。要判断双向链表是否空,按第14~18行构造函数,在给定的双向链表实现中,is_empty() 函数可以有多种正确的实现方式。以下是可能的正确填写选项:
- return head == nullptr;
- return tail == nullptr;
- return head == nullptr && tail == nullptr;
- return size == 0;
所以不能用head.data == 0判断。故选C。
第3题 基于上题代码正确的前提下,填入相应代码完善append(),用于在双向链表尾部增加新节点,横线上应填写( )。
- void append(int data) {
- Node* newNode = new Node{data, nullptr, nullptr};
- if (is_empty()) {
- head = tail = newNode; }
- else {
- _______________________
- }
- ++size;
- }
A. |
| B. |
|
C. |
| D. |
|
解析:答案D。函数名为append,应该是追加,即新节点添加在链表尾部。尾节点的后继指向新节点(tail->next = newNode;),新节点的前驱为尾节点(newNode->prev = tail;),置新节点为尾节点(tail = newNode;)。故选D。
第4题 下列C++代码用循环链表解决约瑟夫问题,即假设n个人围成一圈,从第一个人开始数,每次数到第k个的人就出圈,输出最后留下的那个人的编号。横线上应填写( )。
- struct Node {
- int data;
- Node* next;
- };
- Node* createCircularList(int n) {
- Node* head = new Node{1, nullptr};
- Node* prev = head;
- for (int i = 2; i <= n; ++i) {
- Node* node = new Node{i, nullptr};
- prev->next = node;
- prev = node;
- }
- prev->next = head;
- return head;
- }
- int fingLastSurvival(int n, int k) {
- Node* head = createCircularList(n);
- Node* p = head;
- Node* prev = nullptr;
- while (p->next != p) {
- for (int count = 1; count < k; ++count) {
- prev = p;
- p = p->next;
- }
- _______________________
- }
- cout << "最后留下的人编号是: " << p->data << endl;
- delete p;
- return 0;
- }
A. |
| B. |
|
C. |
| D. |
|
解析:答案A。这是一个单向循环链表,第28行处要要删除(每次数到第k个的人就出圈)节点p,需要先将p的前驱的后继设为p的后继,然后才能删除p,A.正确。B.p删除后获得p的后继,错误。C.p前驱的后继为p,p删除了链表指向错误。D.删除的是p的后继节点,错误。故选A。
第5题 下列C++代码判断一个正整数是否是质数,说法正确的是( )。
- bool is_prime(int n) {
- if (n <= 1)
- return false;
- if (n == 2 || n == 3 || n == 5)
- return true;
- if (n % 2 == 0 || n % 3 == 0 || n % 5 == 0)
- return false;
- int i = 7;
- int step = 4;
- int finish_number = sqrt(n) + 1;
- while (i <= finish_number) {
- if (n % i == 0)
- return false;
- i += step;
- step = 6 - step;
- }
- return true;
- }
A. 代码存在错误,比如5是质数,但因为 5 % 5 余数是0返回了false
B. finish_number 的值应该是n / 2,当前写法将导致错误
C. 当前 while 循环正确的前提是:所有大于3的质数都符合 6k±1 形式
D. while 循环修改如下,其执行效果和执行时间相同。
- for (int i = 2; i < finish_number; i++) {
- if (n % i == 0)
- return false;
- }
- return true;
解析:答案C。本题中的程序应用了质数的6k±1规则:除了2和3之外的所有质数都可以表示为6k±1的形式,因为若数n能被2或3整除,则n为合数;若数n不能被2或3整除,则其除以6的余数不能是0、2、3、4,只能是1或5(例如5余5、7余1、11余5等),因此必然属于6k±1的形式,C.正确。5因第4行条件成立会返回true,所以A.错误;求质数枚举因数的上限为,而不是n/2,B.错误;原程序只枚举了小于
+1的6k±1形式的数,D.选项程序枚举了2 ~
的所有数,原程序时间会更快一些,D.错误。故选C。
第6题 下列C++代码用两种方式求解两个正整数的最大公约数,说法错误的是( )。
- int gcd0(int big, int small) {
- if (big < small) {
- swap(big, small);
- }
- if (big % small == 0) {
- return small;
- }
- return gcd0(small, big % small);
- }
- int gcd1(int big, int small) {
- if (big < small) {
- swap(big, small);
- }
- for (int i = small; i >= 1; --i) {
- if (big % i == 0 && small % i == 0)
- return i;
- }
- return 1;
- }
A. gcd0()函数的时间复杂度为O(log n)
B. gcd1() 函数的时间复杂度为O(n)
C. 一般说来,gcd0()的效率高于gcd1()
D. gcd1() 中的代码 for (int i = small; i >= 1; --i) 应该修改为 for (int i = small; i > 1; --i)
解析:答案D。A.gcd0()函数为辗转相除法递归求最大公约数,辗转相除法(欧几里得算法)的时间复杂度为O(log(min(a, b))),其中a和b为输入的两个整数,设n=min(a,b),时间复杂度为O(log n),正确;B.gcd1()只有一重循环,时间复杂度为O(n),正确、gcd0()时间复杂度为O(log n),gcd1()的时间复杂度为O(n),C.正确,gcd1()中i=1时返回结果仍然正确,D.错误。故选D。
第7题 下面的代码用于判断整数n是否是质数,错误的说法是( )。
- bool is_prime(int n) {
- if (n <= 1) return false;
- int finish_number = static_cast(sqrt(n)) + 1;
- for (int i = 2; i < finish_number; ++i) {
- if (n % i == 0)
- return false;
- }
- return true;
- }
A. 埃氏筛算法相对于上面的代码效率更高
B. 线性筛算法相对于上面的代码效率更高
C. 上面的代码有很多重复计算,因为不是判断单个数是否为质数,故而导致筛选出连续数中质数的效率不高
D. 相对而言,埃氏筛算法比上面代码以及线性筛算法效率都高
解析:答案D。线性筛算法效率>埃氏筛算法>普通算法。A.正确;B.正确;C.正确;D. 线性筛的算法效率高于埃氏筛,错误。故选D。
第8题 唯一分解定理描述了关于正整数的什么性质?( )
A. 任何正整数都可以表示为两个素数的和。
B. 任何大于1的合数都可以唯一分解为有限个质数的乘积。
C. 两个正整数的最大公约数总是等于它们的最小公倍数除以它们的乘积。
D. 所有素数都是奇数。
解析:答案B。当正整数为奇数时(如3):若分解为2(质数)+1(非质数),不满足条件,当正整数为2时(最小质数):只能表示为1+1,但1不是质数,A.错误。唯一分解定理是数论中的一个重要定理,它描述了任意一个大于1的整数都可以唯一地表示为质数的乘积,B.正确。两个正整数的最大公约数等于它们的乘积除以它们的最小公倍数,C.错误。最小质数2是偶数,D.错误。故选B。
第9题 下面的C++代码,用于求一系列数据中的最大值。有关其算法说法错误的是( )。
- int find_max_recursive(const vector& nums, int left, int right) {
- if (left == right)
- return nums[left];
- int mid = left + (right - left) / 2;
- int left_max = find_max_recursive(nums, left, mid);
- int right_max = find_max_recursive(nums, mid + 1, right);
- return max(left_max, right_max);
- }
- int find_max(const vector& nums) {
- if (nums.empty()) {
- throw invalid_argument("输入数组不能为空");
- }
- return find_max_recursive(nums, 0, nums.size() - 1);
- }
A. 该算法采用分治算法 B. 该算法是递归实现
C. 该算法采用贪心算法 D. 该算法不是递推算法
解析:答案C。分治算法将问题分成多个子问题,分别解决子问题,最后合并子问题的解。贪心算法总是选择当前最优的解,不一定能得到全局最优解。递推算法通过已知的解推导出未知的解。本程序采用递归算法编程,运用了分治算法求数据中的最大值。A.、B.正确。C.错误。因为采用递归算法,所以该算法不是递推算法D.正确。故选C。
第10题 下面的C++代码,用于求一系列数据中的最大值。有关其算法说法错误的是( )。
- int find_max(const vector& nums) {
- if (nums.empty()) {
- throw invalid_argument("输入数组不能为空");
- }
- int max_value = nums[0];
- for (int num : nums) {
- if (num > max_value) {
- max_value = num;
- }
- }
- return max_value;
- }
A. 本题 find_max() 函数采用的是迭代算法
B. 本题 find_max() 函数的时间复杂度为O(n)
C. 和上一题的 find_max() 相比,因为没有递归,所以没有栈的创建和销毁开销
D. 本题 find_max() 函数和上一题的 find_max() 时间复杂度相同
解析:答案D。本题中的程序算法属于迭代比较法,没有递归,时间复杂度为O(n)。上一题分治算法的时间复杂度为O(log n),D.错误。故选D。
第11题 下面的 C++ 代码用于在升序数组 lst 中查找目标值 target 最后一次出现的位置。相关说法,正确的是( )。
- int binary_search_last_occurrence(const vector& lst, int target) {
- if (lst.empty()) return -1;
- int low = 0, high = lst.size() - 1;
- while (low < high) {
- int mid = (low + high + 1) / 2;
- if (lst[mid] <= target) {
- low = mid;
- } else {
- high = mid - 1;
- }
- }
- if (lst[low] == target)
- return low;
- else
- return -1;
- }
A. 当 lst 中存在重复的 target 时,该函数总能返回最后一个 target 的位置,即便 lst 全由相同元素组成
B. 当 target 小于 lst 中所有元素时,该函数会返回 0
C. 循环条件改为 while (low <= high) 程序执行效果相同,且能提高准确性
D. 将代码中 (low + high + 1) / 2 修改为 (low + high) / 2 效果相同
解析:答案A。当存在重复值时,mid会不断向右移动,直到找到最后一个target,A.正确。当target大于Ist中所有元素时,该函数会返回-1而不是0,B.错误。循环条件更改后,当low == high时会导致死循环,C.错误。(low + high + 1) / 2是为了防止死循环和确保偏向右侧,去掉+1会导致mid偏向左侧,无法正确找到最后一个出现位置,D.错误。故选A。
第12题 有关下面C++代码的说法,错误的是( )。
- double sqrt_binary(long long n, double epsilon = 1e-10) {
- if (n < 0) {
- throw invalid_argument("输入必须为非负整数");
- }
- if (n == 0 || n == 1) return n;
- // 阶段1
- long long low = 1, high = n;
- long long k = 0;
- while (low <= high) {
- long long mid = (low + high) / 2;
- long long mid_sq = mid * mid;
- if (mid_sq == n) {
- return mid;
- } else if (mid_sq < n) {
- k = mid;
- low = mid + 1;
- } else {
- high = mid - 1;
- }
- }
- long long next_k = k + 1;
- if (next_k * next_k == n) {
- return next_k;
- }
- // 阶段2
- double low_d = (double)k;
- double high_d = (double)(k + 1);
- double mid;
- while (high_d - low_d >= epsilon) {
- mid = (low_d + high_d) / 2;
- double mid_sq = mid * mid;
- if (mid_sq < n) {
- low_d = mid;
- } else {
- high_d = mid;
- }
- }
- double result = (low_d + high_d) / 2;
- long long check_int = (long long)(result + 0.5);
- if (check_int * check_int == n) {
- return check_int;
- }
- return result;
- }
A. “阶段1”的目标是寻找正整数 n 可能的正完全平方根
B. “阶段2”的目标是如果正整数 n 没有正完全平方根,则在可能产生完全平方根附近寻找带小数点的平方根
C. 代码 check_int = (long long)(result + 0.5) 是检查因浮点误差是否为正完全平方根
D. 阶段2的二分法中 high_d - low_d >= epsilon 不能用于浮点数比较,会进入死循环
解析:答案D。“阶段1”的目标是寻找正整数 n 可能的正完全平方根,A.正确。“阶段2”的目标是如果正整数 n 没有正完全平方根,则在可能产生完全平方根附近寻找带小数点的平方根,B.正确。check_int = (long long)(result + 0.5)四舍五入(消除浮点数误差)为long long型整数,然后判断是否为完全平方数,C.正确,浮点数因为有误差不能用==进行比较相等,可用两者差小于某微小值,大于等于某微小值为不等,high_d - low_d >= epsilon是浮点数比较的常规做法,不会导致死循环,当差值小于epsilon时会终止循环。故选D。
第13题 硬币找零问题中要求找给客户最少的硬币。 coins 存储可用硬币规格,单位为角,假设规格都小于10 角,且一定有1角规格。 amount 为要找零的金额,约定必须为1角的整数倍。输出为每种规格及其数量,按规格从大到小输出,如果某种规格不必要,则输出为0。下面是其实现代码,相关说法正确的是( )。
- const int MAX_COINS = 10;
- int result[MAX_COINS] = {0}; // 假设最多10种面额
- int find_coins(const vector& coins, int amount) {
- sort(coins.begin(), coins.end(), greater());
- int n = coins.size();
- for (int i = 0; i < n; ++i) {
- int coin = coins[i];
- int num = amount / coin;
- result[i] = num;
- amount -= num * coin;
- if (amount == 0) break;
- }
- cout << "找零方案如下:" << endl;
- for (int i = 0; i < n; ++i) {
- cout << sorted_coins[i] << "角需要" << result[i] << "枚" << endl;
- }
- return 0;
- }
A. 上述代码采用贪心算法实现
B. 针对本题具体要求,上述代码总能找到最优解
C. 上述代码采用枚举算法
D. 上述代码采用分治算法
解析:答案A。本题中,硬币币种面值按规格从大到小排序(降序)输出,总是先用大面值币种,属于贪心算法,A.正确,C.、D.错误。设三种币种面值coins=[5,4,1],找零金额amount=8,贪心算法会给出5+1+1+1(4枚硬币,5*1+4*0+1*3),但最优解是4+4(2枚,5*0+4*2+1*0),所以贪心算法不总是最优解,B.错误。故选A。
第14题 关于下述C++代码的快速排序算法,说法错误的是( )。
- int randomPartition(std::vector& arr, int low, int high) {
- int random = low + rand() % (high - low + 1);
- std::swap(arr[random], arr[high]);
- int pivot = arr[high];
- int i = low - 1;
- for (int j = low; j < high; j++) {
- if (arr[j] <= pivot) {
- i++;
- std::swap(arr[i], arr[j]);
- }
- }
- std::swap(arr[i + 1], arr[high]);
- return i + 1;
- }
- void quickSort(std::vector& arr, int low, int high) {
- if (low < high) {
- int pi = randomPartition(arr, low, high);
- quickSort(arr, low, pi - 1);
- quickSort(arr, pi + 1, high);
- }
- }
A. 在randomPartition函数中,变量i的作用是记录大于基准值的元素的边界
B. randomPartition函数随机选择基准值,可以避免输入数据特定模式导致的最坏情况下时间复杂度O(n²)
C. 快速排序平均时间复杂度是O(n log n)
D. 快速排序是稳定排序算法
解析:答案D。在randomPartition函数中,变量i的作用是记录大于基准值的元素的边界,A.
正确。randomPartition函数随机选择基准值,可以避免输入数据特定模式导致的最坏情况下时间复杂度O(n²),B.正确。快速排序平均时间复杂度是O(n log n),C.正确。快速排序是不稳定排序,D.错误。故选D。
第15题 小杨编写了一个如下的高精度除法函数,则横线上应填写的代码为( )。
- const int MAXN = 1005; // 最大位数
- struct BigInt {
- int d[MAXN]; // 存储数字,d[0]是个位,d[1]是十位,...
- int len; // 数字长度
- BigInt() {
- memset(d, 0, sizeof(d));
- len = 0;
- }
- };
- // 比较两个高精度数的大小
- int compare(BigInt a, BigInt b) {
- if(a.len != b.len) return a.len > b.len ? 1 : -1;
- for(int i = a.len - 1; i >= 0; i--) {
- if(a.d[i] != b.d[i]) return a.d[i] > b.d[i] ? 1 : -1;
- }
- return 0;
- }
- // 高精度减法
- BigInt sub(BigInt a, BigInt b) {
- BigInt c;
- for(int i = 0; i < a.len; i++) {
- c.d[i] += a.d[i] - b.d[i];
- if(c.d[i] < 0) {
- c.d[i] += 10;
- c.d[i+1]--;
- }
- }
- c.len = a.len;
- while(c.len > 1 && c.d[c.len-1] == 0) c.len--;
- return c;
- }
- // 高精度除法(a/b,返回商和余数)
- pair<BigInt, BigInt> div(BigInt a, BigInt b) {
- BigInt q, r; // q是商,r是余数
- if(compare(a, b) < 0) { // 如果a<b,商为0,余数为a
- q.len = 1;
- q.d[0] = 0;
- r = a;
- return make_pair(q, r);
- }
- // 初始化余数r为a的前b.len位
- r.len = b.len;
- for(int i = a.len - 1; i >= a.len - b.len; i--) {
- r.d[i - (a.len - b.len)] = a.d[i];
- }
- // 逐位计算商
- for(int i = a.len - b.len; i >= 0; i--) {
- // 把下一位加入余数
- if(r.len > 1 || r.d[0] != 0) {
- for(int j = r.len; j > 0; j--) {
- r.d[j] = r.d[j-1];
- }
- _______________________
- } else {
- r.d[0] = a.d[i];
- r.len = 1;
- }
- // 计算当前位的商
- while(compare(r, b) >= 0) {
- r = sub(r, b);
- q.d[i]++;
- }
- }
- // 确定商的长度
- q.len = a.len - b.len + 1;
- while(q.len > 1 && q.d[q.len-1] == 0) q.len--;
- // 处理余数前导零
- while(r.len > 1 && r.d[r.len-1] == 0) r.len--;
- return make_pair(q, r);
- }
A. |
| B. |
|
C. |
| D. |
|
解析:答案A。A.是模拟手工竖式除法时低位的数"落"下来的操作的标准实现:将新数字放入最低位,增加余数的位数,正确。A.正确,都错了。故选A。
2 判断题(每题 2 分,共 20 分)
第1题 下面C++代码是用欧几里得算法(辗转相除法)求两个正整数的最大公约数,a大于b还是小于b都适用。
- int gcd(int a, int b) {
- while (b) {
- int temp = b;
- b = a % b;
- a = temp;
- }
- return a;
- }
解析:正确。不管a>b还是b>a,经int temp = b; b = a % b; a = temp; 运算,因为a%b的结果一定小于b,即b一定小于a,直到b=0时,a就是最大公约数,所以a大于b还是小于b都不影响结果(只是多一次或少一次循环运算)。故正确。
第2题 假设函数gcd()函数能正确求两个正整数的最大公约数,则下面的lcm()函数能求相应两数的最小公倍数。
- int lcm(int a, int b) {
- return a * b / gcd(a, b);
- }
解析:正确。因为最小公倍数乘以最大公约数等于两数之积,即gcd(a,b) * lcm(a,b) =a*b,所以lcm(a,b) =a*b/ gcd(a,b)。故正确。
第3题 下面的C++代码用于输出每个数对应的质因数列表,输出形如:{5: [5], 6: [2, 3], 7: [7], 8: [2, 2, 2]} 。
- int main() {
- int n, m;
- cin >> n >> m;
- if (n > m) swap(n, m);
- map<int, vector<int>> prime_factor;
- for (int i = n; i <= m; ++i) {
- int j = 2, k = i;
- while (k != 1) {
- if (k % j == 0) {
- prime_factor[i] = prime_factor[i] + j;
- k /= j;
- } else {
- ++j;
- }
- }
- }
- for (auto& p : prime_factor) {
- cout << p.first << ": ";
- for (int v : p.second)
- cout << v << " ";
- cout << endl;
- }
- return 0;
- }
解析:错误。第12行逻辑错误:prime_factor[i]是vector<int>类型,不能直接与整数j相加。应改为push_back操作。故错误。
第4题 下面的C++代码实现归并排序。代码在执行时,将输出一次 HERE 字符串,因为merge()函数仅被调用一次。
- void merge(std::vector& arr, int left, int mid, int right) {
- std::vector temp(right - left + 1);
- int i = left;
- int j = mid + 1;
- int k = 0;
- while (i <= mid && j <= right) {
- if (arr[i] <= arr[j]) {
- temp[k++] = arr[i++];
- } else {
- temp[k++] = arr[j++];
- }
- }
- while (i <= mid) {
- temp[k++] = arr[i++];
- }
- while (j <= right) {
- temp[k++] = arr[j++];
- }
- for (int p = 0; p < k; ++p) {
- arr[left + p] = temp[p];
- }
- }
- void mergeSort(std::vector& arr, int left, int right) {
- if (left >= right) {
- return;
- }
- int mid = left + (right - left) / 2;
- mergeSort(arr, left, mid);
- mergeSort(arr, mid + 1, right);
- std::cout << "HERE";
- merge(arr, left, mid, right);
- }
解析:错误。mergeSort()是递归函数,每次递归调用mergeSort()时,只要数组长度大于1(left < right),就会输出HERE字符串,并调用merge()函数。因此HERE会被输出多次。故错误。
第5题 归并排序的最好、最坏和平均时间复杂度均为O(n log n)。
解析:正确。归并排序的时间复杂度与原始数组的有序程度无关,无论数组是否有序,时间复杂度均为O(n log n)。故正确。
第6 题 查字典这个小学生必备技能,可以把字典视为一个已排序的数组。假设小杨要查找一个音首字母为 g 的单词,他首先翻到字典约一半的页数,发现该页的首字母是 m ,由于字母表中 g 位于 m 之前,所以排除字典后半部分,查找范围缩小到前半部分;不断重复上述步骤,直至找到首字母为 g 的页码。这种查字典的一系列操作可看作二分查找。
解析:正确。二分查找的前提是数据必须是有序的,字典按照字母顺序排列满足这一条件。每次查找都将查找范围缩小一半。题目描述的操作步骤与二分查找算法完全一致。故正确。
第7题 求解下图中A点到D点最短路径,其中A到B之间的12可以理解为距离。求解这样的问题常用Dijkstra算法,其思路是通过逐步选择当前距离起点最近的节点来求解非负权重图(如距离不能为负值)单源最短路径的算法。从该算法的描述可以看出,Dijkstra算法是贪心算法。
解析:正确。Dijkstra算法通过“每一步都选择当前距起点最短的节点”的策略逐步扩展最短路径,完全符合贪心算法的定义。故正确。
第8题 分治算法将原问题可以分解成规模更小的子问题,使得求解问题的难度降低。但由于分治算法需要将问题进行分解,并且需要将多个子问题的解合并为原问题的解,所以分治算法的效率通常比直接求解原问题的效率低。
解析:错误。分治算法确实通过将原问题分解为规模更小的子问题来降低求解难度。子问题通常更容易解决,因为规模减小且结构相似(例如,递归求解)。分治算法的效率并非通常比直接求解低。相反,对于许多问题,分治算法可以显著提高效率,因为它能够减少整体计算复杂度。关键原因:分治算法的分解和合并步骤虽然引入额外开销,但通过减少问题规模(例如,从O(n)到O(log n)或O(n²)到O(n log n)),往往能补偿这些开销,从而在渐近时间复杂度上优于直接方法。故错误。
第 9 题 函数 puzzle 定义如下,则调用 puzzle(7) 程序会无限递归。
- int puzzle(int n) {
- if (n == 1) return 1;
- if (n % 2 == 0) return puzzle(n / 2);
- return puzzle(3 * n + 1);
- }
解析:错误。这是3N+1问题猜想,也称考拉兹猜想,中国通常称为角谷猜想,不论n是多少,n经偶数除以2,奇数乘以3+1,最终n=1,对n=7,依次变为7→22→11→34→17→52→26→13→40→20→10→5→16→8→4→2→1,所以调用 puzzle(7) 程序不会无限递归。故错误。
第10题 如下为线性筛法,用于高效生成素数表,其核心思想是每个合数只被它的最小质因数筛掉一次,时间复杂度为O(n)。
- vector linearSieve(int n) {
- vector is_prime(n + 1, true);
- vector primes;
- for (int i = 2; i <= n; ++i) {
- if (is_prime[i]) {
- primes.push_back(i);
- }
- for (int j = 0; j < primes.size() && i * primes[j] <= n; ++j) {
- is_prime[i * primes[j]] = false;
- if (i % primes[j] == 0) {
- break;
- }
- }
- }
- return primes;
- }
解析:正确。线性筛法(也称欧拉筛法)是一种高效生成质数的算法,核心目标是“用最小质因数筛掉每个合数,且每个合数只被筛一次”。其中第13行的break保证了每个合数都只被其最小质因数筛一次。每个合数n只会被它的最小质因数p筛掉一次,因此总共不会超过n次筛选,所以总时间复杂度是 O(n)。故正确。
3 编程题(每题 25 分,共 50 分)
3.1 编程题1
- 试题名称:奖品兑换
- 时间限制:1.0 s
- 内存限制:512.0 MB
3.1.1 题目描述
班主任给上课专心听讲、认真完成作业的同学们分别发放了若干张课堂优秀券和作业优秀券。同学们可以使用这两种券找班主任兑换奖品。具体来说,可以使用a张课堂优秀券和b张作业优秀券兑换一份奖品,或者使用b张课堂优秀券和a张作业优秀券兑换一份奖品。
现在小A有n张课堂优秀券和m张作业优秀券,他最多能兑换多少份奖品呢?
3.1.2 输入格式
第一行,两个正整数n,m,分别表示小A持有的课堂优秀券和作业优秀券的数量。
第二行,两个正整数a,b,表示兑换一份奖品所需的两种券的数量。
3.1.3 输出格式
输出共一行,一个整数,表示最多能兑换的奖品份数。
3.1.4 样例
3.1.4.1 输入样例1
- 8 8
- 2 1
3.1.4.2 输出样例1
- 5
3.1.4.3 输入样例2
- 314159 2653589
- 27 1828
3.1.4.4 输出样例2
- 1599
3.1.5 数据范围
对于60% 的测试点,保证1≤a,b≤100,1≤n,m≤500。
对于所有测试点,保证1≤a,b≤10⁴,1≤n,m≤10⁹。
3.1.6 编写程序思路
解析:由于可以使用a张课堂优秀券和b张作业优秀券兑换一份奖品,或者使用b张课堂优秀券和a张作业优秀券兑换一份奖品,所以可以打通使用。输入n,m,当n>m时交换n,m,保证n<=m,输入a,b,当a>b时交换a,b,保证a<=b。在n>=a和m>=b的条件计数n-=a,m-=b的次数,减后如n>m时交换n,m,保证n<=m。具体完整代码如下:
#include <iostream>
using namespace std;int n, m, a, b, cnt;int main() {cin >> n >> m;cin >> a >> b;if (n > m) {n ^= m; m ^= n; n ^= m; //交换}if (a > b) {a ^= b; b ^= a; a ^= b; //交换}if (a == b) {cout << (n / a) << endl;return 0;}while (n >= a && m >= b) {cnt++; //计数n -= a;m -= b;if (n > m) {n ^= m; m ^= n; n ^= m; //交换}}cout << cnt << endl;return 0;
}
3.2 编程题2
- 试题名称:最大公因数
- 时间限制:1.0 s
- 内存限制:512.0 MB
3.2.1 题目描述
对于两个正整数a,b,它们的最大公因数记为gcd(a,b)。对于k≥3个正整数c₁,c₂,...,cₖ,它们的最大公因数为:
gcd(c₁,c₂,...,cₖ)= gcd(gcd(c₁,c₂,... cₖ₋₁),cₖ)
给定n个正整数a₁,a₂,...,aₙ以及q组询问。对于第i(1≤i≤q)组询问,请求出a₁+i,a₂+i,...,aₙ+i的最大公因数,也即gcd(a₁+i,a₂+i,...,aₙ+i)。
3.2.2 输入格式
第一行,两个正整数n,q,分别表示给定正整数的数量,以及询问组数。
第二行,n个正整数a₁,a₂,...,aₙ。
3.2.3 输出格式 输出共q行,第i行包含一个正整数,表示a₁+i,a₂+i,...,aₙ+i的最大公因数。
3.2.4 样例
3.2.4.1 输入样例1
- 5 3
- 6 9 12 18 30
3.2.4.2 输出样例1
- 1
- 1
- 3
3.2.4.3 输入样例2
- 3 5
- 31 47 59
3.2.4.4 输出样例2
- 4
- 1
- 2
- 1
- 4
3.2.5 数据范围
对于60%的测试点,保证1≤n≤10³,1≤q≤10。
对于所有测试点,保证1≤n≤10⁵,1≤q≤10⁵,1≤aᵢ≤1000。
3.2.6 编写程序思路
分析:方法一:
直接按题意做,编求两个数的最大公因数函数gcd(a, b),然后双重循环外层q循环,内层n循环,循环求g=gcd(g,a[i]),对60%数O(q*n)=10*10³=10⁴没有问题,对于所有测试点O(q*n)=10⁵*10⁵=10¹⁰>10⁸会超时。完整程序代码如下:
#include <iostream>
using namespace std;const int N = 1e5 + 5;int n, q, a[N], g;int gcd(int a, int b) {if (a == 0 || b == 0)return a + b;return gcd(b, a % b);
}int main() {cin >> n >> q;for (int i = 1; i <= n; i++)cin >> a[i];for (int i = 1; i <= q; i++) {g = a[1] + i; //初始最大公因数for (int j = 2; j <= n; j++)g = gcd(g, a[j] + i); //循环求最大公因数cout << g << endl;}return 0;
}
方法二:最大公因数具有线性性质。即对于任意两个整数a和b,以及任意两个整数x和y,有gcd(ax+by, b)=gcd (a, b)。这个性质在解决一些数论问题时非常有用,可以简化计算过程。
gcd(a₁+i,a₂+i,...,aₙ+i)=gcd(a₁+i,(a₂+i-(a₁+i)),...,(aₙ+i-(aₙ₋₁+i))=gcd(a₁+i,(a₂ -a₁),...,(aₙ -aₙ₋₁))
= gcd(a₁+i,gcd((a₂ -a₁),...,(aₙ -aₙ₋₁)))
设g=gcd((a₂ -a₁),...,(aₙ -aₙ₋₁)),则
gcd(a₁+i,a₂+i,...,aₙ+i)= gcd(a₁+i,g)。
为保证aᵢ-aᵢ₋₁>=0,可先对aᵢ按升序排序,时间复杂度O(n log n)= 10⁵*16.60964≈1660964<10⁷,不会超时。具体完整代码如下:
#include <iostream>
#include <algorithm>
using namespace std;const int N = 1e5 + 5; //数据最多10⁵, 5是余量,防计算越界int n, q, a[N], g;int gcd(int a, int b) { //求a, b的最大公因数if (a == 0 || b == 0)return a + b;return gcd(b, a % b);
}int main() {cin >> n >> q;for (int i = 1; i <= n; i++)cin >> a[i];sort(a + 1, a + n + 1); //按升序排序时间复杂度O(n log n)for (int i = 2; i <= n; i++) { //按升序排序时间复杂度O(n)g = gcd(g, a[i] - a[i - 1]); //g初始为0(函数外定义)}for (int i = 1; i <= q; i++) //按升序排序时间复杂度O(q)cout << gcd(a[1] + i, g) << endl;return 0;
}