枚举法,作为编程世界里一个非常基本的方法或者说技巧,它也可以叫穷举法、暴力法、遍历法,深入了解一些算法后,你会发现它在算法世界当中的用途非常的广泛。
概括地说这种方法非常的简单,我们抽象点来说,对于一个问题的解x,这个解满足限制条件f(x),枚举法给出解决问题的方案是一一列举x所有可能的情况,然后判断是否满足限制条件f(x),如果满足,则x是问题的解;反之,不是。
枚举法虽然简单,但是在解决实际问题中往往伴随着比较巧妙的优化与筛选,而且在枚举算啊中体现出的“找到x所有可能的情况”,在其他的一些算法(比如计算几何)也有着用武之地。因此所谓暴力法看似暴力无脑,其实里面还是有很多需要深思熟虑的地方。
我们来看一个例子:对于()()()+()()() = ()()(),9个空填写1~9这9个数字各一次,请问会有多少种符合的情况?
显然我们枚举每一个()可能的取值,即[1,9],需要设置9层循环,然后判断这9个数是否相等,随后验证等式成立,但是基于这个思路编程我们发现在判断9个数是否相同的时候太过麻烦了,有没有较为简便的编码方式呢?
其实类似我们在第一章介绍的桶排序的方式,我们用a[i]表示第i个()的数字,然后我们用book[a[i]]来标记数字a[i]是否出现过,如果∑book[a[i]] = 9 ,则表明9个数字分别出现了一次,是满足限制条件的。
简单的参考代码如下。
#include<cstdio> int main() { int a[10] , i , total = 0 , book[10] , sum; for(a[1] = 1;a[1]<=9;a[1]++) for(a[2]=1;a[2]<=9;a[2]++) for(a[3]=1;a[3]<=9;a[3]++) for(a[4]=1;a[4]<=9;a[4]++) for(a[5]=1;a[5]<=9;a[5]++) for(a[6]=1;a[6]<=9;a[6]++) for(a[7]=1;a[7]<=9;a[7]++) for(a[8]=1;a[8]<=9;a[8]++) for(a[9]=1;a[9]<=1;a[9]++) { for(i = 1;i <= 9;i++) book[i] = 0; for(i = 1;i <= 9;i++) book[a[i]] = 1; sum = 0; for(i =1 ;i <= 9;i++) sum+= book[a[i]]; if(sum==9 &&100*a[1] + 10*a[2] + a[3] + 100*a[4] + 10*a[5] + a[6] == 100*a[7] + 10*a[8] + a[9]) { total++; } } printf("total = %d",total/2); //由加法交换律我们不难理解这里应该除2的,对于结果进行符合题意的筛选计算往往是枚举的关键 }
我们来看另外一个简单的用到枚举的例子——生成数的全排列。
假设我们想要得到1234的所有排列情况,我们便可以设置四层嵌套循环,当四层循环的参数各不相同的时候,则表明当前可以得到一个排列情况,输出即可。
简单的参考代码如下。
for(a = 1;a<=4;a++) for(b=1;b<=4;b++) for(c=1;c<=4;c++) for(d=1;d<=4;d++) if(a!=b &&a!=c&&a!=d&&b!=c&&b!=d&&c!=d) printf("%d%d%d%d ",a,b,c,d);
但是我们会看到这种利用枚举的方法生成全排列的方法,在生成12345……n(n取很大的时候),这种方法就显得有些捉襟见肘了(尤其是判断n个数不相等的时候,而且在循环中出现了大量的数字重复情况浪费了时间),有没有更好的方法呢?在这里先埋下伏笔,在介绍搜索的章节中我们会进行详细的讨论。
我们再给出一个用枚举算法解决的实例。
炸弹人游戏:用二维矩阵储存一个图,:"G"表示有敌人,"#"表示不可破坏的砖头,"."表示空地和可破坏的砖头。炸弹的破坏力极大,可以杀死以安置点为原点的直角坐标系上的任意一个敌人(前提是图中没有被不可破坏的砖头阻隔),那么炸弹应该安置在哪个店,能够杀死最多的敌人呢?
很显然,基于我们储存的图,我们从第一个点开始,想上、下、左、右四个方向走,记录能够杀死的敌人,然后枚举图中的所有点,维护一个最大值即可。
简单的程式如下。
#include<cstdio> using namespace std; int main() { char a[20][21]; int i , j , sum , map = 0 , p , q , x , y , n, m; scanf("%d%d",&n,&m); for(i = 0;i<=n-1;i++) scanf("%s",a[i]); for(i=0;i<=n-1;i++) { for(j = 0;j<=m-1;j++) { if(a[i][j] == '.') { sum = 0; x = i;y = j; while(a[x][y] != '#') { if(a[x][y] == 'G') sum++; x--; } x = i;y = j; while(a[x][y]!='#') { if(a[x][y] == 'G') sum++; x++; } x = i;y = j; while(a[x][y] != '#') { if(a[x][y] == 'G') sum++; y--; } x=i;y=j; while(a[x][y] != '#') { if(a[x][y] == 'G') sum++; y++; } if(sum > map) { map = sum;p = i ,q = j; } } } } printf("将炸弹放置在(%d,%d),最多可消灭%d个敌人 ",p,q,map); return 0; }
我们再来看一个用到枚举算法典型——火柴等式游戏。
一直每个数字用火柴拼出的数目,游戏内容是给出m根(m<=24)火柴,要求拼出() + () = ()的等式,问:这样的等式有多少种?(A!=B时,A + B = C与B + A = C为两种拼法)。其中,”+“与”=“分别用两个火柴
这道题其实体现了枚举的解决实际问题的一个特点——道理简单但是处处藏着优化。
我们从题设条件出发,对于m<=24我们能够提取出怎样的有效信息呢?显然它决定了我们枚举的范围,我们至多有20根火柴来拼数字,而用火柴拼1是最节约的,20个火柴平摊给3个数,每个数字也就分7根火柴,我们凑一个8,能够拼出来的数字为1111,事实上考虑到加法1111这个数字还是太大。因此我们得知,每个数字不会超过1111,那么我们枚举每一位数字,然后计算其使用的火柴,判断其是否满足我们所有的火柴根数,便可得到等式了。
这里还有很关键的一步优化,这里有必要枚举三个数字么?事实上,我们枚举两个数字,通过等式便可以得到第三个数字,并且不用再判断相等(枚举三个数字显然要判断等式是否成立),一举两得,节省了很多时间。
由此我们不难发现,在用暴力枚举解决问题时,思维中一定要时刻绷着优化的弦。
简单的参考代码如下。
#include<cstdio> int fun(int x) { int num = 0; int f[10] ={6,2,5,5,4,5,6,3,7,6}; while(x/10 != 0) { num += f[x%10]; x = x/10; } num += f[x]; return num; } int main() { int a , b , c , m , i , sum = 0; scanf("%d",&m); for(a = 0;a <= 1111;a++) { for(b = 0;b<= 1111;b++) { c = a + b; if(fun(a) + fun(b) + fun(c) == m-4) { printf("%d+%d=%d ",a,b,c); sum++; } } } printf("一共可以拼出%d个不同等式",sum); return 0; }