如何逆向获得计算24的公式

转载请保留作者和原始连接http://www.jianshu.com/p/7768195814cf
2018-06-21更新
------------start------------
添加了Swift和Dart语言的实现。
https://github.com/aesean/TwentyFour/blob/master/Calculate.swift
https://github.com/aesean/TwentyFour/blob/master/calculate.dart
------------end------------
2018-06-19更新
------------start------------
评论说希望能支持一些特殊规则计算,代码已经更新了。新代码是用Kotlin写的,如果需要Java版本,请尝试用IDE自带的插件自行转换Java版。apk的UI没有开放支持任意数量的数字计算,只是开放了支持计算任意结果。需要注意,有个非常讨厌的情况是除法,如果不做四舍五入,1/3*3得到的就不是1了。实际代码里做了约等,极端情况下可能会有计算错误。如果你希望能完美精确计算,请自行扩展MathRuleImpl,
实际代码已经支持计算任意数量的数字,以及任何你希望的结果。详细请参考下面的源码:
https://github.com/aesean/TwentyFour/blob/master/app/src/main/java/com/aesean/twentyfour/CalculateImpl.kt
代码里的main方法示例了5,5,5,1四个数字计算结果的绝对值小于10的计算方式。其他规则请自行尝试,另外由于Tree的遍历用到了递归,所以如果数据个数太多有可能会出现爆栈情况,请自行优化。
------------end------------

也可以下载apk,看下实际效果。
https://github.com/aesean/TwentyFour/raw/master/app-release.apk

黑夜给了我黑色的眼睛,我却用它来敲代码,~笑~。
晚上一个朋友群里,有人问:5,5,5,1如何计算24。当然计算方式随手Google一下就有,但这一下子让我觉得通过某种算法肯定可以逆向推出这个公式。其实很早就有这个想法,就是一直没思路,也懒就一直没弄,这次一下心血来潮就做了下实现。
用Java写的,源码可以参考这里:
https://github.com/aesean/TwentyFour/blob/master/app/src/main/java/com/aesean/twentyfour/TwentyFour.java

这里介绍下思路。通常第一眼看到这个问题,会非常头疼,逆向算24的计算公式,这怎么搞。
我们先看下前面:5,5,5,1的计算结果:(5-(1/5))*5=24。这里仔细看下,这个结果有这样一种规律,我们先假定加减乘除的运算用'?'代替,无论最终的计算公式是什么,结果一定是a?b=24这种形式。然后a和b可能是四个原始数之一,或者也是a?b这种形式由两个数计算出来的。那我们先把a?b这种形式计算结果写个方法出来。

private static float[] math(float a, float b) {
    float[] result = new float[6];
    result[0] = a + b;
    result[1] = a - b;
    result[2] = a × b;
    result[3] = a / b;
    return result;
}

这个方法就是两个数运算后所有可能的结果。这里注意,参数选float是因为可能会有上面5-1/5这种情况,int型做除法运算会丢失精度,所以这里强转为float型,避免除法出错。这里返回的浮点型数组就是a与b做加减乘除后的结果。
好了到了这里其实我们这个方法配合排列组合,已经可以计算任意两个数通过加减乘除如何等于24了。直接遍历排列,然后调用math返回的结果判断是否等于24就可以了。

现在我们需要把两个数的情况,扩展到四个数。我们再看(5-(1/5))*5=24这个结果,这个结果其实可以这么理解,我们可以把最终的结果看成是a?b?c?d=24,abcd分别第一,第二,第三,第四个数。第一个数与第二个数math,然后结果与第三个数math,得出的结果与第四个数math。然后将abcd四个数,排列出所有可能的排列,然后就能计算这四个数在这种形式((a?b)?c)?d下的所有可能的结果了。
排列我们写个方法来实现,这里不贴源码了,我写的排列写的比较懒,也比较烂,只支持4个数的排列。其实这里写的好应该是支持任意数量的数的排列。

private static float[][] arrange(float[] value) {
    .....
    return result;
}

这里返回一个二维数组,包含了所有排列结果。
然后我们借助这个排列返回的结果,然后配合math方法就可以计算所有这种((a?b)?c)?d形式下的计算结果了。写成代码应该类似下面这样

