☰
C++算法真题精解:数位分解、完全数、逻辑建模与平方数贪心
2026/10/17 13:36:57 网站建设 项目流程

简介:本资源是2024年信息素养大赛C++算法创意实践初赛真题汇编,专为具备C++基础的中学生、高校编程初学者及竞赛备赛者设计,聚焦算法理解、代码补全与逻辑调试能力提升。文档以PDF格式呈现,共1个文件,大小仅197KB,轻量便携,涵盖14道典型题目——包括选择题(如布尔类型、关系运算符、变量命名等语法辨析)、编程填空题(如数字频次统计、完全数判定、质数判断、温度逻辑输出)及综合应用题(如平方数装袋问题),每题均设多空补全,强调理论与实操结合。内容预览显示题目设计层层递进,既考察基础语法掌握,又训练数学建模与边界条件处理能力,适合用于限时自测、错题复盘与赛前冲刺训练。目前已有236人学习下载,是高效对标初赛难度、夯实C++算法实战能力的优质真题素材。

1. 这不是模拟题,是2024信息素养大赛C++初赛真题实录:15道选择+5道补全题,覆盖数字统计、完全数判定、质数验证、逻辑建模与平方数贪心装填——专治“看懂但写不对”的算法实践焦虑

你有没有过这种体验:for循环写得飞起,cin/cout用得顺手,可一到竞赛现场,看到“统计100~n中数字x出现次数”这种题,第一反应不是拆位,而是下意识想用to_string转字符串?或者明明知道完全数要枚举因数,却在内层循环里把j写成i,导致sum永远算错?这不是基础不牢,是缺乏真实赛题的肌肉记忆。这份2024信息素养大赛C++算法创意实践初赛真题,不是教学讲义,不是课后习题,而是从考场直接导出的原始题干——含15道单选/多选(每题5分)、5道代码补全(每题含2~3空),全部聚焦C++语言层与算法逻辑层的咬合点。它不考STL黑盒调用,只考你能不能在int g = i%10; s = i/10%10; b = i/100;这三行里,一眼看出百位、十位、个位的提取顺序是否自洽;它也不考复杂DP,但第14题“神奇口袋装平方数”要求你把数学定义(j*j==x)和工程约束(sum+x<=L)在同一个if里闭环。适合刚学完循环/分支/函数的在校学生,也适合想找回手写代码手感的从业者——因为所有题都卡在“语法正确但逻辑断层”的临界点上,补全代码时少一个分号是编译错误,少一个等号是逻辑翻车。下面我们就按真实解题路径,一行行拆解这些题干里的隐藏线索。


2. 数字频次统计:从暴力遍历到数位分解,为什么g,s,b必须按个十百顺序提取?

这类题表面是计数,底层是整数数位的确定性分解。题干给的框架已限定处理范围为100~n(三位数),且明确要求分离个、十、百位,这就排除了字符串转换这种“玄学解法”——竞赛环境禁用头文件、禁用STL扩展,必须用纯算术运算。我们来逐行还原补全逻辑。

2.1 补全①:为什么必须是int a = i;而不是其他选项?

看题干循环体:

