计算 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-agnostic
和algorithms
。【参考方案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的主要内容,如果未能解决你的问题,请参考以下文章