反向波兰表示法的简化算法

时间:2013-11-22 19:55:55

标签: algorithm algebra simplification postfix-notation

几天前,我玩Befunge,这是一种深奥的编程语言。 Befunge使用LIFO堆栈来存储数据。当您编写程序时,0到9之间的数字实际上是Befunge指令,它们将相应的值压入堆栈。因此,对于exmaple,这会将7推到堆栈:

34+

为了推送一个大于9的数字,必须使用小于或等于9的数字进行计算。这将产生123.

99*76*+

在用Befunge解决Euler Problem 1时,我不得不将相当大的数字999推到堆栈。在这里,我开始想知道如何用尽可能少的指令完成这项任务。通过在中缀符号中写下一个术语并取出共同因素,我想出了

9993+*3+*

也可以简单地将两个两位数的数字相乘,产生999,例如

39*66*1+*

我考虑了这个问题,然后决定编写一个程序,根据这些规则在任何给定整数的反向抛光表示法中输出最小的表达式。这是我到目前为止(用NodeJS编写的下划线):

var makeExpr = function (value) {
    if (value < 10) return value + "";
    var output = "", counter = 0;
    (function fn (val) {
        counter++;
        if(val < 9) { output  += val; return; };
        var exp = Math.floor(Math.log(val) / Math.log(9));
        var div = Math.floor(val / Math.pow(9, exp));
        _( exp ).times(function () { output += "9"; });
        _(exp-1).times(function () { output += "*"; });
        if (div > 1) output += div + "*";
        fn(val - Math.pow(9, exp) * div);    
    })(value);
    _(counter-1).times(function () { output+= "+"; });
    return output.replace(/0\+/, "");
};

makeExpr(999);
// yields 999**99*3*93*++

这段代码天真地构造了表达式,并且是长篇大论。现在我的问题:

  • 是否有一种算法可以简化反向抛光表示法中的表达式?
  • 在中缀表示法中简化会更容易吗?
  • 9993+*3+*这样的表达式可以证明是最小的表达式吗?

我希望你能提供一些见解。提前谢谢。

4 个答案:

答案 0 :(得分:2)

还有93*94*1+*,基本上是27*37

如果我要解决这个问题,我首先要尝试均匀划分数字。因此,鉴于999,我将除以9得到111.然后我会尝试除以9,8,7等,直到我发现111是3 * 37。

37是素数,所以我贪婪地除以9,得到4,其余为1。

这似乎给了我最好的结果,我试过了半打。当然,它有点贵,甚至可以测试它的可分性。但也许并不比产生过长的表达更为昂贵。

使用此功能,100变为55*4*。 102适用于29*5*6+

101提出了一个有趣的案例。 101/9 =(9 * 11)+ 2.或者,(9 * 9)+20。我们来看看:

983+*2+  (9*11) + 2
99*45*+  (9*9) + 20

是否更容易直接生成后缀或生成中缀和转换,我真的不知道。我可以看到每个人的利弊。

无论如何,这就是我采取的方法:首先尝试均匀分配,然后贪婪地除以9.不确定我是如何构建的。

一旦你想出来,我肯定希望看到你的解决方案。

修改

这是一个有趣的问题。我想出了一个递归函数,它可以生成一个可靠的生成后缀表达式的工作,但它并不是最佳的。这是在C#。

string GetExpression(int val)
{
    if (val < 10)
    {
        return val.ToString();
    }
    int quo, rem;
    // first see if it's evenly divisible
    for (int i = 9; i > 1; --i)
    {
        quo = Math.DivRem(val, i, out rem);
        if (rem == 0)
        {
            // If val < 90, then only generate here if the quotient
            // is a one-digit number. Otherwise it can be expressed
            // as (9 * x) + y, where x and y are one-digit numbers.
            if (val >= 90 || (val < 90 && quo <= 9))
            {
                // value is (i * quo)
                return i + GetExpression(quo) + "*";
            }
        }
    }

    quo = Math.DivRem(val, 9, out rem);
    // value is (9 * quo) + rem
    // optimization reduces (9 * 1) to 9
    var s1 = "9" + ((quo == 1) ? string.Empty : GetExpression(quo) + "*");
    var s2 = GetExpression(rem) + "+";
    return s1 + s2;
}

对于999,它会生成9394*1+**,我认为这是最佳的。

