Каков хороший способ реализовать нотацию выбора в Java?

... желательно на Java. Вот что у меня есть:

//x choose y
public static double choose(int x, int y) {
    if (y < 0 || y > x) return 0;
    if (y == 0 || y == x) return 1;

    double answer = 1;
    for (int i = x-y+1; i <= x; i++) {
        answer = answer * i;
    }
    for (int j = y; j > 1; j--) {
        answer = answer / j;
    }
    return answer;
}

Мне интересно, есть ли лучший способ сделать это?


person echoblaze    schedule 05.11.2009    source источник
comment
Поскольку функция выбора определена как двоичная функция натуральных чисел (|N x |N => |N), вы не должны использовать удвоения ни в одной точке. Это на самом деле делает выбранное вами решение излишне неоптимальным.   -  person Ralph M. Rickenbach    schedule 28.05.2010


Ответы (6)


choose(n,k) = n! / (n-k)! k!

Вы можете сделать что-то подобное в O (k):

public static double choose(int x, int y) {
    if (y < 0 || y > x) return 0;
    if (y > x/2) {
        // choose(n,k) == choose(n,n-k), 
        // so this could save a little effort
        y = x - y;
    }

    double denominator = 1.0, numerator = 1.0;
    for (int i = 1; i <= y; i++) {
        denominator *= i;
        numerator *= (x + 1 - i);
    }
    return numerator / denominator;
}

ИЗМЕНИТЬ Если x и y большие, вы будете переполняться медленнее (т. е. быть в безопасности для больших значений x и y), если вы разделите свой ответ по мере продвижения:

    double answer = 1.0;
    for (int i = 1; i <= y; i++) {
        answer *= (x + 1 - i);
        answer /= i;           // humor 280z80
    }
    return answer;
person mob    schedule 05.11.2009
comment
-1: ответ излишне теряет точность. Choose(9,4) получает умножение на 7,0/3, что не может быть точно выражено в двойном значении. Смотрите мой для правильной реализации. - person Sam Harwell; 07.11.2009
comment
Действительно? Правильная реализация состоит в том, чтобы преобразовать два long в объекты BigInteger, вызвать BigInteger.GreatestCommonDivisor, а затем вернуть результат обратно в long? Шутки в сторону? - person mob; 08.11.2009
comment
Сначала сделайте так, чтобы ваш возвращал правильный результат, затем мы можем обсудить, какой из них более эффективен. - person Sam Harwell; 08.11.2009
comment
@ 280Z28 Не будь таким самодовольным. choose(11,5) => 461.99999999999994 с этим алгоритмом, choose(11,5) => 2184 с вашим. Но я получаю неправильный ответ в 10 раз быстрее. - person mob; 10.11.2009
comment
Почему вы использовали плавающую точку? В отредактированном ответе переменная answer никогда не является значением с плавающей запятой. Кроме того, если бы вы зацикливали числитель от меньшего к большему, у вас было бы меньше шансов на переполнение. - person Eyal; 05.11.2013

Числа, с которыми вы имеете дело, станут довольно большими и быстро превысят точность значений double, что даст вам неожиданно неправильные результаты. По этой причине вы можете рассмотреть решение с произвольной точностью, такое как использование java.math.BigInteger, которое не будет страдать от этой проблемы.

person Greg Hewgill    schedule 05.11.2009
comment
Я думаю, что обе петли должны быть вместе (вторая петля должна идти в противоположном направлении). - person Tom Hawtin - tackline; 05.11.2009

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

Я думаю, что я бы, вероятно, изменил порядок второго цикла, чтобы оба цикла были восходящими.

Как говорит Грег, если вам нужно получить точные ответы для больших чисел, вам следует рассмотреть альтернативные типы данных. Учитывая, что результатом всегда должно быть целое число, вы можете выбрать BigInteger (несмотря на все деления, результатом всегда будет целое число):

public static BigInteger choose(int x, int y) {
    if (y < 0 || y > x) 
       return BigInteger.ZERO;
    if (y == 0 || y == x) 
       return BigInteger.ONE;

    BigInteger answer = BigInteger.ONE;
    for (int i = x - y + 1; i <= x; i++) {
        answer = answer.multiply(BigInteger.valueOf(i));
    }
    for (int j = 1; j <= y; j++) {
        answer = answer.divide(BigInteger.valueOf(j));
    }
    return answer;
}
person Jon Skeet    schedule 05.11.2009

Я закодировал это на C#, но я попытался сделать его как можно более применимым к Java.

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

Код:

public static long BinomialCoefficient(long n, long k)
{
    if (n / 2 < k)
        return BinomialCoefficient(n, n - k);

    if (k > n)
        return 0;

    if (k == 0)
        return 1;

    long result = n;
    for (long d = 2; d <= k; d++)
    {
        long gcd = (long)BigInteger.GreatestCommonDivisor(d, n);
        result *= (n / gcd);
        result /= (d / gcd);
        n++;
    }

    return result;
}
person Sam Harwell    schedule 05.11.2009
comment
Этот код странный. Почему n продолжает расти? И почему бы не вывести n из d вместо того, чтобы изменять его в цикле? - person Eyal; 05.11.2013

за

N!/((R!)(N-R)!)

используйте это (псевдокод)

if (R>N) return 0;

long r = max(R, N-r)+1;
if (R==N) return 1;

for (long m = r+1, long d = 2; m <= N; m++, d++ ) {
    r *= m;
    r /= d;
}
return r;
person Ralph M. Rickenbach    schedule 28.05.2010

Эта версия не требует BigInteger или арифметики с плавающей запятой и работает без ошибок переполнения для всех n меньше 62. 62 больше 28 — это первая пара, которая приводит к переполнению.

public static long nChooseK(int n, int k) {
    k = Math.min(k, n - k);

    if (n < 0 || k < 0)
        throw new IllegalArgumentException();

    if (k == 0)
        return 1;

    long value = n--;

    for (int i = 2; i <= k; i++) {
        value = Math.multiplyExact(value, n--);
        value /= i;
    }

    return value;
}

Следующий тест доказывает, что это так:

@Test
void nChooseKLongVsBigInt() {
    for (int n = 0; n < 62; n++) {
        for (int k = 0; k <= n; k++) {
            assertEquals(nChooseKBigInt(n, k), BigInteger.valueOf(nChooseK(n, k)));
        }
    }
}

private BigInteger nChooseKBigInt(int n, int k) {
    return factorial(n).divide(factorial(k).multiply(factorial(n - k)));
}

private BigInteger factorial(int number) {
    BigInteger result = BigInteger.ONE;

    for (int factor = 2; factor <= number; factor++) {
        result = result.multiply(BigInteger.valueOf(factor));
    }

    return result;
}
person devconsole    schedule 04.02.2018