计算字谜时如何避免溢出?
Posted
技术标签:
【中文标题】计算字谜时如何避免溢出?【英文标题】:How to avoid overflow when counting anagrams? 【发布时间】:2020-12-06 01:47:41 【问题描述】:令 N 为字符串的大小。设 A, B, C..., Z 为字符串中每个字母出现的次数。
我需要计算字谜的数量:N!/(A!*B!*C!...*Z!)。
最终的结果是保证适合整数,但原始字符串的长度可以是任意大小。
到目前为止,我唯一的想法是对乘积中的数字进行质因式分解,然后消除分母中也存在的分子因子。
有没有更实用的方法来实现这一点?
【问题讨论】:
使用 bigint 库怎么样? 【参考方案1】:您可以通过交错乘法和除法来进行计算,而不是先进行所有分子乘法,然后再除以所有除数。交错操作显着减小了中间值的大小,但并不能完全保证中间结果不会大于最终结果。稍加努力,我们可以找到一个乘法和除法的顺序,其中除法总是精确的,没有中间结果超过最终结果。 (如果您不关心解释,请跳至此答案底部的示例代码。)
了解交错乘法和除法的工作原理很有用。为了精确除法,除法之前的中间值必须是除数的精确倍数。在这种情况下,这是真的,因为乘数是一个稳定递增的整数序列。
这是一个简单的交错示例,只有两个字母。我们要计算 (7 C 5),即aaaaabb
的字谜数。 (这也是一个二项式系数,因为它与询问长度为 7 的列表中 5 个位置的集合的数量相同。我们可以通过将a
s 放在所选的五个位置中,将b
s 放在另外两个。)所以天真的计算是:
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
的乘法。现在,考虑两种可能性:
-
最后一次乘以
p
的倍数。
最后的乘法不是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)
【讨论】:
好吧,我的问题是如何在没有素因数的情况下获得结果以上是关于计算字谜时如何避免溢出?的主要内容,如果未能解决你的问题,请参考以下文章