这为值&lt; = 90生成最佳表达式。0到90之间的每个数字都可以表示为两位一位数的乘积,或表达式(9x + y)的表达式,其中{{1 }和x是一位数字。但是,我不知道这可以保证大于90的值的最佳表达。

答案 1 :(得分:2)

当只考虑乘法和加法时,构造最优公式非常容易,因为该问题具有最优的子结构属性。也就是说,构建[num1][num2]op的最佳方式来自num1num2,它们都是最佳的。如果还考虑重复,那就不再适用了。

num1num2会引起重叠的子问题,因此动态编程适用。

我们可以简单地使用i

  1. 对于均匀划分1 < j <= sqrt(i)的每个i,请尝试[j][i / j]*
  2. 对于每个0 < j < i/2,请尝试[j][i - j]+
  3. 采用最佳找到的公式
  4. 这当然很容易做到自下而上,只需从i = 0开始,然后按照自己想要的数字进行操作。不幸的是,第2步有点慢,所以说100000后开始变得烦人等待它。可能有一些我没有看到的技巧。

    C#中的代码(未经过良好测试,但似乎有效):

    string[] n = new string[10000];
    for (int i = 0; i < 10; i++)
        n[i] = "" + i;
    for (int i = 10; i < n.Length; i++)
    {
        int bestlen = int.MaxValue;
        string best = null;
        // try factors
        int sqrt = (int)Math.Sqrt(i);
        for (int j = 2; j <= sqrt; j++)
        {
            if (i % j == 0)
            {
                int len = n[j].Length + n[i / j].Length + 1;
                if (len < bestlen)
                {
                    bestlen = len;
                    best = n[j] + n[i / j] + "*";
                }
            }
        }
        // try sums
        for (int j = 1; j < i / 2; j++)
        {
            int len = n[j].Length + n[i - j].Length + 1;
            if (len < bestlen)
            {
                bestlen = len;
                best = n[j] + n[i - j] + "+";
            }
        }
        n[i] = best;
    }
    

    这是优化搜索总和的技巧。假设有一个数组,对于每个长度,包含可以使用该长度进行的最大数字。另一个可能不太明显的是,这个数组也给我们提供了一个快速的方法来确定大于某个阈值的最短数字(通过简单地扫描数组并注意超过阈值的第一个位置)。总之,它提供了一种快速丢弃搜索空间大部分的方法。

    例如,长度3的最大数量是81,长度5的最大数量是728.现在,如果我们想知道如何获得1009(素数,所以没有找到因子),首先我们尝试总和第一部分的长度为1(所以1+10089+1000),找到长度为9个字符的9+100095558***+)。

    下一步,检查第一部分长度为3或更小的总和,可以完全跳过。 1009 - 81 = 929和929(如果第一部分是3个字符或更少,则总和的第二部分的最低值)大于728,因此929及以上的数字必须至少为7个字符长。因此,如果总和的第一部分是3个字符,则第二部分必须至少为7个字符,然后在结尾处还有一个+号,因此总数至少为11个字符。到目前为止最好的是9,所以可以跳过这一步。

    下一步,第一部分中包含5个字符,也可以跳过,因为1009 - 728 = 280,要生成280或者高,我们至少需要5个字符。 5 + 5 + 1 = 11,大于9,所以不要检查。

    我们只需要以这种方式检查9,而不是检查大约500金额,并且检查以使跳过成为可能非常快。这个技巧足够好,在我的电脑上生成高达一百万的所有数字只需要3秒钟(之前,需要3秒才能达到100000)。

    以下是代码:

    string[] n = new string[100000];
    int[] biggest_number_of_length = new int[n.Length];
    for (int i = 0; i < 10; i++)
        n[i] = "" + i;
    biggest_number_of_length[1] = 9;
    for (int i = 10; i < n.Length; i++)
    {
        int bestlen = int.MaxValue;
        string best = null;
        // try factors
        int sqrt = (int)Math.Sqrt(i);
        for (int j = 2; j <= sqrt; j++)
        {
            if (i % j == 0)
            {
                int len = n[j].Length + n[i / j].Length + 1;
                if (len < bestlen)
                {
                    bestlen = len;
                    best = n[j] + n[i / j] + "*";
                }
            }
        }
        // try sums
        for (int x = 1; x < bestlen; x += 2)
        {
            int find = i - biggest_number_of_length[x];
            int min = int.MaxValue;
            // find the shortest number that is >= (i - biggest_number_of_length[x])
            for (int k = 1; k < biggest_number_of_length.Length; k += 2)
            {
                if (biggest_number_of_length[k] >= find)
                {
                    min = k;
                    break;
                }
            }
            // if that number wasn't small enough, it's not worth looking in that range
            if (min + x + 1 < bestlen)
            {
                // range [find .. i] isn't optimal
                for (int j = find; j < i; j++)
                {
                    int len = n[i - j].Length + n[j].Length + 1;
                    if (len < bestlen)
                    {
                        bestlen = len;
                        best = n[i - j] + n[j] + "+";
                    }
                }
            }
        }
        // found
        n[i] = best;
        biggest_number_of_length[bestlen] = i;
    }
    

    仍有改进的余地。此代码将重新检查已检查的总和。有一些简单的方法可以使它至少不检查两次相同的总和(通过记住最后的find),但这在我的测试中没有显着差异。应该可以找到更好的上限。

答案 2 :(得分:1)

对于长度为9的999,有44种解决方案:

39149*+**
39166*+**
39257*+**
39548*+**
39756*+**
39947*+**
39499**+*
39669**+*
39949**+*
39966**+*
93149*+**
93166*+**
93257*+**
93548*+**
93756*+**
93947*+**
93269**+*
93349**+*
93366**+*
93439**+*
93629**+*
93636**+*
93926**+*
93934**+*
93939+*+*
93948+*+*
93957+*+*
96357**+*
96537**+*
96735**+*
96769+*+*
96778+*+*
97849+*+*
97858+*+*
97867+*+*
99689+*+*
956*99*+*
968*79*+*
39*149*+*
39*166*+*
39*257*+*
39*548*+*
39*756*+*
39*947*+*

修改

我正在进行一些搜索空间修剪改进,所以抱歉我没有立即发布。 Erlnag中有script。原始的一个需要花费14秒才能获得999,但是这个花费大约需要190毫秒。

<强> EDIT2

对于9999,有1074个长度为13的解决方案。需要7分钟,其中有一些在下面:

329+9677**+**
329+9767**+**
338+9677**+**
338+9767**+**
347+9677**+**
347+9767**+**
356+9677**+**
356+9767**+**
3147789+***+*
31489+77***+*
3174789+***+*
3177489+***+*
3177488*+**+*

C中有version,可以更积极地修剪状态空间,只返回一个解决方案。它更快。

$ time ./polish_numbers 999
Result for 999: 39149*+**, length 9

real    0m0.008s
user    0m0.004s
sys     0m0.000s

$ time ./polish_numbers 99999
Result for 99999: 9158*+1569**+**, length 15

real    0m34.289s
user    0m34.296s
sys     0m0.000s

harold报道他的C#bruteforce version在20多岁时编号相同,所以我很好奇我是否可以改进我的。我通过重构数据结构尝试了更好的内存利用率。搜索算法主要与解决方案的长度有关,并且它存在,因此我将此信息分离为一个结构(best_rec_header)。我还在另一个(best_rec_args)中分离树枝。这些数据仅在给定数量的新的更好解决方案时使用。有code

Result for 99999: 9158*+1569**+**, length 15

real    0m31.824s
user    0m31.812s
sys     0m0.012s

它仍然太慢了。所以我尝试了其他一些版本。 First我添加了一些统计数据来证明我的代码并没有计算所有较小的数字。

Result for 99999: 9158*+1569**+**, length 15, (skipped 36777, computed 26350)

然后我尝试更改code以首先为更大的数字计算+个解决方案。

Result for 99999: 1956**+9158*+**, length 15, (skipped 0, computed 34577)

real    0m17.055s
user    0m17.052s
sys     0m0.008s

几乎快了两倍。但是有一个想法可能有时候我会放弃找到某个数字的解决方案,受到当前best_len限制的限制。所以我尝试make小数字(最多一半n)无限制(注意255best_len限制,以便找到第一个操作数。)

Result for 99999: 9158*+1569**+**, length 15, (skipped 36777, computed 50000)

real    0m12.058s
user    0m12.048s
sys     0m0.008s

很好的改进,但是如果我通过目前为止发现的最佳解决方案来限制这些数字的解决方案。它需要某种计算全局状态。 Code变得更复杂,但结果更快。

Result for 99999: 97484777**+**+*, length 15, (skipped 36997, computed 33911)

real    0m10.401s
user    0m10.400s
sys     0m0.000s

它甚至可以计算十倍的数量。

Result for 999999: 37967+2599**+****, length 17, (skipped 440855)

real    12m55.085s
user    12m55.168s
sys     0m0.028s

然后我决定尝试brute force方法,这更快。

Result for 99999: 9158*+1569**+**, length 15

real    0m3.543s
user    0m3.540s
sys     0m0.000s

Result for 999999: 37949+2599**+****, length 17

real    5m51.624s
user    5m51.556s
sys     0m0.068s

这表明,那不变的事情。对于现代CPU来说尤其如此,当强力方法从更好的矢量化,更好的CPU缓存利用率和更少的分支中获益时。

无论如何,我认为有一些更好的方法可以更好地理解数论或算法空间搜索为A *等等。对于非常大的数字,使用遗传算法可能是个好主意。

<强> EDIT3

harold带来了新的想法,以消除尝试多少钱。我已在此new version中实现了它。它的速度要快一些。

$ time ./polish_numbers 99999
Result for 99999: 9158*+1569**+**, length 15

real    0m0.153s
user    0m0.152s
sys     0m0.000s
$ time ./polish_numbers 999999
Result for 999999: 37949+2599**+****, length 17

real    0m3.516s
user    0m3.512s
sys     0m0.004s
$ time ./polish_numbers 9999999
Result for 9999999: 9788995688***+***+*, length 19

real    1m39.903s
user    1m39.904s
sys     0m0.032s

答案 3 :(得分:0)

别忘了,你也可以推送ASCII值!! 通常,这会更长,但对于更高的数字,它可以变得更短:

如果你需要数字123,那就更好了 "{"99*76*+