循环嵌套经典算法
较难11好的,让我们来一起丰富这段内容,让它变成一篇更完整、更容易理解的参考文章。我会保持原有代码和解释,同时补充更多例子、错误提醒和生活场景,让你能更轻松地掌握循环嵌套这个强大的工具。
循环嵌套的进阶用法:用两层循环解决经典数学问题
在编程中,有时候我们需要让计算机“尝试所有可能的情况”。比如,你想知道用口袋里有限的零花钱买零食,有多少种不同的买法?或者你想找出1到100之间所有的质数,用来验证一个数学猜想?这些问题如果用大脑一个个去尝试,会非常费时,但电脑却特别擅长做这种重复的“枚举”工作。这时候,循环嵌套(即一个循环里面再套一个循环)就成了我们的好帮手。今天我们就通过两个经典的数学问题,来学习如何用两层循环解决实际问题。
什么是循环嵌套?
循环嵌套就像两个互锁的齿轮:外层齿轮转一圈,内层齿轮就要转一整圈。例如,你有一个外层循环控制“班级序号”,内层循环控制“座位号”,那么外层每换一个班级,内层就会把该班级的所有座位都检查一遍。这种结构特别适合需要枚举所有组合的问题。
一、百钱百鸡:用循环找出所有购买方案
问题描述
公鸡5文钱一只,母鸡3文钱一只,小鸡3只一文钱。现在有100文钱,要买100只鸡。问公鸡、母鸡、小鸡各多少只?
解题思路
- 公鸡最多买多少只?100文 ÷ 5文/只 = 20只。
- 母鸡最多买多少只?100文 ÷ 3文/只 ≈ 33只。
- 小鸡的数量 = 100 – 公鸡数 – 母鸡数(因为总鸡数是100)。
我们可以用外层循环穷举公鸡数(从0到20),内层循环穷举母鸡数(从0到33),然后根据“总钱数=100”这个条件检验小鸡的数量是否合理。注意:小鸡3只1文,所以小鸡数必须是3的倍数,并且钱数计算要准确。
代码示例(带中文注释)
#include <iostream>
using namespace std;
int main() {
// 外层循环:穷举公鸡数量,最多20只
for (int rooster = 0; rooster <= 20; rooster++) {
// 内层循环:穷举母鸡数量,最多33只
for (int hen = 0; hen <= 33; hen++) {
// 小鸡数量 = 100 - 公鸡 - 母鸡
int chick = 100 - rooster - hen;
// 条件:小鸡数量不能为负,总钱数等于100文,且小鸡数是3的倍数
if (chick >= 0 &&
5 * rooster + 3 * hen + chick / 3 == 100 &&
chick % 3 == 0) {
cout << "公鸡:" << rooster
<< " 母鸡:" << hen
<< " 小鸡:" << chick << endl;
}
}
}
return 0;
}
运行结果(输出三种方案):
公鸡:0 母鸡:25 小鸡:75
公鸡:4 母鸡:18 小鸡:78
公鸡:8 母鸡:11 小鸡:81
公鸡:12 母鸡:4 小鸡:84
生活中的类比
想象你手上有100元,要买三种不同类型的文具:
- 钢笔:5元/支
- 铅笔:3元/支
- 橡皮:1元/3块
你要买总共100件文具,正好花完100元。那么用循环去枚举钢笔和铅笔的数量,再算出橡皮的数量,就知道有多少种购买方式了。
常见错误提醒
- 小鸡数量忘记检查非负:如果公鸡和母鸡数量加起来超过100,小鸡就会变成负数,但实际不能有负数的鸡。所以
chick >= 0是必须的。 - 小鸡钱数计算错误:
chick / 3在C++中是整数除法,会丢失余数。因此需要单独检查chick % 3 == 0,确保能整除。 - 漏掉公鸡或母鸡为0的情况:循环从0开始,这样才能枚举“不买某种鸡”的方案。
二、找出质数:用循环嵌套筛选
问题描述
找出2到100之间所有的质数。质数是指除了1和它本身以外,不能被其他正整数整除的数(比如2、3、5、7、11……)。
解题思路
- 外层循环:遍历每个数
num,从2到100。 - 内层循环:用
i从2开始,检查num是否能被i整除。如果找到了一个因子(除了1和它本身),那么这个数就不是质数。 - 为了提高效率,内层循环只需检查到
i * i <= num即可。因为如果有一个因子大于 sqrt(num),那么它对应的另一个因子一定小于 sqrt(num),已经检查过了。这样可以减少循环次数。
代码示例(带中文注释)
#include <iostream>
using namespace std;
int main() {
// 外层循环:检查每个数是否是质数
for (int num = 2; num <= 100; num++) {
// 先假设当前数是质数
bool isPrime = true;
// 内层循环:从2开始检查到 sqrt(num)
for (int i = 2; i * i <= num; i++) {
// 如果找到能整除的因子,则不是质数
if (num % i == 0) {
isPrime = false;
break; // 找到一个因子就可以提前退出内层循环
}
}
// 如果 isPrime 仍然为 true,则输出这个数
if (isPrime) {
cout << num << " ";
}
}
cout << endl;
return 0;
}
输出结果:
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
生活中的类比
想象你是一个班长,要检查班里每个同学的作业是否“干净”(只有自己完成,没有抄袭)。对于每个同学,你需要检查他是否被其他人“影响”(即有没有除了1和自身以外的“数字朋友”)。你把每个同学和比他小的其他同学配对检查,如果发现任何两人有“共同点”(因子),就标记这个同学有问题。这样就能找出所有“干净”的同学(质数)。
常见错误提醒
- 忘记初始化
isPrime:如果没有写bool isPrime = true;,那么变量的初始值是不确定的,可能导致错误判断。 - 内层循环范围写错:如果用
i <= num或者i < num,会多出很多不必要的计算。正确的范围是i * i <= num,(等价于i <= sqrt(num))。 - 忘记用
break提前退出:发现一个因子后,后面的检查已经没有意义,用break可以节省时间,尤其在数字很大时效果明显。
三、新手最容易犯的3个错误(总结)
- 忘记变量初始化:尤其是在内层循环中,每次外层循环开始时要重置布尔值、计数器等。比如判断质数时,如果没有在每个
num之前设置isPrime = true,那么上一次循环的结果会影响到这一次。 - 循环边界弄错:例如百钱百鸡中公鸡最大是20只,母鸡最大33只,但有人可能会写成
<= 100,导致无意义的迭代。判断质数时内层循环条件写成i <= num也会浪费大量时间。 - 整数除法与取模的混淆:百钱百鸡中小鸡数除以3要用整数除法,但必须确保能整除;如果不检查余数,可能计算出错误的总钱数。建议总是用
xiao % 3 == 0来确认。
四、完整可运行的示例:帮小美买零食
为了让你更直观地感受循环嵌套的用途,再来看一个贴近生活的例子:
小美有20元零花钱,她想买三种零食:薯片(5元/包)、饼干(3元/包)、糖果(1元/2颗)。她一共要买10件零食(注意件数:一包薯片算1件,一包饼干算1件,2颗糖果算1件),并且正好花完20元。有多少种买法?
这里外层循环枚举薯片数量(04包,因为20÷5=4),内层循环枚举饼干数量(06包,因为20÷3≈6),然后糖果数量 = 10 – 薯片 – 饼干,再检查糖果是否非负、总钱数是否为20、糖果颗数是否为偶数(因为2颗1元,每件是2颗,所以糖果件数必须能被2整除,且每件对应2颗)。
#include <iostream>
using namespace std;
int main() {
cout << "薯片 饼干 糖果(件) 总钱数" << endl;
// 外层:薯片包数,最多4包
for (int chip = 0; chip <= 4; chip++) {
// 内层:饼干包数,最多6包
for (int cookie = 0; cookie <= 6; cookie++) {
// 糖果件数(注意:1件糖果=2颗,即1元)
int candy = 10 - chip - cookie;
// 检查件数非负、总钱数=20元、糖果件数必须非负整数
if (candy >= 0 &&
5 * chip + 3 * cookie + candy * 1 == 20) {
// 糖果件数是整数,但需要确保颗数能被2整除吗?
// 实际上,candy是件数,每件2颗,所以总颗数=2*candy,钱数= candy*1
// 只要candy是整数就行,不需要额外检查%2,因为candy本身就是整数件。
cout << chip << " " << cookie << " "
<< candy << " 20元" << endl;
}
}
}
return 0;
}
运行结果:
薯片 饼干 糖果(件) 总钱数
0 0 10 20元
0 5 5 20元
1 0 9 20元
2 3 5 20元
3 0 7 20元
4 1 5 20元
这个例子也展示了循环嵌套的力量:只用几行代码,就把所有可能组合都枚举出来了。
五、相关指引
如果你已经掌握了上面两个例子,还可以继续学习:
- 多层循环:有时候需要三层甚至更多层循环,比如“百钱百鸡”的变种(买更多种家禽)。
- 循环优化:适当缩小范围、使用
break提前结束,可以显著提升程序效率。 - 双重循环与
continue:在某些情况下,用continue可以跳过不必要的检查,让代码更简洁。 - 枚举算法:循环嵌套是“暴力枚举”的基础,后续可以结合回溯、剪枝解决更复杂的问题(如八皇后、数独等)。
记住:计算机最擅长的就是“傻傻地”重复工作,而循环嵌套就是指挥它做这件事的最佳工具。多动手写几个小例子,你就能熟练运用它来解决有趣的数学问题了。
例题精讲
在C++中,以下哪个循环嵌套的执行次数是m×n?
在循环嵌套中,break语句只能跳出最内层循环。
以下程序使用循环嵌套打印九九乘法表(等腰三角形格式),请填空:
for(int i=1;i<=9;i++){
for(int j=1;___;j++){
cout<<j<<"*"<<i<<"="<<i*j<<"\t";
}
cout<<endl;
}以下关于循环嵌套的说法错误的是?
以下程序输出100以内的所有素数,请在空白处填空:
#include <iostream>
using namespace std;
int main(){
int i,j,isPrime;
for(i=2;i<=100;i++){
isPrime=1;
for(j=2;___;j++){
if(i%j==0){
isPrime=0;
break;
}
}
if(isPrime) cout<<i<<" ";
}
return 0;
}