文章目录
前言
枚举算法是我们在日常中使用到的最多的一个算法,它的核心思想就是:枚举所有的可能。枚举法的本质就是从所有候选答案中去搜索正确的解,使用该算法需要满足两个条件:(1)可预先确定候选答案的数量;(2)候选答案的范围在求解之前必须有一个确定的集合。
枚举算法简单粗暴,他暴力的枚举所有可能,尽可能地尝试所有的方法。虽然枚举算法非常暴力,而且速度可能很慢,但确实我们最应该优先考虑的!因为枚举法变成实现最简单,并且得到的结果总是正确的。
本文收集可以采用枚举算法的一些题例,以供查阅、学习之用。
一、枚举算法基本思想
枚举法就是对可能是解的众多候选者按某种顺序进行逐一枚举和检验,从中找出符合要求的候选解作为问题的解。
枚举法也称为穷举法,其基本思想是:根据所要解决问题的部分条件确定答案的大致范围,并在选定范围内列举出所有可能的方案,并逐一进行验证。若某个方案经过验证符合题设的全部条件,则视为问题的一个解;若全部方案验证后都不符合题设条件,则问题无解。
二、从鸡兔同笼问题说起
1. 《孙子算经》中的鸡兔同笼问题
《孙子算经》中的鸡兔同笼问题:今有雉(zhì)兔同笼,上有35头,下有94足,问雉兔各几何?
对该问题,人们想出了多种解法。
解法1——抬腿法:
(1)假设所有的动物都抬起两条腿
(2)此时鸡没有腿,兔子有两条腿,此时腿的数量除以2,即为兔子的数量:(94-35*2)/2=12
(3)头的数量减掉兔子的数量,即为鸡的数量:35-rabbits
解法2——砍足法:
(1)每只动物均砍掉一半的脚,那么还剩94/2=47只脚,这47只脚中,每只鸡1只脚,每只兔子2只脚,那么47-35=12即为rabbits的数量。即rabbits*2+chicken=47, rabbits+chicken=35
(3)头的数量减掉兔子的数量,即为鸡的数量:35-rabbits=35-12=23
解法2——假设法:
假设全是鸡,其脚数为:35*2=70(只脚)
此时,鸡的总脚数比实际总脚数少:94-70=24(只脚)
则
兔的数量:24/(4-2)=12(只兔)
鸡的数量:35-12=23(只鸡)
或者
假设全是兔子,总脚数为:35*4=140(只脚)
此时,总脚数比实际总脚数多:140-94=46(只脚)
多的这些脚是在每一只鸡上都添加了2只脚得到的,所以
鸡:46/(4-2)=23(只鸡)
兔:35-23=12(只兔)
以上侧重于运用数学思维来解决问题,下面采用计算思维来解决问题,具体而言,就是采用枚举法来解决问题。因为问题非常简单,这里不予分析,读者可以直接阅读代码来体会枚举法的基本思想。
#include<iostream>
using namespace std;
int main() {
unsigned int chickens, rabbits;
for(rabbits=1; rabbits<23; rabbits++) {
chickens = 35 - rabbits;
if(chickens*2 + rabbits*4 == 94) {
cout << "rabbits: " << rabbits << " ";
cout << "chickens: " << chickens << endl;
}
}
return 0;
}
运行代码,输出结果为:
rabbits: 12 chickens: 23
2. 鸡兔同笼问题的其他版本
(1)中国有这样一首民谣:
一队强盗一队狗,
两队并作一对走,
数头一共三百六,
数腿一共八百九,问
多少强盗多少狗?
这道题和《算子算经》中的“鸡兔同笼”是一种类型。只不过把鸡换成了强盗,把兔换成了狗。具体算法可以如下:
(360*4-890)/(4-2)=275
360-275=85
所以,强盗有275人,狗有85条。
(2)现有龟鹤共10只,共有28足,问龟鹤各有几只?
使用砍足法,每只动物均砍掉一半的脚,有
龟:28/2-10=4只
鹤:10-4=6只
2. 其他类似的问题
(1)在日本江户时代出版的《算法童子问》书中记载这样一个问题:院子里有 鸡(chickens)和狗(dogs),厨房的菜墩上还有章鱼(octopuses)。鸡、狗、章鱼合在一起是24个,一共有102只脚。问鸡、狗合章鱼各是多少?(提示:章鱼有8只脚)
因为涉及的对象变为了3种,脚的数量也变成了3种(2只脚,4只脚,8只脚),前面所述鸡兔同笼问题的各种解法都不适用了。这里索性就直奔枚举法了。
代码如下:
#include<iostream>
using namespace std;
int main() {
unsigned int chickens, dogs, octopuses;
for(chickens=1; chickens<24; chickens++) {
for(dogs=1; dogs<24; dogs++) {
octopuses = 24 - chickens -dogs;
if(chickens*2 + dogs*4 + octopuses*8 == 102) {
cout << chickens << " " << dogs << " " << octopuses << endl;
}
}
}
return 0;
}
这里枚举的上限都是24,即假设24个或者全部是chickens,或者全部是dogs。运行程序,输出为
1 21 2
3 18 3
5 15 4
7 12 5
9 9 6
11 6 7
13 3 8
显然,这个问题的解有多组。
三、其他可以采用枚举法的题例
1. 原材料与各产品生产数量问题
某公司使用某种原材料加工两种规格的产品,其中生产每件A产品消耗原材料56kg,生产每件B产品消耗原材料64kg。假设该公司现有800kg的原材料,编写程序计算A、B各生存多少件可以使得剩余的原材料最少?
分析:
由题意可以得出一个二元一次方程:56x + 64y <= 800,通过两个for循环枚举求解即可。但因为这里是<= 800,而不是刚好<= 800,所以这里需要找到那个使得剩余原材料最少的那个最接近或者等于800的那个数(这里设那个数为MAX)。
#include<iostream>
#include<algorithm>
using namespace std;
int main() {
int m = 800;
int a=56, b=64, MAX = -1; //MAX存放最大消耗量
for(int i = 0; i*a<= m; ++i) {
for(int j = 0; i*a + j*b <= m; ++j)
MAX = max(MAX, i*a+j*b);
}
for(int i = 0; i*a <= m; ++i) {
for(int j = 0; i*a + j*b <= m; ++j)
if(MAX == i*a+j*b ) {
cout << "A: " << i << " ";
cout << "B: " << j << endl;
cout << "Material left: " << (m-MAX) << endl;
cout << endl;
}
}
return 0;
}
运行程序,输出如下
A: 4 B: 9
Material left: 0
A: 12 B: 2
Material left: 0
2. ABCD*E=DCBA问题
有这样一个算式:ABCD*E=DCBA 。其中,A、B、C、D、E代表不同的数字(E != 0,E != 1)。编一个程序,找出A、B、C、D、E分别代表什么数字?
#include<iostream>
using namespace std;
int main() {
int A,B,C,D,E;
for(unsigned int n=1000; n<9999 ; n++) {
A=n/1000;
B=n%1000/100;
C=n%100/10;
D=n%10;
for(unsigned int E=2; E<=9; E++) {
if(n*E == D*1000+C*100+B*10+A) {
cout<<n<<"*"<<E<<"="<<n*E<<endl;
break;
}
}
}
}
运行程序,输出
10899=9801
21784=8712
上面的代码需要加入判断A、B、C、D、E各不相同的逻辑判断以进一步完善之。
或者可以(在外层循环)先确定E,再来优化对数ABCD的枚举,以减少程序循环的次数。
类似的问题
ABCDE * F = EDCBA
其中ABCDE代表不同的数字,F也代表某个数字,F !=0 且F != 1(F不等于0且F不等于1)。
3. abcde/fghij=n问题
(1)abcde/fghij=n,其中a,b,c,…,j为数字0~9的不重复的排列(可以有前导0),这里的除为整除,2<=n<=79,
1)按从小到大输出所有形如abcde/fghij=n的表达式;
2)统计这样的组合一共有多少个?
对于这道题大多数人的思路是直接对abcde和fghij进行0~9的枚举,但是这样会导致枚举次数过多。
如何优化,直接看代码。然后可知看似简单的一道题加入枚举前的有效算法设计后能大大提升程序运行的效率。
#include<iostream>
using namespace std;
// 判断两个数是不是由十个数字排列组合而成,数字可以有前导0
// a是一个5位数,b是一个5位数,例如
// 79546/01283=62
// 94763/01528=62
bool judge(int a, int b) {
int p[10] = {11}; //设置一个数组,用来存放形参的各个数位数字;初始化数组
int m;
for(int i=0; i<5; i++) { //求a的各个数位数字
m = a%10;
p[m] = m; //将对应的数字放到数组中对应的位置,如a[2]=2
a /= 10;
}
for(int j=0; j<5; j++) { //求b的各位数字
m = b%10;
p[m] = m;
b /= 10;
}
for(int k=0; k<10; k++) { //判断a和b的各位数字是否有重复
if(p[k] != k) //若有重复,则必有a[n]=11;其中n属于0-9;
return false;
}
return true; //各位数字都不同,返回true
}
int main() {
int n, i;
int c=0; //累计次数,并初始化
for(i=2; i<=79; i++) { //求i范围内的循环
for(n=1234; n*i<=98765; n++) {//两个五位数相除变为分母乘以商,并控制循环范围
if(judge(n, n*i)) { //判断两个数是不是0~9十个数字的排列
cout << (n*i) << " / " << n << " = " << i << endl;
c++;
}
}
}
cout << "c=" << c << endl;
return 0;
}
运行程序,得到c=281;按从小到大输出所有形如abcde/fghij=n的表达式这里节选如下:
13458 / 6729 = 2
13584 / 6792 = 2
13854 / 6927 = 2
…
97062 / 48531 = 2
97230 / 48615 = 2
97302 / 48651 = 2
…
79546 / 1283 = 62
94736 / 1528 = 62
83754 / 1269 = 66
98736 / 1452 = 68
4. 学生列队问题(韩信点兵问题)
(1)某年级的所有同学列队,每9人一排多6人,每7人一排多2人,每5人一排多3人。问该年级至少有多少人?
#include<iostream>
using namespace std;
int main() {
for(int i=5; i<=2000; i++) {
if(i%9==6 && i%7==2 && i%5==3) {
cout << "The number of students is: " << i << endl;
}
}
return 0;
}
运行程序,输出如下
The number of students is: 303
The number of students is: 618
The number of students is: 933
The number of students is: 1248
The number of students is: 1563
The number of students is: 1878
显然,该年级至少有303人。我们可以直接在输出语句后面添加一条break;语句,以终止程序不必要的循环。
(2)在中国数学史上,广泛流传着一个“韩信点兵”的故事:韩信是汉高祖刘邦手下的大将,他英勇善战,智谋超群,为汉朝建立了卓越的功劳。据说韩信的数学水平也非常高超,他在点兵的时候,为了知道有多少兵,同时又能保住军事机密,便让士兵排队报数:
按从1至5报数,记下最末一个士兵报的数为1;
再按从1至6报数,记下最末一个士兵报的数为5;
再按从1至7报数,记下最末一个士兵报的数为4;
最后按从1至11报数,最末一个士兵报的数为10;
请编写程序计算韩信至少有多少兵。
#include <iostream>
using namespace std;
int main() {
for(int i = 1; ; i++) {
if(i%5==1 && i%6==5 && i%7==4 && i%11==10){
cout << "the number of soldiers: " << i << endl;
break;
}
}
return 0;
}
运行程序,输出
the number of soldiers: 2111
(3)有一个较长的阶梯,若每步上2个台阶,最后剩1阶;每步上3个台阶,最后剩2阶;每步上5个台阶,最后剩4阶;每步上6个台阶,最后剩5阶;每步上7个台阶,最后正好1阶不剩。编写程序,求该阶梯至少有多少阶。
int main() {
for(unsigned int n=7; ; n+=7) {
if(n%2==1 && n%3==2 && n%5==4 && n%6==5) {
cout<<n<<endl;
break;
}
}
}
其他同类的问题还有许多。例如
在中国古代著名数学著作《孙子算经》中,有一道题目叫做“物不知数”,原文如下:
有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二。问物几何?
即,一个整数除以三余二,除以五余三,除以七余二,求这个整数。
中国数学家秦九韶于1247年做出了完整的解答,口诀如下:
三人同行七十希,五树梅花廿一支,七子团圆正半月,除百零五使得知。
不过,秦九韶并不是采用的枚举法,而是采用中国余数定理(或称中国剩余定理)解答的,其解法实际上给出了求解一般同余方程组的方法,这里略过。
5. 割羊毛问题
(1)雯雯家养了70只绵羊,每只大羊可剪羊毛1.6kg,每只羊羔可剪羊毛1.2kg。现在总共剪得羊毛106kg,请问大羊和羊羔各有多少只?
int main() {
int n = int(106/1.6);
// cout << n << endl; // 66
for(int bigsheep=1; bigsheep<=n; bigsheep++) {
int sheep = 70 - bigsheep;
if(bigsheep*1.6 + sheep*1.2 == 106) {
cout << "bigsheep: " << bigsheep << ", ";
cout << "sheep: " << sheep << endl;
}
}
return 0;
}
运行程序,输出
bigsheep: 55, sheep: 15
(2)假设1瓶醇酒能喝醉3个人,3瓶醨酒能喝醉1个人。33个客人共喝了19瓶就全醉倒了。请问他们一共喝了几瓶醇酒、几瓶醨酒?
int main() {
for(int liquor=1; liquor<=11; liquor++) {
int wine = 19 - liquor;
if(liquor*3 + wine/3.0 == 33) {
cout << "liquor: " << liquor << ", ";
cout << "wine: " << wine << endl;
}
}
return 0;
}
运行程序,输出
liquor: 10, wine: 9
5. 百钱百鸡问题
所谓百鸡问题,用通俗的说法就是:“用100文钱买100只鸡”的问题。其具体指的是魏晋时期的数学家张丘建的的古算书《张丘建算经》中提出的著名的百鸡问题:“鸡翁一值钱五,鸡母一值线三,鸡雏三值钱一,百钱买百鸡,问鸡翁、鸡母、鸡雏各几何?”意思是说,一只公鸡五文钱,一只母鸡三文钱,三只小鸡一文钱,要用一百文钱刚好买一百只鸡,问公鸡、母鸡和小鸡各买多少只?
分析:
我们考虑分别用x、y、z(均为自然数)来表示大鸡、母鸡与小鸡的数量,那么我们有以下方程:
5x + 3y + (1/3)z = 100 (公鸡5元,母鸡3元,小鸡1元)
x + y + z = 100 (一共100只鸡)
其中第一个方程是钱数方程,第二个方程是数量方程。在此基础上,我们想要求出x、y、z的数量。
我们可以枚举所有可能的x、y值,来判断是否满足方程的要求。具体实现方法如下:
int main() {
int x, y, z;
for (x=0; x<=20; x++) { //cocks 100/5
for (y=0; y<=33; y++) { //hens 100/3
z = 100-x-y; //chicks
if(z>=0 && z%3==0 && 5*x+3*y+z/3 == 100) { //总共100文钱
cout << x << ", " << y << ", " << z << endl;
}
}
}
return 0;
}
运行程序,输出
0, 25, 75
4, 18, 78
8, 11, 81
12, 4, 84
可见,本问题有4组解。
对于程序中if语句的逻辑表达式,这里进行了优化,写成了
z>=0 && z%3==0 && 5*x+3*y+z/3 == 100
但也可以仅仅写成如下形式
5*x + 3*y + z/3.0 == 100
读者稍微比较一下,应该可以看出,前者的效率要高于后者(当然这里问题的规模很小,基本体现不出来),虽然表达式在形式上要长于后者。
练习:
(1)今有1000文钱要去买100只鸡。公鸡每只50文,母鸡每只30文,小鸡3只10文。请问公鸡、母鸡和小鸡各可以买多少只?
(2)今有银子100两,买牛100头,大牛1头10两,小牛1头5两,牛犊1头半两。问大牛、小牛、牛犊各买了多少头?
int main() {
for(int cattle=0; cattle<=10; cattle++) {
for(int cattle2=0; cattle2<=20; cattle2++) {
int cattle3 = 100 - cattle - cattle2;
if(cattle3%2 != 0) continue;
if(10*cattle + 5*cattle2 + 1.0/2*cattle3 == 100) {
cout << cattle << ", " << cattle2 << ", " << cattle3 << endl;
}
}
}
return 0;
}
运行代码,输出
1, 9, 90
(3)用150元钱买3种水果。各种水果加起来一共100个。西瓜10元一个,苹果3元一个,橘子1元1个。设计一个程序,输出每种水果各买了多少个。
int main() {
for(int cattle=0; cattle<=10; cattle++) {
for(int cattle2=0; cattle2<=20; cattle2++) {
int cattle3 = 100 - cattle - cattle2;
if(cattle3%2 != 0) continue;
if(10*cattle + 5*cattle2 + 1.0/2*cattle3 == 100) {
cout << cattle << ", " << cattle2 << ", " << cattle3 << endl; // 1, 9, 90
}
}
}
return 0;
}
运行代码,输出
0 25 75
2 16 82
4 7 89
总结
枚举属于“暴力解决”的范畴,现实生活中,很多问题都可以“暴力解决”——不用太动脑筋,把所有的可能性都列举出来,然后——实验。尽管这样的方法显得很“笨”,但却常常是行之有效的,尤其是在利用计算机来解决问题时。
本文收集整理的上述枚举算法的一些题例,涉及的都是一些相对简单的内容,例如整数、子串。虽然说是枚举算法(也称“穷举法”),这种方法解决问题相对不用太动脑筋,我们同样要注意程序运行的效率问题。在枚举之前对问题进行一定的分析、简化会让写出的枚举算法更加简洁、高效。特别是在问题的规模很大的情况下,往往我们对程序代码的一个小小的优化和改进,就能显著地提升程序的执行效率。
当然,枚举算法也可以枚举复杂的对象。但从简单的问题着手来掌握枚举的基本思想无疑是首要的。
实际上,枚举算法也是培养计算思维最直接的方式和手段。