专栏
第三节——枚举
奥数题
枚举算法又叫做穷举算法,光听这名字是不是就觉得很暴力很暴力呢。我们首先还是从一个小学三年级的奥数题开始吧。
这道奥数题是这样的,□3×6529=3□×8256,在两个□内填入相同的数字使得等式成立。这题用3行代码就可以搞定。
for(i=1;i<=9;i++)
if((i*10+3)*6528 == (30+i)*8256)
printf("%d",i);
这就是最简单的枚举算法.枚举算法的基本思想就是“有序地去尝试每一种可能”。
现在我们来一个稍微复杂一点的奥数题,□□□+□□□=□□□,将数字1~9分别填入9个□中,每个数字只能使用一次使得等式成立。例如 173+286=459 就是一个合理的组合,请问一共有多少种合理的组合呢?注意: 173+286=459 与 286+173=459 是同一种组合!根据枚举思想我们只需要枚举每一位上所有可能的数就好了。
#include <stdio.h>
int main()
{
int a,b,c,d,e,f,g,h,i,total=0;
for(a=1;a<=9;a++)//第1个数的百位
for(b=1;b<=9;b++)//第1个数的十位
for(c=1;c<=9;c++)//第1个数的个位
for(d=1;d<=9;d++)//第2个数的百位
for(e=1;e<=9;e++)//第2个数的十位
for(f=1;f<=9;f++)//第2个数的个位
for(g=1;g<=9;g++)//第3个数的百位
for(h=1;h<=9;h++)//第3个数的十位
for(i=1;i<=9;i++)//第3个数的个位
{ //接下来要判断每一位上的数互不相等
if(a!=b && a!=c && a!=d && a!=e && a!=f && a!=g && a!=h && a!=i && b!=c && b!=d && b!=e && b!=f && b!=g && b!=h && b!=i && c!=d && c!=e && c!=f && c!=g && c!=h && c!=i && d!=e && d!=f && d!=g && d!=h && d!=i && e!=f && e!=g && e!=h && e!=i && f!=g && f!=h && f!=i && g!=h && g!=i && h!=i && a*100+b*10+c+d*100+e*10+f==g*100+h*10+i)
{
total++;
printf("%d%d%d+%d%d%d=%d%d%d\n",a,b,c,d,e,f,g,h,i);
}
}
printf("total=%d",total/2);//想一想为什么要除以2
getchar();getchar();
return 0;
}
注意因为 173+286=459 与 286+173=459 是同一种组合,因此我们在输出的时候需要将 total 除以 2。
这个时候你会发现,如果以后都这样写代码那真是太麻烦了,特别是判断 a、b、c、d、e、f、g、h、i 这九个互不相等的部分。还有更好的方法来实现吗?当然有!那就是我们前面第一节——排序中学过的标记法(简化版桶排序)!用一个 book 数组来解决互不相等的问题。请看下面这段代码。
#include <stdio.h>
int main()
{
int a[10],i,total=0,book[10],sum;
//这里用a[1]=1~a[9]来代替刚才的a,b,c,d,e,f,g,h,i
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]<=9;a[9]++)
{
for(i=1;i<=9;i++) //初始化book数组
book[i]=0;
for(i=1;i<=9;i++) //如果某个数出现过就标记一下
book[a[i]]=1;
//统计共出现了多少个不同的数
sum=0;
for(i=1;i<=9;i++)
sum+=book[i];
//如果正好出现了9个不同的数,并且满足等式条件,则输出
if(sum == 9 && a[1]*100+a[2]*10+a[3]+a[4]*100+a[5]*10+a[6]==a[7]*100+a[8]*10+a[9])
{
total++;
printf("%d%d%d+%d%d%d=%d%d%d\n",a[1],a[2],a[3],a[4],a[5],a[6],a[7],a[8],a[9]);
}
}
printf("total=%d",total/2);
getchar();getchar();
return 0;
}
上面这段代码中,为了方便标记哪些数出现过,我将循环变量 a、b、c、d、e、f、g、h、i 用一个一维数组 a 来代替,用 book 数组来标记 1~9 每个数是否出现过,默认为 0 ,出现过的就设为 1 。然后我们只需要判断 book 数组中有多少个 1 就可以了。如果恰好有 9 个 1 则表示,1~9 每个数都有且只出现过一次。你可能还会觉得这还是很坑,那还有没有更好的方法呢?当然有!我们将在专栏下一节的搜索里面彻底地解决这个问题。现在还是先请看下面——炸弹人的策略。
炸弹人
还记得在小霸王游戏机上面的炸弹人吗?用放置炸弹的方法来消灭敌人。须将画面上的敌人全部消灭后,并找到隐藏在墙里的暗门才能过关。
现在有一个特殊的关卡如下。你只有一枚炸弹,但是这枚炸弹威力超强(杀伤距离超长,可以消灭杀伤范围内所有的敌人)。请问在哪里放置炸弹才可以消灭最多的敌人呢。
我们先将这个地图模型化。墙用 # 表示。这里有两种墙,一种是可以被炸掉的,另外一种是不能被炸掉的。但是由于现在只有一枚炸弹,所以都用 # 表示,炸弹是不能穿墙的。敌人用 G 表示,空地用 . 表示,当然炸弹只能放在空地上。
#############
#GG.GGG#GGG.#
###.#G#G#G#G#
#.......#..G#
#G#.###.#G#G#
#GG.GGG.#.GG#
#G#.#G#.#.###
##G...G.....#
#G#.#G###.#G#
#...G#GGG.GG#
#G#.#G#G#.#G#
#GG.GGG#G.GG#
#############
首先我们需要用一个二位字符数组来存储这个地图,至于将炸弹放置在哪一个点可以消灭的敌人最多,则需要一个个地来尝试。炸弹的爆炸方向是沿上下左右四个方向,因此我们在对每个点进行枚举的时候,需要沿着上下左右四个方向分别统计可以消灭敌人的数目。最终输出可以消灭敌人最多的那个点。请注意这里是从 0 行 0列开始计算的。
如何分别统计上下左右四个方向上可以消灭的敌人数呢?只要搞清楚一个方向,其他的方向都是一样的,这里就以向下统计为例。向下就是 y 不变,x 每次增加 1,直到遇到墙为止。
//向下统计可以消灭的敌人数
while(a[x][y] != '#')
{
if(a[x][y] == "G")
sum++; //如果可以消灭一个敌人就sum++
x++; //x++的作用是继续向下
}
想另外几个方向进行统计的坐标变化如下。
接下来只需要统计在每一个空地上放置炸弹可以消灭的敌人总数(上下左右四个方向上可以消灭的敌人数之和)。最终输出消灭敌人数最多的那个空地的坐标即可,完整代码如下。
#include <stdio.h>
int main()
{
char a[20][21];
int i,j,sum,map=0,p,q,x,y,n,m;
//读入n和m,n表示有多少行字符,m表示每行有多少列
scanf("%d %d",&n,&m);
//读入n行字符
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;//sum用来计数(可以消灭的敌人数),所以需要初始化0
//将当前坐标i,j复制到两个新变量x,y中,以便向上下左右四个方向分别统计可以消灭的敌人数
//向上统计可以消灭的敌人数
x=i;y=j;
while(a[x][y] != '#')//判断是不是墙,如果不是墙就继续
{
//如果当前点是敌人,则进行计数
if(a[x][y] == 'G')
sum++;
//x--的作用是继续向上统计
x--;
}
//向下统计可以消灭的敌人数
x=i;y=j;
while(a[x][y] != '#')
{
if(a[x][y] == 'G')
sum++;
//x++的作用是继续向下统计
x++;
}
//向左统计可以消灭的敌人数
x=i;y=j;
while(a[x][y] != '#')
{
if(a[x][y] == 'G')
sum++;
//y--的作用是继续向左统计
y--;
}
//向右统计可以消灭的敌人数
x=i;y=j;
while(a[x][y] != '#')
{
if(a[x][y] == 'G')
sum++;
//y++的作用是继续向右统计
y++;
}
//更新map的值
if(sum>map)
{
//如果当前点所能消灭的敌人总数大于map,则更新map
map=sum;
//并用p和q记录当前点的坐标
p=i;
q=j;
}
}
}
}
printf("将炸弹放置在(%d,%d),最多可以消灭%d个敌人\n",p,q,map);
getchar();getchar();
return 0;
}
可以输入以下数据进行验证。第一行两个整数为 n m,分别表示迷宫的行和列,接下来的 n 行 m列为地图。
13 13
#############
#GG.GGG#GGG.#
###.#G#G#G#G#
#.......#..G#
#G#.###.#G#G#
#GG.GGG.#.GG#
#G#.#G#.#.###
##G...G.....#
#G#.#G###.#G#
#...G#GGG.GG#
#G#.#G#G#.#G#
#GG.GGG#G.GG#
#############
运行结果是:
将炸弹放置在(9,9)处,最多可以消灭8个敌人
喜欢思考的同学会发现这个算法有问题。比如我们将地图(6,11)的墙改为平地,小人默认站在(3,3)这个位置,如下图。
根据我们之前的算法,应该将炸弹放置在(1,11)处,最多可以消灭11个敌人。其实小人根本无法走到(1,11)处。所以正确的答案应该是将炸弹放在(7,11)处,最多可以消灭10个敌人。那如何解决这种问题呢?不要着急,我们将在专栏的下一节的搜索再来讨论。
火柴棍等式
现在我们有 n 根火柴棍,希望拼出形如 A+B=C 的等式。等式中的 A、B、C 均是用火柴棍拼出来的整数(若该数非零,则最高位不能是 0)。数字 0~9 的拼法如下图所示:
例如现在我们手上有 14 根火柴棍,则可以拼出两个不同的等式 0+1=1 和 1+0=1.
再例如我们手上有 18 根火柴棍,则可以拼出 9 个不同的等式,分别为 0+4=4、0+11=11、1+10=11、2+2=4、2+7=9、4+0=4、7+2=9、10+1=11 和 11+0=11 。
注意:
- 加号与等号各自需要两根火柴棍。
- 如果 A≠B,则 A+B+c 与 B+A=C 视为不同的等式( A、B、C 都大于 0)。
- 所有根火柴棍必须全部用上。
假如现在我们手上有 m 根(m ≤ 24)火柴棍,那么我们究竟可以拼出多少个不同的形如 A+B=C 的等式呢?规定的时限是1秒。
既然是要找出形如 A+B=C 这样的等式,那么最简单的办法就是分别枚举 A、B、C 啦。接下来的问题就是:A、B、C的枚举范围是什么呢?我们只需要在 0~1111 之间枚举就可以了。因为减去加号与等号占用的火柴棍,那么最多剩下 20 根火柴棍。而在10个数字中,数字 1 用到的火柴棍最少。而20根火柴棍最多能组 10 个 1。因此在等式中 A、B、C 中任意一个数都不能超过1111.
接下来就简单了,我们只需要分别枚举 A、B、C。这个算法的时间复杂度是 O(N³)≈11112m³很高,其实我们这里只需要枚举 A 和 B 就可以了,C 可以通过 A+B 算出来,就将原来需要运行 1000多秒的程序降低到了 1 秒。
#include <stdio.h>
int fun(int x)
{
int num=0;//用来计数的变量,一定要记得初始化
int f[10]={6,2,5,5,4,5,6,3,7,6};//用一个数组来记录0~9每个数字需要用多少根火柴棍
while(x/10!=0)//如果x/10的商不等于0的话,说明这个数至少有两位
{
//获得x的末尾数字并将此数所需要到的火柴棍根数累加到num中
num += f[x%10];
x = x/10; //去掉x的末尾数字,例如x的值为123现在x的值为12
}
//最后加上此时x所需用到的火柴棍的根数(此时x一定是个位数)
num += f[x];
return num;//返回需要火柴棍的总根数
}
int main()
{
int a,b,c,m,i,sum=0;//sum是用来计数的,因此一定要初始化为0
scanf("%d",&m);//读入火柴棍的个数
//开始枚举a和b
for(a=0;a<=1111;a++)
{
for(b=0;b<=1111;b++)
{
c=a+b; //计算出c
//fun是我们自己写的子函数,用来计算一个数所需要用火柴棍的总数
//当a使用的火柴棍根数 + b使用的火柴棍的根数 + c使用的火柴棍的根数之和恰好等于m-4时,则成功地找出了一组解
if(fun(a)+fun(b)+fun(c)==m-4)
{
printf("%d+%d=%d\n",a,b,c);
sum++;
}
}
}
printf("一共可以拼出%d个不同的等式",sum);
getchar();getchar();
return 0;
}
可以输入以下数据进行验证。
18
运行结果是:
18
0+4=4
0+11=11
1+10=11
2+2=4
2+7=9
4+0=4
7+2=9
10+1=11
11+0=11
一共可以拼出9个不同的等式
数的全排列
现在我们来研究一种特殊的排列——全排列
123 的全排列是 123、132、213、231、312、321。 1234的全排列是 1234、1243、1324、1342、1423、1432、2134、2143、2314、2341、2413、2431、3124、3142、3241、3214、3412、3421、4123、4132、4213、4231、4312、4321。我们来写个程序解决这个问题。
首先还是求 123 的全排列吧。很简单,三重循环嵌套就可以搞定,代码如下。
for(a=1;a<=3;a++)
for(b=1;b<=3;b++)
for(c=1;c<=3;c++)
if(a!=b && a!=c && b!=c)
printf("%d%d%d\n",a,b,c);
上面的代码中,我们用 for a 循环来枚举第一位,用 for b 循环来枚举第二位,用 for c 循环来枚举第三位。再用一个 if 语句来进行判断,只有当 a、b 和 c 互不相等的时候才能输出。
如果求 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 && b!=c && b!=d && c!=d)
printf("%d%d%d%d\n",a,b,c,d);
没错,123 和 1234 全排列尚算简单,但是求 123456789 的全排列这样写就比较麻烦了。OK,现在终极问题来了:输入一个指定的数 n,输出 1~n 的全排列,又该如何呢?例如:输入 3 时输出 123 的全排列,输入 4 时输出 1234 的全排列······输入 9 时输出 123456789 的全排列。
要解决这个问题是可以做出来的,只是非常繁琐。那有没有方便一点的方法呢?请看第四节——搜索。
参考:《啊哈!算法》