public static void start(float[] mNum) {
    float[] value0 = math(mNum[0], mNum[1]);
    // 1-1-1-1
    //当三个数与一个数计算的时候
    for (int i = 0; i < value0.length; i++) {
        float[] value1 = math(value0[i], mNum[2]);
        for (int i1 = 0; i1 < value1.length; i1++) {
            float[] value2 = math(value1[i1], mNum[3]);
            for (int i2 = 0; i2 < value2.length; i2++) {
                if (value2[i2] == 24f) {
                     //成功计算出24
                }
        }
    }
}

先前两个数做math运算,然后跟第三个数做math,然后结果和第四个数做math,那这种形式先不说能不能覆盖全部可能的情况,先看看能不能覆盖(5-(1/5))*5=24,可以实际跑下代码,发现并不能。但这里打印结果发现可以计算出-24,问题在哪儿?问题出5-(1/5)上,这里并不是前两个数的结果与第三个运算,这里是第一个数与第二三个数的结果做运算。
这里头疼了,按照现在的math方法,我们需要把所有可能的abcd运算形式写出来,可能要写:(a?(b?c))?d,a?((b?c)?d),a?(d?(b?c)),等等好多情况。比较笨的情况就是将所有结果全部列举出来,一共四个数结果也不是非常多。
但是有个更加优雅的解决方法。我们再看下计算结果,其中包含24,这表示什么,这表示把x-y,换成y-x就可以了。也就是说对于减法运算需要处理换位的情况,其实不光是减法,除法也存在换位问题,我们现在计算方式应该还会计算出1/24这种情况。再看
(a?(b?c))?d,
a?((b?c)?d),
a?(d?(b?c))
这些形势,其实都是换位。(a?(b?c))?d和a?(d?(b?c))的区别其实就是一个换位,而且如果这两个计算里(a?(b?c))?d的第三个问号和a?(d?(b?c))的第一个问号是减法或者除法的时候才会出现结果不一致的情况。
好了,原因明白,那怎么处理呢?非常简单,改造下math方法就行了。

private static float[] math(float a, float b) {
    float[] result = new float[6];
    result[0] = a + b;
    result[1] = a - b;
    result[2] = a × b;
    result[3] = a / b;
    result[4] = b / a;
    result[5] = b - a;
    return result;
}

我们在math方法里把可能的换位情况加进去。
这时候看下能不能覆盖到(5-(1/5))5=24,我们一定覆盖到(1/5-5)5这种情况,这个很容易明白,但因为我们加了换位,所以第二个减号a-b可以被换位为b-a,变成这样(5-(1/5))5,实际跑下,看下效果。
成功计算出24。这时候其实已经可以覆盖到很多种情况下的计算24了。还漏了一种情况。就是((a?b)?c)?d,再加上换位,无法解决这种情况(100-96)
(10-6),这种情况抽象下就是:(a?b)?(c?d)=24,我们前面的运算可以解决所有的最后一步是和abcd四个数之一做运算的情况,但还有一种是最后是两个运算结果之间的运算。所以start方法还应该加入(a?b)?(c?d)这种情况。

//当两个数与两个数计算的时候
    float[] rightTwo = math(mNum[2], mNum[3]);
    for (int i = 0; i < value0.length; i++) {
        for (int i1 = 0; i1 < rightTwo.length; i1++) {
            float[] result = math(value0[i], rightTwo[i1]);
            for (int i2 = 0; i2 < result.length; i2++) {
                if (result[i2] == 24f) {
                     // 成功计算出24
                }
            }
        }
    }

加上后,我们再计算下,发现成功解决(100-96)*(10-6)=24了。那我们再想想会不会还有没有覆盖到情况。我们逆推下,最终无论怎么算24都是两个数的?运算,然后这两个数要么一个来计算得出,一个来自abcd,要么两个都来自计算得出。不存在第三种情况。然后针对第一种里,那个计算得出的数,一定是两个数计算然后和剩下一个数做?运算,不存在其他情况。而第二种的,更简单了,两个数?运算,剩下两个数?运算,结果再?运算也不存在其他情况。好了,现在应该是所有情况都覆盖到了。
那还有个问题,我们代码可以跑到 // 成功计算出24这一行,那怎么格式化输出实际的公式呢?也很简单,math方法返回的数组是所有的结果,而结果在数组里的位置代表了计算时候所使用的运算形式。我们可以写这样一个方法把index转化为运算符号。

private static String getSymbol(int index) {
    switch (index) {
        case 0:
            return " + ";
        case 1:
            return " - ";
        case 2:
            return " * ";
        case 3:
            return " / ";
        case 4:
            return " / ";
        case 5:
            return " - ";
        default:
            throw new RuntimeException("未知的index类型");
    }
}

然后我们通过start方法里每一层的for循环的i参数就可以拿到计算最终结果的时候所使用的符号了。
代码类似这样:

String p0, p1, p2;if (i < 4) {
    p0 = LEFT_BRACKETS + (int) mNum[0] + getSymbol(i) + (int) mNum[1] + RIGHT_BRACKETS;
} else {
    p0 = LEFT_BRACKETS + (int) mNum[1] + getSymbol(i) + (int) mNum[0] + RIGHT_BRACKETS;
}
if (i1 < 4) {
    p1 = LEFT_BRACKETS + p0 + getSymbol(i1) + (int) mNum[2] + RIGHT_BRACKETS;
} else {
    p1 = LEFT_BRACKETS + (int) mNum[2] + getSymbol(i1) + p0 + RIGHT_BRACKETS;
}
if (i2 < 4) {
    p2 = p1 + getSymbol(i2) + (int) mNum[3];
} else {
    p2 = (int) mNum[3] + getSymbol(i2) + p1;
}

这里的4写法很不好,懒的改了,这里的4就是math方法里的大于4的运算都是需要显示换位的,所以这里需要根据是否大于4决定显示输出是否换位。
到了这里,大致过程就结束了。
如果把前面的思路敲进Android,差不多就是这个样子。

Paste_Image.png
Paste_Image.png
Paste_Image.png

还有整个计算会有重复计算的情况,所以会有结果除重,还有结果格式化输出注意换位等细节就不详细说了,具体可以直接参考源码。

转载请保留作者和原始连接http://www.jianshu.com/p/7768195814cf
也可以下载apk,https://github.com/aesean/TwentyFour/raw/master/app-release.apk

你可能感兴趣的:(如何逆向获得计算24的公式)