アプローチ1:
C(n、r)= n!/(n-r)!r!
アプローチ2:
本の中で wilfによる組み合わせアルゴリズム 、私はこれを見つけました:
C(n、r)はC(n-1,r) + C(n-1,r-1)
と書くことができます。
例えば.
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)を計算するより良い方法はどれですか?.
どちらの方法でも時間を節約できますが、最初の方法では 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で除算することです(およびstillオーバーフローが発生する可能性があります)。
アプローチ2:
このアプローチでは、実際に パスカルの三角形 を構築します。動的なアプローチは、再帰的なアプローチよりも高速です(最初のアプローチはO(n^2)
で、もう1つのアプローチは指数関数です)。ただし、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から開始する場合、上書きされる値を保存する別の変数が必要になります。
あなたの再帰的なアプローチは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の効率的な実装です
再帰的アプローチは問題ありませんが、アプローチでDPを使用すると、サブ問題を再度解決するためのオーバーヘッドが削減されます。
nCr(n,r) = nCr(n-1,r-1) + nCr(n-1,r);
nCr(n,0)=nCr(n,n)=1;
サブ結果を2次元配列に保存することで、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を素因数に分解することにより、分割をすべて回避します。ウラジミールは、エラトステネスのふるいを使用してこれを非常に効率的に行うことができると述べているように、フェルマーの小さな定理を使用してnCr mod MOD(MODは素数)を計算します。
動的プログラミングを使用すると、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;
}
}