快算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↵
- 2 3 4 1↵
- 7 2 1 1↵
- no↵
- yes↵
- yes↵
1秒 64M 0
1、问题分析
首先大致列举 4 位数字组合运算的所有可能情况,大致可以将导致结果不同的因素有:
- 四位数字的排列顺序:如 2、3 、4、1 与 2、4、3、1
- 四位数字的运算顺序(括号如何加):如(2 + 3) - (4 + 1) 与 (2 + 3) - 4 + 1
- 四位数字之间的运算(3个运算符):如 (2 + 3) - (4 + 1) 与 (2 + 3) - (4 - 1)
粗略地算一下大概有 种,所以枚举一定不会超时。但是类别比较多,这里主要是需要理清楚如何将所有情况都搜索到,主要考虑回溯法,剪枝不剪枝都没关系了~
2、思路
先看一个小细节,通过观察发现,其实第一个因素(不同的排列顺序)不同(2、3因素相同时)或许不会有影响,如:(2 + 3) - (4 + 1) 与 (3 + 2) - (1 + 4)。其实就是:
加、乘与前后顺序无关,减、除与前后顺序有关。
分别考虑三种因素会使得回溯变得很复杂,而且上面也说明了1、2因素单独列举时会产生重复部分,故这里考虑将1、2因素合并考虑,第3个因素单独考虑。那么,算法的大概思路如下:
- 从四个数中选取两个数进行运算,运算结果为x,此时还剩下三个数待运算(x与未参与运算的两个数)
- 从三个数中选取两个数进行运算,运算结果为x,此时还剩下两个数待运算(x与未参与运算的一个数)
- 选取剩下的两个数进行运算,得到最终的结果y
- 若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、易错点总结
-
flag数组每次循环内一定要初始化!不然会保留上次的结果导致本次的错误
-
记得在列举两种除法运算时跳过除数为 0 的情况!否则会re
-
在step = 4 即判定结果的时候,一定是对结果位判断是否为24(此时只有结果位的flag为false),否则可能中间结果出现了24也会被判断成yes。不明白这一点的,可以试试这个用例:3 4 2 3,正确结果为no。我当时就是因为这一点wa了最后一个用例
-
num数组应该用double储存,因为中间会储存中间值,会出现小数
-
对于结果是否等于24的判定是浮点数的判定,因为浮点数的运算是无法完全精准的,要排除一些误差。故不要直接与24判等,而是与24的差值较小即可。
如有帮助,不妨点赞~