【问题标题】:Simplification Algorithm for Reverse Polish Notation逆波兰表示法的简化算法
【发布时间】:2013-11-22 19:55:55
【问题描述】:

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

34+

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

99*76*+

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

9993+*3+*

也可以简单地将两个两位数相乘,得到 999,例如

39*66*1+*

我考虑了一会儿,然后决定编写一个程序,根据这些规则以反向波兰表示法对任何给定整数输出最小的表达式。这就是我目前所拥有的(用 NodeJS 和 underscorejs 编写的):

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+* 这样的表达式是最小的吗?

希望您能提供一些见解。提前致谢。

【问题讨论】:

    标签: algorithm algebra simplification postfix-notation


    【解决方案1】:

    还有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
    

    是直接生成postfix还是生成infix并转换更容易,我真的不知道。我可以看到每种方法的优点和缺点。

    无论如何,这就是我要采取的方法:首先尝试均分,然后贪婪地除以 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+**,我认为这是最佳的。

    这会为 (9x + y) 形式的表达式表示,其中 x 和 @987654330 @ 是一位数字。但是,我不知道这是否能保证大于 90 的值的最佳表达式。

    【讨论】:

      【解决方案2】:

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

      num1num2 会产生重叠的子问题,因此可以使用动态规划。

      我们可以简单地,为一个号码i

      1. 对于每个均分i1 &lt; j &lt;= sqrt(i),尝试[j][i / j]*
      2. 对于每个0 &lt; j &lt; i/2,请尝试[j][i - j]+
      3. 采用最好的公式

      这当然很容易实现自下而上,只需从 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 个总和,并且使跳过成为可能的检查非常快。这个技巧足够好,在我的 PC 上生成高达一百万的所有数字只需要 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),但这在我的测试中没有显着差异。应该可以找到更好的上限。

      【讨论】:

      • 所以你必须为直到N的所有数字开始这段代码,并且每次你尝试所有的乘法和加法。仅对N 来说,这听起来不是很有效。你听说过递归吗?
      • @Hynek-Pichi-Vychodil 是的,我明确地摆脱了它。这就是DP的全部想法。递归如何更好?它最终仍然要求所有这些较低的数字。
      • 是的,当可以保证结果的大小将比目前最好的要大时,您会一次又一次地要求较低的 nubers,但不会一次又一次地要求所有的 nubers。
      • @Hynek-Pichi-Vychodil 我仍然看不出有什么更好的地方。这只是 O(n^2) 并给出了所有 i &lt;= n 的结果。
      • @Hynek-Pichi-Vychodil 我想了一个优化总和搜索的新技巧(在我的回答中解释)。它可能可以进一步改进。有了这个技巧,100k 的时间缩短到 110 毫秒(!!)。
      【解决方案3】:

      长度为 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。原版的 999 需要 14 秒,而这款则需要 190 毫秒左右。

      编辑2

      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 的一半)无限制(注意255 作为best_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* 等算法进行空间搜索。对于非常大的数字,使用遗传算法可能是个好主意。

      编辑3

      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
      

      【讨论】:

      • 不错的答案...可惜我对 C 很烂。
      • 它可以用任何语言编写。你可以做什么(在 Erlang 中只有不可变的结构)和速度会有所不同。
      【解决方案4】:

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

      如果您需要数字 123,那会更好 "{"99*76*+

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-12-24
        • 2015-06-15
        • 1970-01-01
        • 2021-11-29
        • 2020-01-15
        • 1970-01-01
        • 2014-01-13
        相关资源
        最近更新 更多