第三节——枚举

专栏

算法(C语言)

第三节——枚举

奥数题

枚举算法又叫做穷举算法,光听这名字是不是就觉得很暴力很暴力呢。我们首先还是从一个小学三年级的奥数题开始吧。

这道奥数题是这样的,□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 每个数都有且只出现过一次。你可能还会觉得这还是很坑,那还有没有更好的方法呢?当然有!我们将在专栏下一节的搜索里面彻底地解决这个问题。现在还是先请看下面——炸弹人的策略。

炸弹人

还记得在小霸王游戏机上面的炸弹人吗?用放置炸弹的方法来消灭敌人。须将画面上的敌人全部消灭后,并找到隐藏在墙里的暗门才能过关。

炸弹人1

现在有一个特殊的关卡如下。你只有一枚炸弹,但是这枚炸弹威力超强(杀伤距离超长,可以消灭杀伤范围内所有的敌人)。请问在哪里放置炸弹才可以消灭最多的敌人呢。

炸弹人2

我们先将这个地图模型化。墙用 # 表示。这里有两种墙,一种是可以被炸掉的,另外一种是不能被炸掉的。但是由于现在只有一枚炸弹,所以都用 # 表示,炸弹是不能穿墙的。敌人用 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)这个位置,如下图。

炸弹人3

根据我们之前的算法,应该将炸弹放置在(1,11)处,最多可以消灭11个敌人。其实小人根本无法走到(1,11)处。所以正确的答案应该是将炸弹放在(7,11)处,最多可以消灭10个敌人。那如何解决这种问题呢?不要着急,我们将在专栏的下一节的搜索再来讨论。

火柴棍等式

现在我们有 n 根火柴棍,希望拼出形如 A+B=C 的等式。等式中的 A、B、C 均是用火柴棍拼出来的整数(若该数非零,则最高位不能是 0)。数字 0~9 的拼法如下图所示:

数字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 。

注意:

  1. 加号与等号各自需要两根火柴棍。
  2. 如果 A≠B,则 A+B+c 与 B+A=C 视为不同的等式( A、B、C 都大于 0)。
  3. 所有根火柴棍必须全部用上。

假如现在我们手上有 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 的全排列。

要解决这个问题是可以做出来的,只是非常繁琐。那有没有方便一点的方法呢?请看第四节——搜索。

参考:《啊哈!算法》

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FantasyQin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值