快算24 | 回溯

快算24

成绩 10 开启时间 2020年04月7日 星期二 08:55
折扣 0.8 折扣时间 2020年05月1日 星期五 23:55
允许迟交 关闭时间 2020年05月1日 星期五 23:55

         一副牌,除了大小王之外还有52张,从1到13每个数目各有四张。要求设计一个程序,对于任意给出52张牌中的四张,运用+-×÷四种运算来判断能否每个数只能用一次,但不能不用,算出24来。注意,给出的4个数是无序的,可以添加括号。

  测试输入 期待的输出 时间限制 内存限制 额外进程
测试用例 1  
  1. 1 1 1 1↵
  2. 2 3 4 1↵
  3. 7 2 1 1↵
 
  1. no↵
  2. yes↵
  3. yes↵
1秒 64M 0

1、问题分析

首先大致列举 4 位数字组合运算的所有可能情况,大致可以将导致结果不同的因素有:

  1. 四位数字的排列顺序:如 2、3 、4、1 与  2、4、3、1
  2. 四位数字的运算顺序(括号如何加):如(2 + 3) - (4 + 1) 与 (2 + 3) - 4 + 1
  3. 四位数字之间的运算(3个运算符):如 (2 + 3) - (4 + 1) 与 (2 + 3) - (4 - 1) 

粗略地算一下大概有 A{_{4}^{4}} * 5 * 3^3 = 3888 种,所以枚举一定不会超时。但是类别比较多,这里主要是需要理清楚如何将所有情况都搜索到,主要考虑回溯法,剪枝不剪枝都没关系了~


2、思路

       先看一个小细节,通过观察发现,其实第一个因素(不同的排列顺序)不同(2、3因素相同时)或许不会有影响,如:(2 + 3) - (4 + 1) 与 (3 + 2) - (1 + 4)。其实就是:

加、乘与前后顺序无关,减、除与前后顺序有关。 


      分别考虑三种因素会使得回溯变得很复杂,而且上面也说明了1、2因素单独列举时会产生重复部分,故这里考虑将1、2因素合并考虑,第3个因素单独考虑。那么,算法的大概思路如下:

  1. 从四个数中选取两个数进行运算,运算结果为x,此时还剩下三个数待运算(x与未参与运算的两个数)
  2. 从三个数中选取两个数进行运算,运算结果为x,此时还剩下两个数待运算(x与未参与运算的一个数)
  3. 选取剩下的两个数进行运算,得到最终的结果y
  4. 若y=24,即这样子选取的结果是可以的,输出yes。否则重新选取(若全部都选取完都没有找到结果为24,就要输出no)

1、2因素合并考虑的部分:三次选取的过程就是对于整个1、2因素的合并,包含了所有的1、2因素不同而导致的情况。

3因素考虑的部分:对应于每次对于选取出的两个数字(无序)进行运算的过程。但是注意,这里不应该只枚举 4 种运算,而是 6 种!因为对于两个数无序运算的所有结果,减法、除法应该枚举两次!


3、算法实现

       这里的实现巧妙地使用了两个循环的嵌套 与 flag数组标记 来模拟选取过程,做到了不重不漏。比较难理解的就是:循环变量 i、j分别代表寻找的两个数,一定是未被标记的(也就是未被计算过的,否则会跳过该位的选取)。注意:这里每次选出的两个数字的运算结果,又储存回 i 的 位置,继续进行运算。而只有 j 位置会被标记。也就是取出两个,保存结果。

        进入循环体后再对于两个数枚举 6 种运算。

     (其实回溯算法的每一层step的值就是对应本文 2 中列举的算法思路的步数。)

此题有点难理解,按照我的梳理好好消化一下代码应该能很快想明白~下面贴出 AC 代码:

//
// Created by A on 2020/4/29.
//
#include <cstdio>
#include <cmath>
#include <cstring>

#define DIF 0.0000001

bool flag[5] = {false};   //标记该位是否被计算过
double num[5] = {0};   //每一位的值

bool Calc24(int step) {
    if (step == 4) {
        for (int i = 0; i < 4; i++)  //结果可能在1、2、3、4位中(取决于运算的顺序)
            if (!flag[i] && fabs(num[i] - 24) <= DIF)  //剩余位上(结果)的值几乎为24
                return true;
        return false;
    }

    for (int i = 1; i <= 4; i++)
        if (!flag[i])
            for (int j = i + 1; j <= 4; j++) {
                if (!flag[j]) {
                    double temp1 = num[i], temp2 = num[j];  //分别取出待运算的两位
                    flag[j] = true;   //由于计算结果储存在第 i 位可以继续参与运算,而第 j 位不再继续

                    /* 分别搜索:两种数的 6 种运算 */
                    num[i] = temp1 + temp2;
                    if (Calc24(step + 1))
                        return true;

                    num[i] = temp1 - temp2;
                    if (Calc24(step + 1))
                        return true;

                    num[i] = temp1 * temp2;
                    if (Calc24(step + 1))
                        return true;

                    if (temp2 != 0) {   //排除分母为零的情况
                        num[i] = temp1 / temp2;
                        if (Calc24(step + 1))
                            return true;
                    }

                    num[i] = temp2 - temp1;
                    if (Calc24(step + 1))
                        return true;

                    if (temp1 != 0) {   //排除分母为零的情况
                        num[i] = temp2 / temp1;
                        if (Calc24(step + 1))
                            return true;
                    }

                    /* 还原,便于回溯 */
                    num[i] = temp1;
                    flag[j] = false;
                }
            }
    return false;  //一直没有出口
}

int main() {
    while (EOF != scanf("%lf %lf %lf %lf", &num[1], &num[2], &num[3], &num[4])) {
        memset(flag, false, 5 * sizeof(bool));   //一定注意初始化
        if (Calc24(1))
            printf("yes\n");
        else
            printf("no\n");
    }
}

4、易错点总结

  1.  flag数组每次循环内一定要初始化!不然会保留上次的结果导致本次的错误

  2. 记得在列举两种除法运算时跳过除数为 0  的情况!否则会re

  3. 在step = 4 即判定结果的时候,一定是对结果位判断是否为24(此时只有结果位的flag为false),否则可能中间结果出现了24也会被判断成yes。不明白这一点的,可以试试这个用例:3 4 2 3,正确结果为no。我当时就是因为这一点wa了最后一个用例

  4. num数组应该用double储存,因为中间会储存中间值,会出现小数

  5. 对于结果是否等于24的判定是浮点数的判定,因为浮点数的运算是无法完全精准的,要排除一些误差。故不要直接与24判等,而是与24的差值较小即可。



如有帮助,不妨点赞~

猜你喜欢

转载自blog.csdn.net/weixin_43787043/article/details/105847797
今日推荐