枚举法(穷举法)

学习算法的一些笔记。

枚举法(穷举法)思想

也称穷举法,是用于穷尽每一种可能的情况,效率不高,适用于没有明显规律的情况。

填数游戏

比如有下面这样一个数学题:

    算法描述题
    x      算
    ----------
    题题题题题题

要求分别计算出各汉字的可能性。

分析:其中一共有5个不同的字“算法描述题”,需要分别定义5个整形变量,取值在[0, 9],要使算式有意义,结果就不能为0。然后求出被乘数(算法描述题)、乘数(算)、结果(题题题题题题),如果能使算式成立就输出结果。

/**
   * 填数游戏:算法描述题,如下:
   * <pre>
   * 算法描述题
   * x     算
   * ----------
   * 题题题题题题
   * </pre>
   * 要使算式有意义,“算”和“题”就不能为0。
   */
  public static void 算法描述题() {
    int i1, i2, i3, i4, i5;//“算法描述题”
    long multi, result;//分别保存被乘数和结果
    for (i1 = 1; i1 <= 9; i1++) {// 算
      for (i2 = 0; i2 <= 9; i2++) {//法
        for (i3 = 0; i3 <= 9; i3++) {//描
          for (i4 = 0; i4 <= 9; i4++) {//述
            for (i5 = 1; i5 <= 9; i5++) {//题
              multi = i1 * 10000 + i2 * 1000 + i3 * 100 + i4 * 10 + i5;
              result = i5 * 100000 + i5 * 10000 + i5 * 1000 + i5 * 100 + i5 * 10 + i5;
              if (multi * i1 == result) {
                System.out.println(String.format("%s x %s = %s", multi, i1, result));
              }
            }
          }
        }
      }
    }
  }

最终结果:79365 x 7 = 555555。

填运算符

给定几个操作数和结果值,利用程序填写运算符,使等式成立。
这里先写了5个操作数的情况,直接上代码,然后慢慢分析。

