1. 卷面设计与考点分布分析
1.1 这套模拟卷的定位
这几年我带选手备赛全国青少年信息素养大赛算法应用主题赛,最大的感受就是:初赛不仅要考“会不会写代码”,更考验选手在限定时间内能不能快速判断考点、避开常见坑。C++赛项的初赛模拟卷,题型基本稳定在选择题、判断题、程序阅读/填空题和编程实操题这几大块,而且命题风格很贴近中小学信息学竞赛的经典套路。
这套模拟卷2的设计,就是按照近年初赛的常见难度和知识点覆盖来出的。它不追求偏题怪题,而是把C++语言基础、常用算法、数据结构这些“必考盘”都过一遍。适合三类人使用:一是正在备赛的选手,用来检验自己知识点有没有盲区;二是指导老师,可以直接拿去做课堂限时测验;三是家长想帮孩子摸底,这份卷子的难度和区分度也够用。
1.2 考点覆盖与命题思路
初赛和复赛最大的区别在于,初赛更看重“广”而不是“深”。很多选手复赛能AC难题,初赛却在基础概念上丢分,原因就是复习时只盯难题,忽略了语言细节。
这套模拟卷2的考点安排如下:
| 题号 | 题型 | 核心考点 | 考查层次 |
|---|---|---|---|
| 第1题 | 单选 | 数组下标与内存布局 | 理解 |
| 第2题 | 单选 | 冒泡排序的比较次数 | 应用 |
| 第3题 | 单选 | 二分查找的前提条件 | 理解 |
| 第4题 | 单选 | 递归的终止条件 | 应用 |
| 第5题 | 单选 | 指针与引用区别 | 理解 |
| 第6题 | 单选 | 栈的LIFO特性 | 应用 |
| 第7题 | 单选 | 贪心算法适用场景 | 理解 |
| 第8题 | 单选 | 时间复杂度分析 | 分析 |
| 第9题 | 判断 | 结构体对齐 | 理解 |
| 第10题 | 判断 | 全局变量初始化 | 理解 |
| 第11题 | 判断 | 字符串比较 | 应用 |
| 第12题 | 程序填空 | 埃氏筛法 | 应用 |
| 第13题 | 程序阅读 | 递归回溯求排列 | 分析 |
| 第14题 | 编程 | 前缀和区间查询 | 综合 |
| 第15题 | 编程 | 贪心+排序经典题 | 综合 |
从表格能看出来,基础题占大头,但最后两道编程题是拉开差距的关键。命题思路和信奥赛入门组的风格一脉相承:不考你背了多少模板,而是考你在真实场景里能不能想到用这个算法。
2. 选择题精讲与逐题解析
2.1 语言基础类选择题
第1题:声明int a[10];后,以下哪个表达式可以表示数组a的第6个元素?
A.a[5]B.a[6]C.*(a+5)D.*(a+6)
这道题考的是数组下标从0开始以及指针运算。答案选A和C,但初赛单选一般只选一个最符合的,所以标准答案是A。*(a+5)在语法上完全等价于a[5],但题目问的是“哪个表达式可以表示”,A更直接。很多选手错选了B,就是顺手把“第6个”理解成了下标6,这是最典型的低级失误。
第2题:对长度为n的数组进行冒泡排序,最坏情况下需要比较多少次?
A. n-1 B. n C. n(n-1)/2 D. n²
这题考冒泡排序的基本原理。外层循环跑n-1趟,第i趟内层比较n-i次,总比较次数是 (n-1) + (n-2) + ... + 1 = n(n-1)/2,选C。有选手纠结“优化过的冒泡排序”,但题目没提优化,默认就是标准写法,不要自己加条件。
2.2 算法思想类选择题
第3题:使用二分查找算法,要求待查找的数组必须满足什么条件?
A. 元素无重复 B. 元素有序 C. 元素为正整数 D. 元素个数为偶数
答案是B。二分查找的核心逻辑是每次根据中间值和目标值的大小关系,排除一半区间,这要求数组必须有序。无重复不是硬性条件,有重复时只是“找哪一个”的问题,不影响算法运行。很多教材会在“元素无重复”上设陷阱,因为这属于额外条件而非必要前提。
第4题:一个递归函数如果缺少终止条件,会发生什么?
A. 编译错误 B. 运行结果错误 C. 栈溢出 D. 程序正常结束但速度慢
答案是C。递归每次调用都会在系统栈上分配新的栈帧,如果没有终止条件,函数会无限调用,直到栈空间耗尽,程序崩溃。调试时最常见的表现就是“段错误”或者“Process terminated with status -1073741571”。需要提醒的是,递归深度过大即使有终止条件也可能栈溢出,这也是为什么很多竞赛题对递归深度敏感的题目会建议用迭代实现。
2.3 数据结构与C++特性类选择题
第5题:关于C++中的指针和引用,下列说法正确的是?
A. 引用可以重新绑定到另一个变量 B. 指针可以为空,引用不能 C. 指针和引用在传参时效率完全相同 D. 引用占用内存大小一定小于指针
答案是B。引用在初始化后就不能再绑定其他变量,这是它和指针最大的区别。指针可以为空(nullptr),但引用必须初始化且不能为空,所以使用引用比指针更安全。C选项看起来合理但其实不对——传引用和传指针底层实现类似,但语义不同,而且对于小对象,传值可能比传引用更快。
第6题:栈这种数据结构的特点是?
A. 先进先出 B. 先进后出 C. 随机存取 D. 只能存整数
答案是B。栈是典型的后进先出(LIFO)结构,就像往箱子里放书,最后放进去的最先拿出来。这个考点本身不难,但会结合函数调用、括号匹配、表达式求值等场景来考。比如检查括号是否匹配,就是遇到左括号入栈、右括号弹栈并检查是否匹配。
2.4 算法分析与策略类选择题
第7题:以下哪个问题最适合用贪心算法求解?
A. 求一个图的所有最短路径 B. 在有序数组中查找某个数 C. 找零钱问题(硬币面额1、5、10、25) D. 求解N皇后问题
答案是C。找零钱问题在硬币面额满足一定条件时,贪心策略(每次选最大面额)能得到最优解。A需要Dijkstra或Floyd这类更复杂的算法,B用二分查找,D一般用回溯。这道题的深层考点是:贪心算法只保证局部最优,是否能达到全局最优需要证明或依赖问题的特殊性质。很多选手以为贪心就是“看着办”,其实竞赛里用贪心必须先想清楚可行性。
第8题:以下代码片段的时间复杂度是多少?
for (int i = 1; i <= n; i *= 2) { for (int j = 1; j <= n; j++) { // 常数操作 } }A. O(n) B. O(n log n) C. O(n²) D. O(log n)
答案是B。外层循环以2倍速度增长,执行次数约为 log₂n 次;内层循环固定执行n次,相乘得 O(n log n)。这类题是初赛必考的,关键是要看清循环变量的变化方式——是 i++、i*=2,还是 i+=k,直接决定复杂度是线性、对数还是根号级别。
3. 判断题、程序填空与阅读题的考查逻辑
3.1 判断题里的“文字陷阱”
判断题一共3道,看似简单,但正确率往往不高,因为命题人会在细节上做文章。
第9题:结构体struct Node { char c; int x; };在64位系统下的大小是5字节。
这句话是错的。结构体存在内存对齐规则,char占1字节,int占4字节,但因为对齐要求,int的起始地址必须是4的倍数,编译器会在char后面填充3个字节,所以实际大小是8字节而不是5。这个知识点考得特别勤,不只是笔试,实际编程时如果涉及文件读写、网络传输,结构体对齐会造成严重的兼容性问题。
第10题:C++中,全局变量未初始化时,其值默认为0;局部变量未初始化时,其值为不确定。
这句话是对的。全局变量存放在静态存储区,程序启动时会被清零;局部变量分配在栈上,不会自动清零,值是随机的。这个考点直接关系到实际编码习惯——局部变量必须手动初始化,否则可能跑出“幽灵Bug”,而且这种Bug最难复现。
第11题:在C++中,"abc" == "abc"的结果一定为true。
这句话在竞赛环境里是错的。"abc"是字符串字面量,比较的是指针地址而不是内容。同一个程序里相同的字符串字面量是否复用同一块内存,C++标准不保证,由编译器决定。所以比较字符串内容必须用strcmp或std::string的==运算符。
3.2 程序填空题:埃氏筛法
第12题:补全以下代码,实现输出1到n之间的所有质数。
#include <iostream> #include <cstring> using namespace std; int main() { int n; cin >> n; bool isPrime[n + 1]; memset(isPrime, true, sizeof(isPrime)); isPrime[0] = isPrime[1] = false; for (int i = 2; ______; i++) { if (isPrime[i]) { for (int j = i * i; j <= n; ______) { isPrime[j] = false; } } } for (int i = 2; i <= n; i++) { if (isPrime[i]) cout << i << " "; } return 0; }第一个空填i * i <= n,第二个空填j += i。这是埃氏筛法的标准写法,核心思想是:如果一个数是质数,那么它的倍数一定是合数,直接标记为false。从i * i开始而不是从i * 2开始,是一个经典优化——因为 i * 2、i * 3 这些更小的倍数已经被更小的质数筛掉了,重复标记没有意义。
这道题还隐含一个坑:bool isPrime[n + 1]是变长数组,标准C++不支持,但一些编译器(如GCC)允许作为扩展。比赛时最好用vector<bool>或动态数组,避免编译不通过。我见过不止一个选手在Dev-C++里能跑,换到比赛环境直接编译失败,原因就是这个。
3.3 程序阅读题:递归回溯求排列
第13题:阅读以下代码,写出当n=3时程序的输出。
#include <iostream> using namespace std; int n = 3; int a[10]; bool used[10]; void dfs(int k) { if (k > n) { for (int i = 1; i <= n; i++) cout << a[i] << " "; cout << endl; return; } for (int i = 1; i <= n; i++) { if (!used[i]) { used[i] = true; a[k] = i; dfs(k + 1); used[i] = false; } } } int main() { dfs(1); return 0; }输出结果是1到3的全排列,共6行:
1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1这道题考的是回溯算法的基本功。很多选手能看懂递归,但追问“为什么输出是这个顺序”就说不清了。核心在于for循环的顺序:每次都从1开始尝试,used数组记录哪些数已经用过,回溯时把标记撤销。这是一个典型的“选/不选”思想,也是DFS的入门形态。
4. 编程实操题:思路、代码与运行细节
4.1 第14题:前缀和解决区间查询
题目描述:给定n个整数和m次询问,每次询问给出区间 [l, r],求区间内所有数的和。其中 1 ≤ n, m ≤ 100000。
如果每次询问都暴力遍历区间,时间复杂度是O(n*m),n和m都是10⁵级别时根本跑不动。正确解法是前缀和:预处理一个数组pre[i],表示前i个数的和。这样区间 [l, r] 的和就等于pre[r] - pre[l-1],单次询问O(1)完成。
#include <iostream> using namespace std; const int MAXN = 100005; long long a[MAXN], pre[MAXN]; int main() { int n, m; cin >> n >> m; for (int i = 1; i <= n; i++) { cin >> a[i]; pre[i] = pre[i - 1] + a[i]; } while (m--) { int l, r; cin >> l >> r; cout << pre[r] - pre[l - 1] << endl; } return 0; }这里我特别用了long long而不是int,因为n最大10⁵,每个数如果也到10⁵量级,区间和能到10¹⁰,超出int范围。这是专门设计的易错点——数据范围一上来,很多选手就忘了考虑溢出。
4.2 第15题:贪心 + 排序解决活动安排问题
题目描述:有n个活动,每个活动有开始时间s和结束时间t,同一时间只能参加一个活动,求最多能参加多少个活动。
这是最经典的贪心入门题,策略是:按结束时间从小到大排序,依次选择结束时间最早且与已选活动不冲突的活动。
#include <iostream> #include <algorithm> using namespace std; struct Activity { int s, t; }; bool cmp(Activity a, Activity b) { return a.t < b.t; } int main() { int n; cin >> n; Activity acts[100005]; for (int i = 0; i < n; i++) { cin >> acts[i].s >> acts[i].t; } sort(acts, acts + n, cmp); int cnt = 0, lastEnd = 0; for (int i = 0; i < n; i++) { if (acts[i].s >= lastEnd) { cnt++; lastEnd = acts[i].t; } } cout << cnt << endl; return 0; }这里最容易出错的地方有两个:一是排序规则写反,按开始时间排序在某些数据下会得到错误答案;二是区间重叠的判断条件,题目要求“同一时间只能参加一个活动”,那么边界条件是s >= lastEnd还是s > lastEnd,取决于题目对端点时刻的描述。比如一个活动在时间3结束,另一个在时间3开始,能不能同时参加?要看题目是开区间还是闭区间。这道题用>=,因为标准活动安排问题认为端点重叠不算冲突。
4.3 两道编程题的时间复杂度分析
第14题前缀和,预处理O(n),单次查询O(1),总复杂度O(n+m),在10⁵数据量下运行时间几乎可以忽略。第15题排序O(n log n),贪心扫描O(n),总复杂度O(n log n),同样轻松通过。
为什么初赛会选这两道题?因为它们看起来简单,但考察了三个重要能力:能不能根据数据范围估算时间复杂度、能不能发现暴力解法的瓶颈、能不能用经典算法思想优化。这三项能力,正是后续省赛和国赛的基础。
5. 初赛高频失分点与备赛建议
5.1 我总结的五个高频失分点
第一类是审题不仔细。不看数据范围,上来就写暴力算法,或者没有注意题目要求输出格式的细节(比如行末有没有多余空格)。这类失分最冤,但每场模拟赛都会出现。
第二类是C++语言细节掌握不牢。比如第9题的结构体对齐,第11题的字符串比较,还有int和long long混用、数组越界不报错等问题。初赛笔试部分特别喜欢考这些,因为它们是“会写代码但不理解底层原理”的分水岭。
第三类是算法模板背熟了但不会应用。很多选手能把快排、二分、DFS的模板默写出来,但题目换个场景就认不出来。比如第15题活动安排,本质是贪心,但把背景换成“最多能安排几场会议”,有些人就反应不过来。
第四类是STL使用不熟练。sort、vector、stack、queue、map这些是比赛中最常用的工具,但很多选手只会在Dev-C++里用#include <bits/stdc++.h>,真到比赛让孩子手写头文件或用到特定容器时就开始犯迷糊。我建议备赛期间把常用STL容器和算法函数的用法整理成一张速查表,考前过一遍比刷十道题都管用。
第五类是调试能力不足。程序运行结果不对,不会用输出中间变量的方式定位问题,也不理解“分段排查”的思路。特别是递归和循环的程序,经常是写了半天发现死循环,然后只能干瞪眼。
5.2 备赛建议:用“以赛代练”的方式刷题
如果你现在离初赛还有2到4周,我的建议是按“三轮复习法”来准备。
第一轮(知识扫盲):把C++语法基础、数组/字符串/结构体、顺序/分支/循环、函数与递归、常用算法(枚举、模拟、排序、二分、贪心)这六大块过一遍,每块至少做5道对应题目。不用追求难题,基础题能全对就是胜利。
第二轮(限时模拟):用这份模拟卷2和模拟卷1做限时训练,严格按初赛的时间要求来。做完不要只对答案,要把每道错题的知识点写下来,统计自己哪类题目错得最多。如果选择题错得多,说明基本概念有漏洞;如果编程题没AC,说明代码实现能力需要加强。
第三轮(重点突破):根据错题统计,集中攻自己的薄弱板块。比如时间复杂度分析总出错,就专门练复杂度估算;递归回溯写不对,就每天手写一遍DFS模板,直到形成肌肉记忆。
5.3 一个提升代码稳定性的习惯
我想特别强调一个看起来“笨”但非常有效的方法:每写完一道题,不要提交通过就结束,回过来看能不能把自己代码里的“魔法数字”改成常量、能不能把重复代码提取成函数。这个习惯能显著降低代码出错概率。
比如第14题的MAXN = 100005,为什么是100005而不是100000?因为很多题目下标从1开始,多开5个位置防止越界。这种细节平时不注意,在比赛紧张的时候就会犯低级错误。我每次带选手训练都会反复强调:数组愿意多开一点,别在边界上抠那点内存。
附:模拟卷2参考答案
选择题
| 题号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| 答案 | A | C | B | C | B | B | C | B |
判断题
| 题号 | 9 | 10 | 11 |
|---|---|---|---|
| 答案 | 错 | 对 | 错 |
程序填空题
- 第12题:第一个空填
i * i <= n,第二个空填j += i
程序阅读题
- 第13题:输出1到3的6种全排列(1 2 3 / 1 3 2 / 2 1 3 / 2 3 1 / 3 1 2 / 3 2 1)
编程题
- 第14题:前缀和法,用
long long避免溢出 - 第15题:按结束时间排序,贪心选择,判断条件为
acts[i].s >= lastEnd
拿到答案之后,我的建议是不要只看对错,而是把每道错题对应到考纲里的具体知识点,重新做一遍同类题。我在带选手的过程中有个体会:初赛不是拼谁天赋高,而是拼谁基础扎实、失误少。认真把这份模拟卷吃透,弄清楚每一道题的来龙去脉,比盲目刷十套题都管用。