什么是评估"n
选k
"值的最有效方法?我认为暴力方法是通过单独计算每个阶乘来找到n! / k! / (n-k)!
.
更好的策略可能是根据这个递归公式使用DP,nCk == (n-1)C(k-1) + (n-1)C(k)
。有没有其他更好的方法来评估n
选k
,以复杂度和避免溢出的风险为考虑?
什么是评估"n
选k
"值的最有效方法?我认为暴力方法是通过单独计算每个阶乘来找到n! / k! / (n-k)!
.
更好的策略可能是根据这个递归公式使用DP,nCk == (n-1)C(k-1) + (n-1)C(k)
。有没有其他更好的方法来评估n
选k
,以复杂度和避免溢出的风险为考虑?
以下是我的版本,它完全使用整数(k的除法始终产生整数商),速度为O(k):
function choose(n, k)
if k == 0 return 1
return (n * choose(n - 1, k - 1)) / k
我递归写它是因为它非常简单而且漂亮,但如果你喜欢,可以把它转换成迭代解决方案。
(n - (k-i)) / i
可能不是整数。 - pomberfactor (n - (k-i)) / i
可能实际上不是整数,但是从 x=1
到 y
的因子积将总是可被 y
整除的(因为恰好有 y 个连续的整数)。 - Lambder(n choose k)
是避免溢出最简单的方法。不需要分数或乘法。(n choose k)
。Pascal三角形的第n
行和第k
个条目给出该值。
看看这个页面。这是一个O(n^2)
操作,只包含加法,可以用动态规划解决。对于能够适合64位整数的任何数字,它都会非常快速。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]
在遇到类似问题后,我决定编译最佳解决方案并对每个方案进行了一些不同值的n和k的简单测试。起初我有大约10个函数,逐渐淘汰出那些明显错误或在特定值处停止工作的函数。在所有的解决方案中,用户user448810提供的答案是最简洁、最易于实现的,我非常喜欢它。下面是包括我运行的每个测试的次数、每个函数的代码、函数的输出以及得到该输出所花费的时间的代码。我只运行了20000次,在重新运行测试时仍存在时间波动,但您应该可以获得每个函数的整体表现。
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//EXPECTED VALUES
//x choose x = 1
//9 choose 4 =126
//52 choose 5 = 2598960;
//64 choose 33 = 1.777090076065542336E18;
//# of runs for each test: 20000
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//https://dev59.com/3WnWa4cB1Zd3GeqP4dXd#12983878
public static double combination(long n, long k) {
double sum=0;
for(long i=0;i<k;i++) {
sum+=Math.Log10(n-i);
sum-=Math.Log10(i+1);
}
return Math.Pow(10, sum);
}
/*
10 choose 10
0.9999999999999992
Elapsed=00:00:00.0000015
9 choose 4
126.00000000000001
Elapsed=00:00:00.0000009
52 choose 5
2598959.9999999944
Elapsed=00:00:00.0000013
64 choose 33
1.7770900760655124E+18
Elapsed=00:00:00.0000058
*/
//........................................................
//https://dev59.com/3WnWa4cB1Zd3GeqP4dXd#19125294
public static double BinomCoefficient(long n, long k) {
if (k > n) return 0;
if (n == k) return 1; // only one way to chose when n == k
if (k > n-k) k = n-k; // Everything is symmetric around n-k, so it is quicker to iterate over a smaller k than a larger one.
double c = 1;
for (long i = 1; i <= k; i++) {
c *= n--;
c /= i;
}
return c;
}
/*
10 choose 10
1
Elapsed=00:00:00
9 choose 4
126
Elapsed=00:00:00.0000001
52 choose 5
2598960
Elapsed=00:00:00.0000001
64 choose 33
1.7770900760655432E+18
Elapsed=00:00:00.0000006
*/
//........................................................
//https://dev59.com/UWUp5IYBdhLWcg3wY29V#15302448
public static double choose(long n, long k) {
if (k == 0) return 1;
return (n * choose(n-1, k-1)) / k;
}
/*
10 choose 10
1
Elapsed=00:00:00.0000002
9 choose 4
126
Elapsed=00:00:00.0000003
52 choose 5
2598960
Elapsed=00:00:00.0000004
64 choose 33
1.777090076065543E+18
Elapsed=00:00:00.0000008
*/
//........................................................
//My own version which is just a mix of the two best above.
public static double binomialCoeff(int n, int k) {
if (k > n) return 0;
if (k > n-k) k = n-k; // Everything is symmetric around n-k, so it is quicker to iterate over a smaller k than a larger one.
double recusion(long n, long k) {
if (k == 0) return 1; // only one way to chose when n == k
return (n * recusion(n-1, k-1)) / k;
}
return recusion(n,k);
}
/*
10 choose 10
1
Elapsed=00:00:00
9 choose 4
126
Elapsed=00:00:00.0000001
52 choose 5
2598960
Elapsed=00:00:00.0000002
64 choose 33
1.777090076065543E+18
Elapsed=00:00:00.0000007
*/
//........................................................
//https://en.wikipedia.org/wiki/Binomial_coefficient
public static double binomial(long n, long k) {
// take advantage of symmetry
if (k > n-k) k = n-k;
long c = 1;
for (long i = 1; i <= k; i++, n--) {
// return 0 on potential overflow
if (c/i >= long.MaxValue/n) return 0;
// split c * n / i into (c / i * i) + (c % i * n / i)
c = (c / i * n) + (c % i * n / i);
}
return c;
}
/*
10 choose 10
1
Elapsed=00:00:00.0000006
9 choose 4
126
Elapsed=00:00:00.0000002
52 choose 5
2598960
Elapsed=00:00:00.0000003
64 choose 33
1.7770900760655424E+18
Elapsed=00:00:00.0000029
*/
n!/k!(n-k)!
方法的问题不在于成本,而是在于!
增长得非常迅速,以至于即使对于nCk
的值来说,这些中间计算也不在64位整数的范围内。如果您不喜欢kainaw的递归加法方法,可以尝试乘法方法:
nCk == product(i=1..k) (n-(k-i))/i
其中product(i=1..k)
表示当i
取值1,2,...,k
时所有项的乘积。最快的方法可能是使用公式,而不是帕斯卡三角形。当我们知道后面要除以相同的数字时,让我们先不进行乘法运算。 如果 k < n/2,则让 k = n - k。我们知道 C(n,k) = C(n,n-k) 现在:
n! / (k! x (n-k)!) = (product of numbers between (k+1) and n) / (n-k)!
至少采用这种技术,您永远不会将一个数除以您之前乘过的数字。您有(n-k)次乘法和(n-k)次除法。
我正在考虑一种避免所有除法的方法,即通过找到我们必须相乘的数字和我们必须相除的数字之间的最大公约数来实现。稍后我会尝试编辑。
如果有帮助到其他人的话,我希望这个答案能够解决问题。
我之前没有看到过这个答案。
def choose(n, k):
nominator = n
for i in range(1,k):
nominator *= (n-i)
k *= i
return nominator/k
n*(n-1)*(n-2)*...*(k+1)
代替n!/k!
。 许多因子都会被消除,完全计算n!
和k!
没有意义。 - Tim Goodman