《啊哈算法》——枚举

  枚举法,作为编程世界里一个非常基本的方法或者说技巧,它也可以叫穷举法、暴力法、遍历法,深入了解一些算法后,你会发现它在算法世界当中的用途非常的广泛。

  概括地说这种方法非常的简单,我们抽象点来说,对于一个问题的解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;
}
原文地址:https://www.cnblogs.com/rhythmic/p/5449641.html