Какой лучший способ рассчитать nCr

подход 1:
C (n,r) = n!/(н-р)!Р!

подход 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);
}

посмотреть выход здесь.

в подходе 2 есть перекрывающиеся подзадачи, где мы вызываем рекурсию, чтобы снова решить те же подзадачи. Мы можем избежать этого, используя Динамическое Программирование.

Я хочу знать, какой лучший способ вычислить C (n,r)?.

4 ответов


оба подхода сэкономят время, но первый из них очень склонен к переполнение целых чисел.

подход 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:

в этом подходе вы фактически создадите треугольник Паскаля. Динамический подход гораздо быстрее чем рекурсивный (первый -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, Вам понадобится другая переменная для хранения перезаписываемого значения.


Я думаю, что ваш рекурсивный подход должен эффективно работать с 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;

теперь мы можем легко построить решение DP, сохранив наши подрешетки в 2-D массиве -

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);
}

теперь, если вы хотите продолжить otimise, получение простой факторизации биномиального коэффициента, вероятно, является наиболее эффективным способом его вычисления, особенно если умножение дорогой.

самый быстрый метод, который я знаю, это Владимира!--10-->. Можно избежать разделения всех вместе, разложив nCr на простые факторы. Как говорит Владимир, Вы можете сделать это довольно эффективно, используя сито Эратосфена.Кроме Того, Используйте теорема Ферма для вычисления nCr mod MOD (Где MOD-простое число).


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;