常用算法深入学习实录
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.2 比较“笨”的枚举算法思想

之所以说枚举方法比较“笨”,是因为在面对问题时它会去尝试每一种情况。打一个比方,假设有两个小朋友A和B在玩捉迷藏的游戏,规定只能藏在树上、屋顶上和墙角。A比较聪明,他在找之前会先考虑到B恐高,所以他推测出B只能藏在角落处;而B比较笨,他不会考虑A会不会爬树,他会随便选一个地方找,如果发现这个地方没有,他会去寻找另外一个地方。如果可以藏的地方有多个,B会挨个地方寻找,直到找到A为止。上述B挨个地方寻找的做法就和枚举算法思想的原理一样。

再看枚举思想的定义:在进行归纳推理时,如果逐个考察了某类事件的所有可能情况,因而得出一般结论,那么该结论是可靠的,这种归纳方法叫做枚举法。由此可见,上面例子中B的寻找过程就属于枚举思想的范畴。

2.2.1 枚举算法基础

枚举算法的思想是:将问题的所有可能的答案一一列举,然后根据条件判断此答案是否合适,保留合适的,丢弃不合适的。在C语言中,枚举算法一般使用while循环实现。使用枚举算法解题的基本思路如下所示:

(1)确定枚举对象、枚举范围和判定条件。

(2)逐一枚举可能的解,验证每个解是否是问题的解。

枚举算法一般按照如下3个步骤进行:

(1)题解的可能范围,不能遗漏任何一个真正解,也要避免有重复。

(2)判断是否是真正解的方法。

(3)使可能解的范围降至最小,以便提高解决问题的效率。

枚举算法的主要流程如图2-1所示。

图2-1 枚举算法流程图

2.2.2 实践演练

为了说明枚举算法的基本用法,接下来将通过一个具体实例的实现过程,详细讲解枚举算法思想在编程中的基本应用。

根据上述问题描述,我们用枚举算法解决了上述两个问题。首先解决实例1的问题,根据“百钱买百鸡”的枚举算法分析,我们编写实现文件xiaoji.c,具体实现代码如下所示。

#include <stdio.h>
int main()
{
    int x,y,z;//定义3个变量,分别表示公鸡、母鸡和小鸡的个数
    for(x=0;x<=20;x++)
    {
        for(y=0;y<=33;y++)
        {
          z=100-x-y;
          if (z%3==0 &&x*5+y*3+z/3==100) //3种鸡一共100只
                printf("公鸡:%d,母鸡:%d,小鸡:%d\n",x,y,z);
        }
    }
    getch();
    return 0;
}

执行后的效果如图2-2所示。

图2-2 执行效果

一个实例不能说明枚举算法思想的基本用法,接下来开始解决“举一反三”中的问题,根据“填写运算符”的枚举算法分析,编写实现文件yunsuan.c,具体实现代码如下所示。

#include <stdio.h>
int main()
{
    int j,i[5];                                 //循环变量,数组i用来表示4个运算符
    int sign;                                   //累加运算时的符号
    int result;                                 //保存运算式的结果值
    int count=0;                                //计数器,统计符合条件的方案
    int num[6];                                 //保存操作数
    float left,right;                           //保存中间结果
    char oper[5]={' ','+','-','*','/'};        //运算符
    printf("输入5个数,之间用空格隔开:");
    for(j=1;j<=5;j++)
        scanf("%d",&num[j]);
    printf("输入结果:");
    scanf("%d",&result);
    for(i[1]=1;i[1]<=4;i[1]++)              //循环4种运算符,1表示+,2表示-,3表示*,4表示/
    {
        if((i[1]<4) || (num[2]!=0))         //运算符若是/,则第二个运算数不能为0
        {
          for(i[2]=1;i[2]<=4;i[2]++)
          {
                if((i[2]<4) || (num[3]!=0))
                {
                    for(i[3]=1;i[3]<=4;i[3]++)
                    {
                        if((i[3]<4) || num[4]!=0)
                        {
                            for(i[4]=1;i[4]<=4;i[4]++)
                            {
                                if((i[4]<4) || (num[5]!=0))
                                {
                                    left=0;
                                    right=num[1];
                                    sign=1;
                //使用case语句,将4种运算符填到对应的空格位置,并进行运算
                for(j=1;j<=4;j++)
                                    {
                                        switch(oper[i[j]])
                                        {
                                            case '+':
                                                left=left+sign*right;
                                                sign=1;
                                                right=num[j+1];
                                                break;
                                            case '-':
                                                left=left+sign*right;
                                                sign=-1;
                                                right=num[j+1];
                                                break;//通过f=-1实现减法
                                            case '*':
                                                right=right*num[j+1];
                                                break;//实现乘法
                                            case '/':
                                                right=right/num[j+1];//实现除法
                                                break;
                                        }
                                    }
                  //开始判断,如果运算式的结果和输入的结果相同,则表示找到一种算法,并输出这个解
                  if(left+sign*right==result)
                                    {
                                        count++;
                                        printf("%3d:",count);
                                        for(j=1;j<=4;j++)
                                            printf("%d%c",num[j],oper[i[j]]);
                                        printf("%d=%d\n",num[5],result);
                                    }
                                }
                            }
                        }
                    }
                }
          }
        }
    }
    if(count==0)
        printf("没有符合要求的方法!\n");
    getch();
    return 0;
          }

在上述代码中,定义了left和right两个变量,left用于保存上一步的运算结果,right用于保存下一步的运算结果。因为“×”和“÷”的优先级高于“+”和“-”,所以计算时先计算“×”和“÷”,再计算“+”和“-”。执行后的效果如图2-3所示。

图2-3 执行后的效果

高手真经——什么时候选择使用枚举法

也许很多读者觉得枚举(enumeration)有点“笨”,很多人称其为“暴力算法(brute force enumeration)”;但是枚举却又总是我们面对算法问题时最先被想起的。

在任何情况下,都需要选准最合适的对象,无论是枚举还是其他算法思想,这是最关键的。选准(枚举)对象的根本原因在于优化,具体表现为减少求解步骤,缩小求解的解空间,或者是使程序更具有可读性和易于编写。有时候选好了枚举对象,确定了枚举思想解决问题,问题就迎刃而解了。有的时候,当题目逼着你用枚举思想解题时,需要考虑的往往是从众多枚举对象中选择最适合的,这需要辨别的智慧。

运用枚举思想思考时需要面对的问题如表2-1所示。

表2-1 运用枚举法时需要面对的问题

用枚举法解题的最大缺点是运算量比较大,解题效率不高。如果枚举范围太大(一般以不超过200万次为限),则由于效率低的问题而会在时间上难以承受。但枚举算法的思路简单,无论是程序编写还是调试都很方便。所以如果题目的规模不是很大,在规定的时间与空间限制内能够求出解,那么最好是采用枚举法,而无须太在意是否还有更快的算法,这样可以使你有更多的时间去解答其他难题。