方法1:
C(n,r)= n!/(n-r)!r!
方法2:
在Combinatorial Algorithms by wilf一书中,我发现了这一点:
C(n,r)可以写成C(n-1,r) + C(n-1,r-1)
。
e.g。
C(7,4) = C(6,4) + C(6,3)
= C(5,4) + C(5,3) + C(5,3) + C(5,2)
. .
. .
. .
. .
After solving
= C(4,4) + C(4,1) + 3*C(3,3) + 3*C(3,1) + 6*C(2,1) + 6*C(2,2)
如您所见,最终解决方案不需要任何乘法。在每种形式C(n,r)中,n == r或r == 1.
以下是我实施的示例代码:
int foo(int n,int r)
{
if(n==r) return 1;
if(r==1) return n;
return foo(n-1,r) + foo(n-1,r-1);
}
请在此处查看output。
在方法2中,存在重叠的子问题,我们正在调用递归来再次解决相同的子问题。我们可以使用Dynamic Programming来避免它。
我想知道哪个是计算C(n,r)的更好方法?
答案 0 :(得分:54)
这两种方法都可以节省时间,但第一种方法很容易integer overflow。
方法1:
这种方法将在最短的时间内生成结果(最多n/2
次迭代),并且可以通过仔细进行乘法来减少溢出的可能性:
long long C(int n, int r) {
if(r > n - r) r = n - r; // because C(n, r) == C(n, n - r)
long long ans = 1;
int i;
for(i = 1; i <= r; i++) {
ans *= n - r + i;
ans /= i;
}
return ans;
}
此代码将从较小的一端开始乘以分子,并且由于任何k
个连续整数的乘积可被k!
整除,因此不存在可除性问题。但是溢出的可能性仍然存在,另一个有用的技巧可能是在进行乘法和除法之前将n - r + i
和i
除以它们的GCD(并且可能发生仍然溢出)。
方法2:
在这种方法中,您实际上将构建Pascal's Triangle。动态方法比递归方法快得多(第一个是O(n^2)
,而另一个是指数)。但是,您也需要使用O(n^2)
内存。
# define MAX 100 // assuming we need first 100 rows
long long triangle[MAX + 1][MAX + 1];
void makeTriangle() {
int i, j;
// initialize the first row
triangle[0][0] = 1; // C(0, 0) = 1
for(i = 1; i < MAX; i++) {
triangle[i][0] = 1; // C(i, 0) = 1
for(j = 1; j <= i; j++) {
triangle[i][j] = triangle[i - 1][j - 1] + triangle[i - 1][j];
}
}
}
long long C(int n, int r) {
return triangle[n][r];
}
然后,您可以在C(n, r)
时间内查找任何O(1)
。
如果您需要特定的C(n, r)
(即不需要完整的三角形),则可以通过从上到下覆盖三角形的同一行来使内存消耗O(n)
。
# define MAX 100
long long row[MAX + 1]; // initialized with 0's by default if declared globally
int C(int n, int r) {
int i, j;
// initialize by the first row
row[0] = 1; // this is the value of C(0, 0)
for(i = 1; i <= n; i++) {
for(j = i; j > 0; j--) {
// from the recurrence C(n, r) = C(n - 1, r - 1) + C(n - 1, r)
row[j] += row[j - 1];
}
}
return row[r];
}
内循环从末尾开始以简化计算。如果从索引0开始,则需要另一个变量来存储被覆盖的值。
答案 1 :(得分:9)
我认为您的递归方法应该与DP
一起高效工作。但是一旦限制增加,它就会开始出现问题。见http://www.spoj.pl/problems/MARBLES/
这是我在网上评委和编码比赛中使用的功能。所以它的工作速度非常快。
long combi(int n,int k)
{
long ans=1;
k=k>n-k?n-k:k;
int j=1;
for(;j<=k;j++,n--)
{
if(n%j==0)
{
ans*=n/j;
}else
if(ans%j==0)
{
ans=ans/j*n;
}else
{
ans=(ans*n)/j;
}
}
return ans;
}
这是您的方法#1的有效实施
答案 2 :(得分:3)
你的递归方法很好但是用你的方法使用DP将减少再次解决子问题的开销。现在我们已经有了两个条件 -
nCr(n,r) = nCr(n-1,r-1) + nCr(n-1,r);
nCr(n,0)=nCr(n,n)=1;
现在我们可以通过将子结果存储在二维数组中来轻松构建DP解决方案 -
int dp[max][max];
//Initialise array elements with zero
int nCr(int n, int r)
{
if(n==r) return dp[n][r] = 1; //Base Case
if(r==0) return dp[n][r] = 1; //Base Case
if(r==1) return dp[n][r] = n;
if(dp[n][r]) return dp[n][r]; // Using Subproblem Result
return dp[n][r] = nCr(n-1,r) + nCr(n-1,r-1);
}
现在,如果你想进一步加强,那么得到二项式系数的素数因子化可能是计算它的最有效方法,特别是如果乘法是昂贵的。
我所知道的最快的方法是弗拉基米尔的方法。通过将nCr分解为素因子,可以避免一起划分。正如Vladimir所说,使用Eratosthenes筛可以非常有效地完成这项工作。另外,使用 Fermat的小定理来计算nCr mod MOD(其中MOD是素数)。
答案 3 :(得分:0)
使用动态编程,您可以轻松找到nCr,这是解决方案
package com.practice.competitive.maths;
import java.util.Scanner;
public class NCR1 {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(System.in)) {
int testCase = scanner.nextInt();
while (testCase-- > 0) {
int n = scanner.nextInt();
int r = scanner.nextInt();
int[][] combination = combination();
System.out.println(combination[n][r]%1000000007);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static int[][] combination() {
int combination[][] = new int[1001][1001];
for (int i = 0; i < 1001; i++)
for (int j = 0; j <= i; j++) {
if (j == 0 || j == i)
combination[i][j] = 1;
else
combination[i][j] = combination[i - 1][j - 1] % 1000000007 + combination[i - 1][j] % 1000000007;
}
return combination;
}
}
答案 4 :(得分:-1)
unsigned long long ans = 1,a=1,b=1;
int k = r,i=0;
if (r > (n-r))
k = n-r;
for (i = n ; k >=1 ; k--,i--)
{
a *= i;
b *= k;
if (a%b == 0)
{
a = (a/b);
b=1;
}
}
ans = a/b;