/**
   * 根据输入的 5 个运算数和 1 个结果值,程序自动添加运算符号使等式成立。<br/>
   * 如下:a、b、c、d、e、f代表用户输入的几个数字,程序在a、b、c、d、e中加入 + - x / 符号使等式成立。
   * <pre>
   * a b c d e = f
   * </pre>
   * <pre>
   * 分析:
   * 1、要使算式有意义,除数就不能为0。
   * 2、乘除法的优先级高于加减法,可使用变量保存乘除法左右两边的结果,最后再做加减法。
   * </pre>
   *
   * @param numbers 运算数
   * @param result  结果值
   */
  public static boolean 填数游戏(int[] numbers, int result) {
    boolean successFlag = false;
    System.out.println(String.format("%s %s %s %s %s = %s", numbers[0], numbers[1], numbers[2], numbers[3], numbers[4], result));
    int count = 0;//统计有多少符合条件的方案
    float left = 0, right = 0;//记录运算符左侧和右侧的运算结果
    int sign;//累加运算时的符号
    char[] operator = {'+', '-', 'x', '/'};// 运算符集合
    int[] i = new int[4];//表示算式中的4个位置的运算符,0表示+,1表示-,2表示x,3表示/
    int operatorSize = operator.length;// 算式中运算符的个数
    for (i[0] = 0; i[0] < operatorSize; i[0]++) {//第 1 个位置的运算符
      if (i[0] < 3 || numbers[1] != 0) {// 如果运算符是/,则后面的运算数不能是0
        for (i[1] = 0; i[1] < operatorSize; i[1]++) {//第 2 个位置的运算符
          if (i[1] < 3 || numbers[2] != 0) {
            for (i[2] = 0; i[2] < operatorSize; i[2]++) {//第 3 个位置的运算符
              if (i[2] < 3 || numbers[3] != 0) {
                for (i[3] = 0; i[3] < operatorSize; i[3]++) {//第 4 个位置的运算符
                  if (i[3] < 3 || numbers[4] != 0) {
                    left = 0;
                    right = numbers[0];
                    sign = 1;

                    //依次判断每位符号
                    for (int j = 0; j < operatorSize; j++) {
                      switch (operator[i[j]]) {
                        case '+':
                          left = left + sign * right;
                          sign = 1;
                          right = numbers[j + 1];
                          break;
                        case '-':
                          left = left + sign * right;
                          sign = -1;
                          right = numbers[j + 1];
                          break;
                        case 'x':
                          right = right * numbers[j + 1];
                          break;
                        case '/':
                          right = right / numbers[j + 1];
                          break;
                      }
                    }
                    if (left + sign * right == result) {
                      count++;
                      String str = String.format("%s: %s %s %s %s %s %s %s %s %s = %s", count, numbers[0], operator[i[0]], numbers[1], operator[i[1]], numbers[2], operator[i[2]], numbers[3], operator[i[3]], numbers[4], result);
                      System.out.println(str);
                      successFlag = true;
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
    return successFlag;
  }

分析上面的代码,可以发现有这样几个步骤:

  • 循环运算符
    • 循环第一位运算符
      • 判断第一位运算符不为除法或者第二个运算符不为0
    • 循环第二位运算符
      • 判断第二位运算符不为除法或者第三个运算符不为0
    • 循环第三位运算符
      • 判断第三位运算符不为除法或者第四个运算符不为0
    • 循环第四位运算符
      • 判断第四位运算符不为除法或者第五个运算符不为0
  • 依次取出每位符号
    • 通过 switch 匹配运算符
      • 计算临时结果
  • 计算最终结果

因为这里只计算5个数,所以只添加 4 个运算符,每个位置上的运算符又有 4 种情况,需要要循环每个位置的 4 种情况。这里约定使用一个数组 i 来表示每个位的运算符,数组有索引,也有结果值。索引用于表示第几位运算符,结果值用来表示是什么运算符。约定 0 表示加法,1 表示减法,2 表示乘法,3 表示除法。那么可以这样表示每位运算符的不同情况:

  • i[0] 的值为0、1、2、3分别表示第一位的运算符的加、减、乘、除。
  • i[1] 的值为0、1、2、3分别表示第二位的运算符的加、减、乘、除。
  • i[2] 的值为0、1、2、3分别表示第三位的运算符的加、减、乘、除。
  • i[3] 的值为0、1、2、3分别表示第四位的运算符的加、减、乘、除。

所以就有了这样几个 for 循环:每层循环表示第几位的运算符,每次循环得出来的值为运算符的值。

for (i[0] = 0; i[0] < operatorSize; i[0]++) {
  for (i[1] = 0; i[1] < operatorSize; i[1]++) {
    for (i[2] = 0; i[2] < operatorSize; i[2]++) {
      for (i[3] = 0; i[3] < operatorSize; i[3]++) {

每个 for 循环的里面还有一个 if 判断是干什么的?
因为要使等式有意义,除数不能为0,这个 if 判断就是这个目的。
很直观的想法是这样的:如果运算符是除法 && 后面一个操作数是0,则结束当前循环,否则继续执行。
我们只关心继续执行的情况,所以取其反面:如果运算符不是除法,或者后面一个操作数不是0,继续执行,其他情况不做考虑。

再详细的说明判断逻辑,其中包含两种情况:

  • 如果运算符不是除法,对于加减乘,运算数可以任何数字,包括0。
  • 如果运算符后面的运算数不是 0,对于加减乘除来讲,算式都有意义。

所以在每个 for 循环后跟了一个这样的 if 判断

if (i[0] < 3 || numbers[1] != 0) {
if (i[1] < 3 || numbers[2] != 0) {
if (i[2] < 3 || numbers[3] != 0) {
if (i[3] < 3 || numbers[4] != 0) {

对等式进行有意义判断之后,就开始计算了。依次循环每位运算符,然后做比较。
此前定义了一个操作符集合char[] operator = {'+', '-', 'x', '/'};

//依次判断每位符号
for (int j = 0; j < operatorSize; j++) {
  switch (operator[i[j]]) {
    case '+':
      left = left + sign * right;
      sign = 1;
      right = numbers[j + 1];
      break;
    case '-':
      left = left + sign * right;
      sign = -1;
      right = numbers[j + 1];
      break;
    case 'x':
      right = right * numbers[j + 1];
      break;
    case '/':
      right = right / numbers[j + 1];
      break;
  }
}

switch (operator[i[j]])正如之前所讲,利用 i[] 的值表示某个运算符,下标(这里是 j)表示第几位运算符。
计算过程:定义两个变量 left、right,用于保存临时结果。left 保存运算符左侧的结果,right 表示运算符右侧的结果。变量 sign 表示下次计算的符号为正还是负,即加减法的实现。下面以3 / 2 x 6 - 8 + 1 = 2为例来进行分析。

  • 初始化:left = 0,right = 3,sign = 1。
  • 除法:left = 0,right = right / 2 = 1.5。
  • 乘法:left = 0,right = right * 6 = 9。
  • 减法:left = left + sign * right = 9,right = 8,sign = -1。
  • 加法:left = left + sign * right = 9 + (-1 * 8) = 1,right = 1,sign = 1。
  • 最终结果:left + sign * right = 1 + 1 = 2。

总结运算过程:

  • 加减法
    • left = left + sign * right。
    • 加法时 sign 要变为1,减法时 sign 要变为-1。
    • 加减法的运算是滞后的,是在下一步运算时才计算的,比如上面的减法,是在加法那一步才计算的,所以需要用 sign 来记录下一步运算的符号。用 right 来记录下一步运算的运算数。
    • left 用于记录运算符左侧的数:因为加减法的时候是不立即运算的,所以先将加减法左侧的数记录下来。
    • right 的值变为下一个运算数。
  • 乘除法
    • 乘法:right = right * numbers[j + 1]。
    • 除法:right = right / numbers[j + 1]。
    • 乘除法直接进行运算,left 的值不变。

我写了一个测试函数:

private static void 测试填数游戏() {
    new Thread(new Runnable() {
      @Override
      public void run() {
        boolean flag = false;
        Random random = new Random();
        int[] arr = new int[5];
        while (!flag) {
          for (int i = 0; i < arr.length; i++) {
            arr[i] = random.nextInt(10);
          }
          flag = 填数游戏(arr, random.nextInt(50));
          try {
            Thread.sleep(100);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    }).start();
  }

我想了一下,有这样几个问题,需要我们去一一验证或者进行修改。
运算符不重复的情况怎么写?
当操作数不是固定的 5 个数的情况下该如何写?
这里只操作的整形数,那浮点型数是否也可如此计算?
负数的情况又是如何?
如果有括号该如何编码?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值