【问题标题】:How to avoid overflow when counting anagrams?计算字谜时如何避免溢出?
【发布时间】:2020-12-06 01:47:41
【问题描述】:

令 N 为字符串的大小。设 A, B, C..., Z 为字符串中每个字母出现的次数。

我需要计算字谜的数量:N!/(A!*B!*C!...*Z!)。

最终的结果是保证适合整数,但原始字符串的长度可以是任意大小。

到目前为止,我唯一的想法是对乘积中的数字进行质因式分解,然后消除分母中也存在的分子因子。

有没有更实用的方法来实现这一点?

【问题讨论】:

  • 使用 bigint 库怎么样?

标签: algorithm anagram


【解决方案1】:

您可以通过交错乘法和除法来进行计算,而不是先进行所有分子乘法,然后再除以所有除数。交错操作显着减小了中间值的大小,但并不能完全保证中间结果不会大于最终结果。稍加努力,我们可以找到一个乘法和除法的顺序,其中除法总是精确的,没有中间结果超过最终结果。 (如果您不关心解释,请跳至此答案底部的示例代码。)

了解交错乘法和除法的工作原理很有用。为了精确除法,除法之前的中间值必须是除数的精确倍数。在这种情况下,这是真的,因为乘数是一个稳定递增的整数序列。

这是一个简单的交错示例,只有两个字母。我们要计算 (7 C 5),即aaaaabb 的字谜数。 (这也是一个二项式系数,因为它与询问长度为 7 的列表中 5 个位置的集合的数量相同。我们可以通过将as 放在所选的五个位置中,将bs 放在另外两个。)所以天真的计算是:

  1   ×2   ×3   ×4   ×5   ×6   ×7   ÷1   ÷2   ÷3   ÷4   ÷5   ÷1   ÷2
  1    2    6   24  120  720 5040 5040 2520  840  210   42   42   21

最大的中间值是 5040。这不是溢出(除非我们使用 8 位算术),但它比需要的大得多。这是交错选项:

  1   ÷1   ×2   ÷2   ×3   ÷3   ×4   ÷4   ×5   ÷5   ×6   ÷1   ×7   ÷2
  1    1    2    1    3    1    4    1    5    1    6    6   42   21

现在,最大的中间结果是 42,它甚至不会溢出 char。如果我们除以 2,我们将得到相同的结果!首先,而不是从 5 开始!:

  1   ÷1   ×2   ÷2   ×3   ÷1   ×4   ÷2   ×5   ÷3   ×6   ÷4   ×7   ÷5
  1    1    2    1    3    3   12    6   30   10   60   15  105   21

按照这个顺序,有更多的中间值超过了最终结果,但最大的仍然没有接近原始的 5040。

很明显,在上述两种情况下,划分都是准确的,但可能并不那么明显,为什么必须如此。证明(使用归纳法)并不难,但直观的解释并不是很复杂。考虑上面第二个示例中的(最终)除以 5。在这个简单的例子中,之前没有除以因子 5 的除法,而且肯定有之前乘以 5 的倍数,所以除法是精确的也就不足为奇了。

但是假设之前有一次除以 5 的倍数。如果是这样,那么该除法一定早很长时间,因为之前的四次除法是被小于 5 的数字所除。换句话说,在任何时候在我们除以p 的情况下,先前除以p 的倍数之前必须至少有p 连续整数的乘法。其中一个乘法必须是p 的倍数,因为每个p 整数都有一个p 的倍数。由于自那次乘法以来没有除以 p,因此我们可以依赖 p 仍然是累积结果的一部分,因此除以 p 是安全的。

也很容易看出,除法之后的中间结果是单调递增的。这是因为在乘法/除法序列中,乘数必须大于除数;乘数只是一个递增序列,而除数会定期重置为 1。这反过来意味着最大的中间值不能超过最大除数乘以最终结果。因此,如果我们能够以稍宽的整数类型进行中间计算,我们将能够避免溢出。这可能是一个足够好的解决方案,但可能允许最终结果是语言的最大整数类型,在这种情况下,中间计算没有更广泛的类型。我们需要更好的保证。