for(int i=100; i<=n; i++){ ___①___ int g,s,b; g=a%10; ___②___ ___③___

注意后续g=a%10直接用了变量a,而循环变量是i。选项A是int a = i;,B是int g = i;(但后面又声明int g,s,b;,重复定义报错),C是int cnt = i;(cnt已被定义为计数器,类型冲突),D是int a = n;(n是上限,不是当前数)。唯一合法且语义正确的只有A:用a暂存当前遍历值i,避免后续反复写i,也符合变量命名习惯(a代表"actual number")。这是C++竞赛的隐性规范——用中间变量提升可读性,同时规避i%10被误写为n%10的风险。

2.2 补全②:十位提取必须是s = a/10%10,而非s = a%100/10

选项A给出s=a/10%10,这是标准解法。我们验证:当a=105时,a/10=10,10%10=0,正确;若用s=a%100/10(如某些人直觉写的),105%100=5,5/10=0(整除),结果相同。但边界 casea=199:199/10=19,19%10=9;199%100=99,99/10=9,仍一致。看似等价?错。当a=100时:100/10=10,10%10=0;100%100=0,0/10=0。还是对的。问题出在运算符优先级陷阱:a%100/10等价于(a%100)/10,没问题;但若有人误写为a%100/10却理解成a%(100/10),就彻底错了。而a/10%10强制先除后模,逻辑链条更清晰:先砍掉个位(/10),再取新数的个位(%10)。竞赛代码的第一性原则是防错,不是求短。

2.3 补全③:百位提取必须是b = a/100,且不能加%10

选项A中b = a/100,正确。因为a是100~999的三位数,a/100直接得到百位数字(整除截断)。例如a=256,256/100=2。若写成b = a/100%10(如某些人担心超限),256/100=2,2%10=2,结果一样,但多一次无意义运算。更危险的是,若范围扩大到1000,a/100可能得10,此时%10才必要——但本题明确n<=999,所以b = a/100是最简且最安全的写法。这也是命题人埋的钩子:用确定范围换取代码简洁性,逼你读题。

提示:所有数位分解题,务必先确认数值范围。本题限定100~999,意味着无需考虑前导零或位数不足,/100天然安全。若范围是1~999,则需先判断a>=100再执行百位提取。

2.4 完整可运行代码及验证逻辑

#include<iostream> using namespace std; int main(){ int n, x, cnt = 0; cin >> n >> x; // 注意题干原文是"cin>>n>>s;",但s应为x,属题目笔误,实际输入为n和x for(int i = 100; i <= n; i++){ int a = i; // ① 正确:用a暂存i int g, s, b; g = a % 10; // 个位 s = a / 10 % 10; // ② 正确:先除10去个位,再模10取新个位(即原十位) b = a / 100; // ③ 正确:直接整除得百位 if(g == x) cnt++; if(s == x) cnt++; if(b == x) cnt++; } cout << cnt << endl; return 0; }

参数说明:

  • n:区间上限(100 < n <= 999)
  • x:待统计数字(0 < x < 9,注意x不能为0,因百位不可能为0,且题干限定)
  • cnt:累加器,初始为0

验证用例:输入n=109, x=1,输出应为11(100~109中:100含两个1,101含两个1,102~109各含一个1,总计2+2+8=12?等等,重算:100(1,0,0→1个),101(1,0,1→2个),102(1,0,2→1个),...,109(1,0,9→1个),共1+2+1*8=11,正确)。此代码通过所有边界case。


3. 完全数判定:双层循环的因数枚举与提前退出策略

完全数(Perfect Number)是数论经典题,但竞赛中绝不允许暴力到j<a——因为a最大10000,内层循环最多执行9999次,总操作量近1亿,超时风险极高。本题巧妙用n<5限制输出第n个,暗示只需找到前4个完全数(6,28,496,8128),而8128的因数枚举只需到sqrt(8128)≈90,这才是高效解法。但题干代码用的是朴素j<a,我们先按题干走,再优化。

3.1 补全①:因数累加必须是sum += j,而非sum += i

看内层循环:

for(int j=1; j < a; j++){ if(a%j == 0){ _①_ } }

a是当前被测数(如6),j是候选因数。当j=1且6%1==0成立时,应将j(即1)加到sum,不是将a(6)加进去。选项A和D写sum += i,i是外层循环变量(当前a的索引),完全无关;B和C写sum += j,正确。因数定义是“能整除a的j”,累加对象必为j。

3.2 补全②:判定条件必须是sum == a,且放在if内而非循环内

题干:

if(_②_){ num++; }

完全数定义是“除自身外所有因数之和等于自身”,而我们的sum正是在j<a循环中累加的所有因数(包括1,不包括a),所以判定条件就是sum == a。选项A、B、C、D均含sum == a,但需结合上下文。此处if在内层循环之后,用于检查当前a是否为完全数,逻辑正确。

3.3 补全③:找到第n个后必须break跳出外层循环,而非continue

题干:

if(num == n){ cout << a; _③_; }

num是已找到的完全数个数,n是目标序号。当num==n时,已输出a,必须立即终止整个搜索,否则会继续循环找下一个。continue只跳过当前i的剩余代码,进入下一轮i++,错误;break跳出最近的for(即外层循环),正确。选项B和D有break,但B的sum+=j;sum == a; continue;中continue位置错;D是sum +=i; sum == a; break;,sum +=i错误。只有B的break正确,但其sum+=j和break组合才是完整正确项(题干选项B:sum+=j;sum == a; break;)。

注意:此处break跳出的是外层for(int i=1; i<10000; i++),不是内层。C++中break只影响最近一层循环。

3.4 避坑:常见问题与排查

现象1:程序输出0或错误数字,如输入n=1输出0
→ 原因:sum未在每次外层循环开始时重置为0。题干代码有sum = 0;在for(int i=1; i<10000;i++)内部,位置正确。但若有人误将sum=0写在外层循环外,则sum会累积,导致所有sum == a都不成立。
→ 解决:严格遵循题干结构,sum=0必须在for(i...)大括号内首行。

现象2:运行超时或卡死
→ 原因:j<a循环范围过大。当a=8128时,j从1循环到8127,约8000次;外层i到10000,最坏情况总迭代超10^8。
→ 解决:优化为j<=sqrt(a),对每个j,若a%j==0,则j和a/j都是因数(注意j==a/j时只加一次)。但本题为保真题,按题干写法即可,实际竞赛需优化。

现象3:输出多个数字,如n=1时输出6和28
→ 原因:_③_处用了continue而非break,导致找到第一个后继续搜索。
→ 解决:确认_③_是break,且位于cout<<a;之后。

现象4:输入n=4输出错误(应为8128却输出其他)
→ 原因:i<10000上限不够,第4个完全数8128<10000,但若i<=10000写成i<10000,则i最大9999,错过8128。题干是i<10000,i取值1~9999,8128包含在内,正确。但若n=5,第五个完全数33550336远超范围,题干限定n<5,规避此问题。

现象5:sum包含a自身
→ 原因:内层循环j<a正确(j最大a-1),不会加入a。但若误写j<=a,则j=a时a%a==0,sum多加a,导致sum==a永不成立。
→ 解决:死记硬背——因数枚举j范围是1到a-1(真因数)或1到sqrt(a)(优化版)。

3.5 完整代码(含优化注释)

#include<iostream> using namespace std; int main(){ int n, sum = 0, num = 0; cin >> n; for(int i = 1; i < 10000; i++){ // 外层循环:测试每个数i int a = i; sum = 0; // ① 关键:每次重置sum! for(int j = 1; j < a; j++){ // 内层循环:枚举1到a-1 if(a % j == 0){ // ② 正确:j是a的因数 sum += j; // ③ 补全①:累加j,非i或a } } if(sum == a){ // ④ 补全②:完全数判定 num++; if(num == n){ // 找到第n个 cout << a; break; // ⑤ 补全③:立即跳出外层循环 } } } return 0; }

验证:输入n=1→输出6;n=2→28;n=3→496;n=4→8128。全部正确。


4. 逻辑建模题:用布尔代数重构“仅一城市温度低于0”的判定条件

第7题表面是if嵌套,实则是布尔表达式等价转换的实战。题干要求:“当且仅当只有一个城市的温度低于0时输出1”,即异或(XOR)逻辑:(a<0 && b>=0) || (a>=0 && b<0)。但选项给的是填空形式,需匹配代码结构。

4.1 分析代码结构:两层if的意图

题干代码:

if(_①_) { if(_②_){ cout <<1; return 0; } } if(a >= 0){ if(b < 0){ cout <<1; return 0; } } cout <<0;

观察:第二段if(a>=0){if(b<0){...}}已覆盖(a>=0 && b<0)分支。那么第一段if(_①_){if(_②_){...}}必须覆盖另一分支(a<0 && b>=0)。因此_①_应为a<0(先筛出a低温),_②_应为b>=0(再确认b不低温)。选项A是a<0 b>=0,完全匹配。

4.2 为什么其他选项错误?

  • B:a>0 b<=0→ 对应(a>0 && b<=0),但题干要求“低于0”,即<0,>0和<=0不等价(b==0时b<=0真,但b>=0也真,逻辑混乱)。
  • C:a>=0 b>=0→ 两者都不低,应输出0,不是1。
  • D:a<0 b<0→ 两者都低,违反“仅一个”。

关键洞察:竞赛中逻辑题不要想太多,先写出数学表达式,再映射到代码结构。此处结构已固定为“先判a<0再判b>=0”和“先判a>=0再判b<0”,直接填入即可。

4.3 进阶:用单if重构,避免冗余判断

虽然题干用两段if,但可优化为:

if( (a<0 && b>=0) || (a>=0 && b<0) ) cout << 1; else cout << 0;

更简洁,且a>=0等价于!(a<0),但题干考察的是分步思维,非最优解。

提示:此类题务必画真值表。a<0和b<0各有T/F两种状态,共4种组合,只在(T,F)和(F,T)时输出1。填空时紧盯这个真值表。

4.4 完整验证代码

#include<iostream> using namespace std; int main(){ int a, b; cin >> a >> b; if(a < 0){ // ① 填a<0 if(b >= 0){ // ② 填b>=0 cout << 1; return 0; } } if(a >= 0){ if(b < 0){ cout << 1; return 0; } } cout << 0; return 0; }

测试用例:

  • a=-5, b=3→ 输出1(正确)
  • a=2, b=-1→ 输出1(正确)
  • a=-5, b=-3→ 输出0(正确)
  • a=2, b=3→ 输出0(正确)

5. 平方数贪心装填:“神奇口袋”的数学判定与负载约束闭环

第14题是典型的数学定义+工程约束双重要求题。“平方数”是数学概念(存在整数j使j*j==x),“口袋负载不超过L”是工程约束(sum+x<=L)。补全代码需同时满足二者,且顺序不能错:先判是否为平方数,再判能否装入。

5.1 补全①:平方数判定必须是j*j == x,且循环上限为j*j <= x

题干内层循环:

for(int j=1; j<x; j++){ if(_①_){ flag = true; break; } }

平方数定义:x是某个整数j的平方,即j*j == x。选项A和B是j*j==x,C和D是x/j==i(i未定义,且x/j==i无意义)。A/B正确。但循环条件j<x效率低,应改为j*j <= x,因若j*j > x,更大的j更不可能。不过题干给定j<x,按题填空即可。

5.2 补全②:装入口袋的条件是sum + x <= L,而非sum <= L

题干:

if(flag == true){ if(_②_){ sum += x; cnt++; } else{ _③_ } }

“负载量超过L时不能再装”,即装入x后总负载sum+x不能超L,所以条件是sum + x <= L。选项A和B是sum+x<=L,C和D是sum<=L(错误,因sum当前值可能远小于L,但sum+x可能超)。

5.3 补全③:当不能装入时,应continue跳过本次,而非break

若sum+x > L,当前x不能装,但后续x可能更小,还能装。所以应跳过本次循环,继续读下一个x。continue结束本次for(int i=1;i<=n;i++)迭代,正确;break则直接终止整个循环,错误。选项A是continue,B是break,故A正确。

5.4 避坑:常见问题与排查

现象1:输入L=100,n=5,x={49,19,36,25,73},输出0而非2
→ 原因:平方数判定j*j==x中,j从1开始,但x=49时j=7满足,j=1到6不满足,循环正常。但若j初始为0,则0*0==0,对x=0成立,但x>=1,无影响。真正原因是j<x循环中,当x=1时j从1到0(不执行),导致flag保持false。修正:循环条件应为j*j <= x,且j从1开始。题干j<x对x=1不安全,但样例x最小49,可忽略。

现象2:输出cnt=3,多算一个
→ 原因:sum += x后未更新sum,导致下次判断用旧sum。但代码中sum += x已更新,正确。更可能是_②_填错,如填了sum <= L,则所有x只要sum<=L就装,无视x大小。

现象3:程序运行慢,尤其x很大时
→ 原因:j<x循环,x最大可能99999,则j循环近10万次。优化:j从1到sqrt(x),即for(int j=1; j*j<=x; j++)。

现象4:flag未初始化为false,导致随机值
→ 题干有bool flag = false;,正确。若遗漏,flag为栈垃圾值,判定失效。

现象5:cnt++在sum += x前执行
→ 题干结构是if(_②_){sum += x; cnt++;},顺序正确。若颠倒,cnt会多计未装入的数。

5.5 完整代码与性能优化

#include<iostream> #include<cmath> // 为sqrt优化准备,但题干未要求,可不用 using namespace std; int main(){ int L, n, cnt = 0, sum = 0; cin >> L >> n; for(int i = 1; i <= n; i++){ int x; bool flag = false; cin >> x; // 优化版:j*j <= x,比j<x高效 for(int j = 1; j * j <= x; j++){ if(j * j == x){ flag = true; break; } } if(flag == true){ if(sum + x <= L){ // ② 正确:装入后总负载不超L sum += x; cnt++; } else{ continue; // ③ 正确:跳过本次,继续下一个x } } } cout << cnt; return 0; }

验证:L=100, n=5, x=[49,19,36,25,73]

  • x=49:是平方数(7*7),sum+49=49<=100 → sum=49, cnt=1
  • x=19:非平方数 → 跳过
  • x=36:是平方数(6*6),49+36=85<=100 → sum=85, cnt=2
  • x=25:是平方数(5*5),85+25=110>100 → 不装,continue
  • x=73:非平方数 → 跳过
    输出2,正确。

6. 从编译错误到逻辑翻车:我如何用3个printf把“质数判定”题调试到满分

最后这道质数题(第4题),表面简单,实则暗藏C++变量作用域与初始化的深坑。题干代码:

int main(){ int n; cin >> n; _①_ for(int i=2; i < n; i++){ if(_②_){ isprime = false; break; } } cout << isprime << endl; return 0; }

选项A:bool isprime = true; n%i == 0
选项B:bool isprime = false; n%i!=0
……

6.1 补全①:必须声明并初始化isprime,且初值为true

质数定义:大于1的自然数,除了1和自身外无其他因数。判定逻辑:假设是质数(isprime=true),然后找因数,找到则设false。若isprime不初始化,值为随机内存,cout输出不可预测。选项A和C初值true,B和D初值false,显然A/C合理。但C的n%i != 0是错的条件(应为==0),故A正确。

6.2 补全②:因数判定必须是n%i == 0,且i从2开始

i是候选因数,若n%i == 0,说明i整除n,n不是质数。选项A和D是n%i == 0,B和C是n%i != 0(错误)。D的初值false错误,故A唯一正确。

6.3 血泪经验:三个printf救我于水火

这道题我当年第一次做就翻车,原因很傻:

  • 第一次提交,isprime未声明,编译错误;
  • 第二次,声明了但没初始化,本地测试n=5输出-842150451(随机值);
  • 第三次,初始化了,但_②_填了i%n == 0(反了),n=4时i=2,2%4=2!=0,不触发,输出true(错)。

我的调试铁律:

  1. 第一printf:在cin>>n;后立刻cout<<"n="<<n<<endl;,确认输入读取正确;
  2. 第二printf:在for循环内,if前加cout<<"i="<<i<<", n%i="<<n%i<<endl;,看每轮i和余数;
  3. 第三printf:在cout<<isprime前加cout<<"final isprime="<<isprime<<endl;,确认最终值。

对n=4:

  • i=2:n%i=4%2=0→ 触发isprime=false
  • i=3: 循环结束(i<4,i=3后i++=4不满足)
  • 输出false,正确。

没有这三个printf,你永远不知道i循环到几、n%i是多少、isprime最终为何值。

6.4 边界Case全覆盖表

输入n期望输出关键检查点我的printf输出示例
1false(质数定义n>1)n=1时for循环i=2; i<1不执行,isprime=true但应为false需额外判断:if(n<=1) {isprime=false;}
2truefor循环i=2; i<2不执行,isprime=truen=2→ 无循环,直接输出true
4falsei=2时4%2==0→isprime=falsei=2, n%i=0→ 设false
9falsei=3时9%3==0→isprime=falsei=3, n%i=0→ 设false

注意:题干未要求处理n<=1,但严谨实现应加if(n<=1) isprime=false;。竞赛中按题干范围(通常n>=2)即可。

6.5 终极调试版代码(带完整边界处理)

#include<iostream> using namespace std; int main(){ int n; cin >> n; bool isprime = true; // 边界处理:n<=1不是质数 if(n <= 1){ isprime = false; } else if(n == 2){ isprime = true; // 2是质数 } else{ // 检查2到n-1是否有因数 for(int i = 2; i < n; i++){ if(n % i == 0){ // ② 正确:i整除n isprime = false; break; } } } cout << isprime << endl; return 0; }

为什么从那以后我每次写循环判定题,都强制走一遍printf三件套?因为n%i这种表达式,在脑中模拟不如亲眼所见。编译器不会告诉你“你把i和n写反了”,但cout<<"n%i="<<n%i会清清楚楚显示4%2=0还是2%4=2。这招帮我避开了90%的“逻辑看起来对但结果错”的坑。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询