计算 n 的值 选择 k

Posted

技术标签:

【中文标题】计算 n 的值 选择 k【英文标题】:Calculate value of n choose k 【发布时间】:2013-02-24 11:07:01 【问题描述】:

评估 n 选择 k 的值的最有效方法是什么? 我认为的蛮力方法是找到 n factorial / k factorial / (n-k) factorial 。

更好的策略可能是根据这个recursive formula使用dp。还有其他更好的方法来评估 n 选择 k 吗?

【问题讨论】:

阶乘计算在空间和时间方面都比您的递归替代方案更有效 好吧,对于初学者,您可以将 n!/k! 替换为 n*(n-1)*(n-2)*...*(k+1) 当许多因素抵消时,完全计算 n!k! 毫无意义。 您考虑的 n 范围是多少? @AndrewMorton 我必须计算 n 选择 k 其中 n @M42:此问题与您链接的问题不重复。该问题要求 n 中 k 个元素的所有组合,而此问题只需要 number 个此类组合。 【参考方案1】:

这是我的版本,它纯粹以整数工作(除以 k 总是产生整数商)并且在 O(k) 时速度很快:

function choose(n, k)
    if k == 0 return 1
    return (n * choose(n - 1, k - 1)) / k

我递归地写了它,因为它是如此简单和漂亮,但如果你愿意,你可以将它转换为一个迭代解决方案。

【讨论】:

不是 O(k); k 严格小于 n,所以你不能忽略 n 对运行时的贡献。充其量,你可以说它是 O(k M(n)),其中 M(n) 是你的乘法算法的速度. 正确,但迂腐。上述函数进行 O(k) 乘法和除法。我忽略了操作本身的位复杂性。 这个函数计算n!/k!。这不是问题所在 @icepack:不,它没有。分子的范围从 n 到 n-k+1。分母的范围从 k 到 1。因此,choose(9,4) = (9*8*7*6) / (4*3*2*1) = 126,这是正确的。相比之下,9!/4! = 362880 / 24 = 15120。 这是递归形式的乘法。它确实是 O(k),它是最快的,除非估计 k!使用斯特林的近似值就足够了(en.wikipedia.org/wiki/Stirling%27s_approximation)。有一个分而治之的阶乘版本,可能也有帮助 (gmplib.org/manual/Factorial-Algorithm.html)【参考方案2】:

您可以为此使用乘法公式:

http://en.wikipedia.org/wiki/Binomial_coefficient#Multiplicative_formula

【讨论】:

当n-k 请注意(n - (k-i)) / i可能不是整数 @pomber 个人factor (n - (k-i)) / i 实际上可能不是整数,但来自x=1 upto y 的因子的乘积将始终被y 整除(因为正好有y 个连续整数) 公式略有更新:wikimedia.org/api/rest_v1/media/math/render/svg/…【参考方案3】:

计算二项式系数(n choose k) 而不溢出的最简单方法可能是使用帕斯卡三角形。不需要分数或乘法。 (n choose k)。帕斯卡三角形的nth 行和kth 条目给出了值。

Take a look at this page。这是一个只有加法的O(n^2) 操作,您可以使用动态规划来解决。对于任何可以放入 64 位整数的数字来说,这将是闪电般的速度。

【讨论】:

和 O(n^2) 额外空间 @rici 正确,但这将是对所提出方法的优化【参考方案4】:

如果您要计算许多这样的组合,计算帕斯卡三角无疑是最佳选择。正如你已经知道递归公式,我想我可以在这里传递一些代码:

MAX_N = 100
MAX_K = 100

C = [[1] + [0]*MAX_K for i in range(MAX_N+1)]

for i in range(1, MAX_N+1):
    for j in range(1, MAX_K+1):
        C[i][j] = C[i-1][j-1] + C[i-1][j];

print C[10][2]
print C[10][8]
print C[10][3]

【讨论】:

【参考方案5】:

n!/k!(n-k)! 方法的问题与其说是成本问题,不如说是! 增长非常迅速的问题,因此即使nCk 的值在 64 位范围内也是如此整数,中间计算不是。如果您不喜欢 kainaw 的递归加法方法,您可以尝试乘法方法:

nCk == product(i=1..k) (n-(k-i))/i

其中product(i=1..k) 表示当i 取值1,2,...,k 时所有项的乘积。

【讨论】:

在最终答案不再适合机器字之前,您对溢出破坏事物的可能性是正确的,但我不喜欢您的解决方案:它会为某些因素产生小数值,例如当 n=4 和 k=3 时,i=2 因子。 (当然,这些因素最终会相乘得到一个整数,但你的方式意味着中间结果需要存储在浮点数中——哎呀!)【参考方案6】:

最快的方法可能是使用公式,而不是帕斯卡三角形。当我们知道以后要除以相同的数字时,让我们开始不做乘法。 如果 k

n! / (k! x (n-k)!) = (product of numbers between (k+1) and n) / (n-k)!

至少使用这种技术,您永远不会除以以前用来相乘的数字。你有 (n-k) 个乘法和 (n-k) 个除法。

我正在考虑一种避免所有除法的方法,即在我们必须相乘的数字和我们必须除的数字之间找到 GCD。我稍后会尝试编辑。

【讨论】:

找到GCD肯定会减少操作量。不幸的是,GCD 的发现本身将是一项艰巨的任务。 是的,我很害怕。但是当乘法很大时,GCD 将在较小的数字上计算。实际上我不确定 GCD 是否比除法更难。 我倾向于持怀疑态度,但看看结果会很有趣:) Division 和 GCD 对于 2 个大小为 n 的数字都有 O(n^2) 复杂度。在这里,我们将计算一个大数和一个小数的除法,而 GCD 将计算 2 个小数,但我们需要对所有数字进行除法。如果我必须手工做,我想我会尝试至少找到明显的倍数和 GCD,以避免做无用的除法。 如果你想要 C(n,k) 的素因数分解,你可以使用 Krummer 定理。 (您需要知道所有小于或等于 n 的素数。)planetmath.org/encyclopedia/KummersTheorem.html 这并不能完全避免除法,因为您需要能够为每个素数 p 表达 k 和 n-k 基 p。【参考方案7】:

如果你有一个阶乘查找表,那么 C(n,k) 的计算将会非常快。

【讨论】:

对于大量的 n 和 k,查找表可能令人望而却步。该表之外的值也应该有一个选项。 @Pedrom 没有提到问题中数字大小的限制。它被标记为language-agnosticalgorithms【参考方案8】:

“最有效”是一个糟糕的要求。你想提高什么效率?堆栈?记忆?速度?总的来说,我认为递归方法是最有效的,因为它只使用加法(一种廉价的操作),并且在大多数情况下递归不会太糟糕。功能是:

nchoosek(n, k)

    if(k==0) return 1;
    if(n==0) return 0;
    return nchoosek(n-1, k-1)+nchoosek(n-1,k);

【讨论】:

这是一个树递归,对于 n 和 k 的大值,它可能根本无法完成。 这是O(2^n) 时间和O(n) 空间。阶乘计算是O(n) 时间和O(1) 空间。 这绝对是指数级的。计算 nchoosek(n,k) 的复杂性至少是 nchoosek(n,k),因为你的基本情况是 0 和 1。如果你对动态编程做同样的事情,你会得到一个 ^2 的复杂性,在这里你'多次重新计算相同的结果。 @AndrewMao 每次调用此函数都会在递归树中产生 2 个节点。递归在 n 步后停止(我假设 k 树中的 2^n 个节点,O(2^n) 运行时间。记忆化与这种方法是正交的,记忆化的好处与递归无关。 即使有记忆,这也是一个糟糕的方法。 C(10^12, 2) 需要一万亿次加法,但乘法公式是瞬时的。

以上是关于计算 n 的值 选择 k的主要内容,如果未能解决你的问题,请参考以下文章

C 程序计算从 n 个不同对象中选择 k 个对象的方法的数量。 'k' 和 'n' 都是整数

作业:我如何计算这个函数的时间复杂度?

阶乘计算

十五:阶乘计算

蓝桥杯 阶乘计算

大数处理--阶乘计算