所以让我们回到解释为什么当我们要除以p 时,我们知道中间值可以被p 整除。关键是在最近的p 乘法中必须有p 的乘法。现在,考虑两种可能性:

  1. 最后一次乘以 p 的倍数。
  2. 最后的乘法不是p的倍数。

在情况2中,最后一次乘法之前的中间值已经有p作为因子,所以我们可以先进行除法。在情况 1 中,最后一个乘法本身是 p 的某个倍数,因此我们可以在进行乘法运算之前将乘数除以 p。很容易知道我们正在研究这两种情况中的哪一种,只需将乘数除以除数即可。通过该修改,我们保证没有中间结果大于最终结果,因此如果最终结果可表示,则不会溢出。

这是一个简单的 C 实现。可以进行多种优化,但我尽量保持简单;因为它的执行时间通常以微秒为单位:

long long count_anagrams(int n, int letters[26]) {
  long long count = 1;
  for (int mult = 1, divisor = 1, letter = 0; mult <= n; ++mult, ++divisor) {
    while (divisor > letters[letter]) {
      ++letter;
      divisor = 1;
    }
    if (mult % divisor == 0)
      count *= mult / divisor;
    else {
      count /= divisor;
      count *= mult;
    }
  }
  return count;
}

一个测试用例,针对一个使用 bignums 的简单 Python 程序进行验证:

$ ./anagrams abcdddddddddddddddddddddddddddeeeeeffffggg
There are 7467095163297369600 anagrams of abcdddddddddddddddddddddddddddeeeeeffffggg

【讨论】:

    【解决方案2】:

    我找到了一个使用 2 个数组以及每个部分的乘法项的解决方案。使用 GCD 简化术语。这是我的 C++ 代码:(地图有字符及其各自的频率)。

    unsigned int fatnk(int n, map<char, int> &k)
    {
       vector<int> numerator, denominator;
       for (int i = 2; i <= n; i++)
         numerator.push_back(i);
       for (auto it : k)
          if (it.second > 1)
             for (int i = 2; i <= it.second; i++)
                denominator.push_back(i);
       for (int i = 0; i < numerator.size(); i++)
          for (int j = 0; numerator[i] > 1 && j < denominator.size(); j++)
          {
              if (denominator[j] == 1)
                 continue;
              int d = gcd(numerator[i], denominator[j]);
              if (d == 1) 
                 continue;
              numerator[i] /= d;
              denominator[j] /= d;
          }
      unsigned int ans = 1;
      for (auto it : numerator)
         ans *= it;
      return ans;
    }
    

    【讨论】:

      【解决方案3】:

      您不需要单独分解数字。只需分解 1..n 范围内所有内容的乘积即可。这是一个O(n log(log(n))) 操作。然后你就可以取消了。

      这里是 Python:

      def factor_range(n):
          is_prime = [True for i in range(n+1)]
          factorization = {}
          
          for p in range(2, n+1):
              if is_prime[p]:
                  power = p
                  factors = 0
                  while power <= n:
                      s = power
                      while s <= n:
                          factors = factors + 1
                          is_prime[s] = 0
                          s = s + power
                      power = power * p
                  factorization[p] = factors
          return factorization
      

      (在我的笔记本电脑上,这可以在一秒钟内给出 1000000 的完全分解版本。)

      【讨论】:

        【解决方案4】:

        为 N 的素数创建一个映射,其中键是 int(素数),值是计数。 为 N 做这张地图。

        say, N = 10
        factors = 2 x 5
        map[2] = 1
        map[5] = 1
        
        

        然后遍历 A,B,...Z 之类的计数并找到素因数并从上图减少计数

        say A= 5, factors= 5 x1
        //just mark 
        map[5] = 1-1 = 0
        
        similarly, for B....Z
        

        现在为了答案,从最大的素数开始遍历map,如果值为正则继续乘键,如果值为负则继续除以键。

        
            tmp = 5 , //largest prime factor
            result = 1
            for(int i=tmp;i>1;i--) {
              if(map[tmp]>0) {
                result = result * tmp * map[tmp];
               } else if( map[tmp]<0) {
                  result = result / (tmp * map[tmp] * -1);`
               }
            }
            
            print(result)
        
        

        【讨论】:

        • 好吧,我的问题是如何在没有素因数的情况下获得结果
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-05-16
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多