При решении задач комбинаторики часто возникает необходимость в расчете биномиальные коэффициентов. Бином Ньютона, т.е. разложение image также использует биномиальные коэффициенты. Для их расчета можно использовать формулу, выражающую биномиальный коэффициент через факториалы: image или использовать рекуррентную формулу:image Из бинома Ньютона и рекуррентной формулы ясно, что биномиальные коэффициенты — целые числа. На данном примере хотелось показать, что даже при решении несложной задачи можно наступить на грабли.
Прежде чем перейти к написанию процедур расчета, рассмотрим так называемый треугольник Паскаля.
          1
       1     1
    1     2     1
  1    3     3     1
1   4     6     4     1

или он же, но немного в другом виде. В левой колонке строки значение n, дальше в строке значения image для k=0..n
 n          биномиальные коэффициенты
 0      1
 1      1      1
 2      1      2      1
 3      1      3      3      1
 4      1      4      6      4      1
 5      1      5     10     10      5      1
 6      1      6     15     20     15      6      1
 7      1      7     21     35     35     21      7      1
 8      1      8     28     56     70     56     28      8      1
 9      1      9     36     84    126    126     84     36      9      1
10      1     10     45    120    210    252    210    120     45     10      1
11      1     11     55    165    330    462    462    330    165     55     11      1
12      1     12     66    220    495    792    924    792    495    220     66     12      1
13      1     13     78    286    715   1287   1716   1716   1287    715    286     78     13      1
14      1     14     91    364   1001   2002   3003   3432   3003   2002   1001    364     91     14      1
15      1     15    105    455   1365   3003   5005   6435   6435   5005   3003   1365    455    105     15      1
16      1     16    120    560   1820   4368   8008  11440  12870  11440   8008   4368   1820    560    120     16      1


В полном соответствии с рекуррентной формулой значения image равны 1 и любое число равно сумме числа, стоящего над ним и числа «над ним+шаг влево». Например, в 7й строке число 21, а в 6й строке числа 15 и 6: 21=15+6. Видно также, что значения в строке симметричны относительно середины строки, т.е. image. Это свойство симметричности бинома Ньютона относительно a и b и оно видно в факториальной формуле.
Ниже для биномиальных коэффициентов image я буду также использовать представление C(n,k) (его проще набирать, да и формулу-картинку не везде можно вставить.

Расчет биномиальных коэффициентов через факториальную формулу


поскольку биномиальные коэффициенты неотрицательные, будем использовать в расчетах беззнаковый тип.
// расчет факториала n
unsigned fakt(int n)
{
   unsigned r;
   for (r=1;n>0;--n)  
         r*=n;
   return r;
}
// расчет C(n,k)
unsinged bci(int n,int k)
{
   return fakt(n)/(fakt(k)*fakt(n-k));
}


Вызовем функцию bci(10,4) — она вернет 210 и это правильное значение коэффициента C(10,4). Значит, задача расчета решена? Да, решена. Но не совсем. Мы не ответили на вопрос: при каких максимальных значениях n,k функция bci будет работать правильно? Прежде чем начать искать ответ, условимся, что используемый нами тип unsigned int 4-х байтный и максимальное значение равно 232-1=4'294'967'295. При каких n,k C(n,k) превысит его? Обратимся к треугольнику Паскаля. Видно, что максимальные значения достигаются в середине строки, т.е. при k=n/2. Если n четно, то имеется одно максимальное значение, а если n нечетно, то их два. Точное значение C(34,17) равно 2333606220, а точное значение C(35,17) равно 4537567650, т.е. уже больше максимального unsigned int.
Напишем тестовую процедуру
void test()
{
    for (n=10;n<=35;++n) 
    printf("%u %u",n,bci(n,n/2);
 // для C++ можно использовать cout<<n<<" "<<bci(n,n/2)<<endl;
}

Запустим ее и увидим
10 252
11 462
12 924
13 532
14 50
15 9
16 1
17 2
18 1
19 0
20 0
21 1
22 0
23 4
24 1
25 0
26 1
27 0
28 1
29 0
30 0
31 0
32 2
33 2
34 0
35 0


Значение в очередной строке должно быть примерно в 2 раза больше, чем в предыдущей. Поэтому последний правильно вычисленный коэффициент (см треугольник выше) — это C(12,6) Хотя unsigned int вмещает 4млрд, правильно вычисляются значения меньше 1000. Вот те раз, почему так? Все дело в нашей процедуре bci, точнее в строке, которая сначала вычисляет большое число в числителе, а потом делит его на большое число в знаменателе. Для вычисления C(13,6) сначала вычисляется 13!, а это число > 6млрд и оно не влезает в unsigned int.
Как оптимизировать расчет image? Очень просто: раскроем 13! и сократим числитель и знаменатель на 7! В результате получится image. Запрограммируем расчет по этой формуле
unsigned bci(int n,int k) 
{
	if (k>n/2) k=n-k; // возьмем минимальное из k, n-k.. В силу симметричность C(n,k)=C(n,n-k)
	if (k==1)  return n;
	if (k==0)  return 1;
	unsigned r=1;
	for (int i=1; i<=k;++i) {
		r*=n-k+i;
		r/=i;
	}
	return r;
}

:
и снова протестируем
10 252
11 462
12 924
13 1716
14 3432
15 6435
16 12870
17 24310
18 48620
19 92378
20 184756
21 352716
22 705432
23 1352078
24 2704156
25 5200300
26 10400600
27 20058300
28 40116600
29 77558760
30 155117520
31 14209041
32 28418082
33 39374192
34 78748384
35 79433695



Явно лучше, ошибка возникла при расчете C(31,15). Причина понятна — все то же переполнение. Сначала умножаем на 31 (оп-па — переполнение, потом делим на 15). А что, если использовать рекурсивную формулу? Там только сложение, переполнения быть не должно.
Что ж, пробуем:
unsigned bcr(int n,int k) 
{
	if (k>n/2) k=n-k;
	if (k==1)  return n;
	if (k==0)  return 1;
	return bcr(n-1,k)+bcr(n-1,k-1);
}
void test()
{
    for (n=10;n<=35;++n) 
    printf("%u %u",n,bcr(n,n/2);
 // для C++ можно использовать cout<<n<<" "<<bcr(n,n/2)<<endl;
}


Результат теста
10 252
11 462
12 924
13 1716
14 3432
15 6435
16 12870
17 24310
18 48620
19 92378
20 184756
21 352716
22 705432
23 1352078
24 2704156
25 5200300
26 10400600
27 20058300
28 40116600
29 77558760
30 155117520
31 300540195
32 601080390
33 1166803110
34 2333606220
35 242600354


Все, что влезло в unsigned int, посчиталось правильно. Вот только строчка с n=34 считалась около минуты. При расчете C(n,n/2) делается два рекурсивных вызова, поэтому время расчета экспоненциально зависит от n. Что же делать — получается либо неточно, либо медленно. Выход — в использовании 64 битных переменных.

Замечание по результатам обсуждений: в конце статьи добавлен раздел, где приведен простой и быстрый вариант «bcr с запоминанием» одного из участников обсуждения.

Использование 64 битных типов для расчета C(n,k)


Заменим в функции bci unsigned int на unsigned long long и протестируем в диапазоне n=34..68. n=34 — это максимальное значение, которое правильно считается bci, а C(68,34) ~2.8*1019 уже не влезает в unsigned long long ~1.84*1019
unsigned long long bcl(int n,int k) 
{
	if (k>n/2) k=n-k;
	if (k==1)  return n;
	if (k==0)  return 1;
	unsigned long long r=1;
	for (int i=1; i<=k;++i) {
		r*=n-k+i;
		r/=i;
	}
	return r;
}

void test()
{
    for (n=34;n<=68;++n) 
    printf("%llu %llu",n,bcl(n,n/2));
 // для C++ можно использовать cout<<n<<" "<<bcl(n,n/2)<<endl;
}
 

Результат теста
34 2333606220
35 4537567650
36 9075135300
37 17672631900
38 35345263800
39 68923264410
40 137846528820
41 269128937220
42 538257874440
43 1052049481860
44 2104098963720
45 4116715363800
46 8233430727600
47 16123801841550
48 32247603683100
49 63205303218876
50 126410606437752
51 247959266474052
52 495918532948104
53 973469712824056
54 1946939425648112
55 3824345300380220
56 7648690600760440
57 15033633249770520
58 30067266499541040
59 59132290782430712
60 118264581564861424
61 232714176627630544
62 465428353255261088
63 321255810029051666
64 66050867754679844
65 454676336121653775
66 350360427585442349
67 23341572944240599
68 46683145888481198


Видим, что ошибка возникает при n=63 по той же причине, что и в bci. Сначала умножение на 63 (и переполнение), затем деление на 31.

Дальнейшее повышение точности и расчет при n>67


Ошибка возникла при n=63, а при n=68 результат уже не влезает в unsigned64. Поэтому можно сказать «до n<=62 функция bcl считает правильно, дальнейшее увеличение точности требует либо int128 либо длинной арифметики». А если очень высокая точность не нужна, но хочется считать биномиальные коэффициенты при n=100...1000? Снова берем процедуру bci и меняем в ней типы unsigned int на double:
double bcd(int n,int k) 
{
	if (k>n/2) k=n-k; // возьмем минимальное из k, n-k.. В силу симметричности C(n,k)=C(n,n-k)
	if (k==1)  return n;
	if (k==0)  return 1;
	double r=1;
	for (int i=1; i<=k;++i) {
		r*=n-k+i;
		r/=i;
	}
	return ceil(r-0.2); // округлим до ближайшего целого, отбросив дробную часть
                       // комментарий изменен после обсуждений: такой способ использован,  чтобы расхождение с точным значением 
                       // было как можно позже. Где-то оно превышало 0.5 и простой round не годился 
}
void testd()
{
    for (n=50;n<=1000;n+=50) 
    printf("%d %.16e\n",n,bcd(n,n/2));
 // для C++ можно использовать cout<<n<<" "<<bcd(n,n/2)<<endl;
}


Результат теста
50 1.2641060643775200e+014
100 1.0089134454556417e+029
150 9.2826069736708704e+043
200 9.0548514656103225e+058
250 9.1208366928185793e+073
300 9.3759702772827310e+088
350 9.7744946171567713e+103
400 1.0295250013541435e+119
450 1.0929255500575370e+134
500 1.1674431578827770e+149
550 1.2533112137626624e+164
600 1.3510794199619429e+179
650 1.4615494992533863e+194
700 1.5857433585316801e+209
750 1.7248900341772600e+224
800 1.8804244186835327e+239
850 2.0539940413411323e+254
900 2.2474718820660189e+269
950 2.4629741379276902e+284
1000 2.7028824094543663e+299

Даже для n=1000 удалось посчитать! Переполнение double произойдет при n=1030.
Расхождение расчета bcd с точным значением начинается с n=57. Он небольшое — всего 8. При n=67 отклонение 896.

Для экстремалов и «олимпийцев»


В принципе, для практических задач точности функции bcd достаточно, но в олимпиадных задачах часто даются тесты «на грани». Т.е. теоретически может встретится задача, где точности double недостаточно и C(n,k) влезает в unsigned long long еле-еле. Как избежать переполнения для таких крайних случаев? Можно использовать рекурсивный алгоритм. Но если он для n=34 считал минуту, то для n=67 будет считать лет 100. Можно запоминать рассчтанные значения (см Дополнение после публиукации).Также можно использовать рекурсию не для всех n и k, а только для «достаточно больших». Вот процедура расчета, которая считает правильно для n<=67. Иногда и для n>67 при малых k (к примеру, считает C(82,21)=1.83*1019).
unsigned long long bcl(int n,int k) 
{
	if (k>n/2) k=n-k;
	if (k==1)  return n;
	if (k==0)  return 1;
	unsigned long long r;
	if (n+k>=90) {
		// разрядности может не хватить, используем рекурсию
		r=bcl(n-1,k);
		r+=+bcl(n-1,k-1);
	}
	else {
		r=1;
		for (int i=1; i<=k;++i) {
			r*=n-k+i;
			r/=i;
		}
	}
	return r;
}

В какой-то из олимпиадных задач мне потребовалось вычислять много C(n,k) для n >70, т.е. они заведомо не влезали в unsigned long long. Естественно, пришлось использовать «длинную арифметику» (свою). Для этой задачи я написал «рекурсию с памятью»: вычисленные коэффициенты запоминались в массиве и экспоненциального роста времени расчета не было.

Дополнение после публикации


При обсуждении часто упоминаются варианты с запоминанием рассчитанных значений. У меня есть код с динамическим выделением памяти, но я его не привел. На даный момент вот самый простой и эффективный из комментария chersanya: http://habrahabr.ru/post/274689/#comment_8730359http://habrahabr.ru/post/274689/#comment_8730359

unsigned bcr_cache[N_MAX][K_MAX] = {0};

unsigned bcr(int n,int k) 
{
	if (k>n/2) k=n-k;
	if (k==1)  return n;
	if (k==0)  return 1;
        if (bcr_cache[n][k] == 0)
            bcr_cache[n][k] = bcr(n-1,k)+bcr(n-1,k-1);
        return bcr_cache[n][k];
}


Если в программе надо использовать [почти] все коэффициенты треугольника Паскаля (до какого-то уровня), то приведенный рекурсивный алгоритм с запоминанием — самый быстрый способ. Аналогичный код годится и для unsigned long long и даже для длинной арифметики (хотя там, наверное, лучше динамически вычислять и выделять требуемый объем памяти). Конкретные значения N_MAX могут быть такими:
35 — посчитает все коэффициенты C(n,k), n< 35 для unsigned int (32 бита)
68 — посчитает все коэффициенты C(n,k), n< 68 для unsigned long long (64 бита)
200 — посчитает коэффициенты C(n,k), n< 200 и некоторых k для unsigned long long (64 бита). Например, для С(82,21)=~1.83*1019
K_MAX — это может быть N_MAX/2+1, но не больше 35, поскольку C(68,34) за границей unsigned long long.
Для простоты можно всегда брать K_MAX=35 и не думать «войдет — не войдет» (до тех пор, пока не перейдем к числами с разрядностью >64 бита).

Комментарии (72)


  1. stack_trace
    08.01.2016 19:14
    +4

    Извините, но C++ тут совсем непричём.


    1. Teivaz
      08.01.2016 19:25
      +2

      Здесь этот тэг можно применить по аналогии со Stack Overflow, которые пишут:

      Use this tag for questions about code (to be) compiled with a C++ compiler.


      1. stack_trace
        08.01.2016 19:37

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


        1. zelserg
          08.01.2016 20:29

          Я все примеры компилировал на С++. Заголовком я хотел сказать «можно брать функции расчета, вставлять их в программы на языках С, С++ и все будет работать». На Бейсике или Паскале это не получится. Что Вас не устраивает?


          1. stack_trace
            09.01.2016 06:36

            Я же написал, что ничего против не имею, почему вы у меня спрашиваете, что меня не устраивает?


            1. zelserg
              09.01.2016 08:36

              А хочу понять логику высказывания «С++ непричем». Первый отзыв и сразу «непричем»… Если мне нужно найти некий алгоримХ на языке С++, я пишу в поисковике «алгоритмХ С++» и если поисковик возвращает мне алгоритмХ на PHP, то вот этот РНР — «непричем». А если он возвращает алгоритм на С++ или С, то это то, что надо. Я его беру и использую.


              1. stack_trace
                09.01.2016 09:28

                Да потому что широко распространено заблуждение, что если вы можете что-то написать на C то код на C++ будет выглядеть также. Это заблуждение и это чаще всего неверно. То, что код для С компилируется плюсовым компилятором почти всегда и так всем известно, это не делает его кодом на С++. Этот же код, например, отлично скомпилировался бы и Objective C компилятором, может и этот хаб/тег добавите?


                1. zelserg
                  09.01.2016 09:56

                  «То, что код для С компилируется плюсовым компилятором почти всегда и так всем известно». В том то и дело, что раз «почти», то есть исключения. Вот я и написал в заголовке С++ в скобках, показывающее, что примеры в статье к исключениям не относятся.
                  В заголовке указано: на Си (С++), Objective C не подходит ни под одну из категорий?
                  Если вы трактуете простой и понятный текст своебразным образом, то причем тут я, заголовок, С и С++?


                  1. stack_trace
                    09.01.2016 10:08
                    +2

                    Послушайте, я никак не трактую ваш текст, я просто сделал замечание, с которым, я думаю, согласится любой, для кого C++ — основной язык разработки. Код на С != код на С++, равно как С != С++. Заходя в хаб С++ я ожидаю увидеть код на С++, а заходя в хаб С — код на С. При этом я прекрасно отдаю себе отчёт в том, что почти любой код из хаба С скомпилируется как С++.

                    Обычно, указывают что код на C++ когда он явно на C++ и ничем больше не скомпилируется. Так можно ещё поставить теги C++11, C++14, C++17. А что? Компиляторами, поддерживающими эти стандарты он тоже скомпилируется.

                    Вообще, не стоит воспринимать моё замечание так в штыки. Вы просили пояснений — я их дал, я не собирался с вами спорить.

                    В заголовке указано: на Си (С++), Objective C не подходит ни под одну из категорий?
                    Конечно же, нет.


                1. zelserg
                  09.01.2016 10:06
                  -3

                  Насчет заблуждений. Я занимаюсь программированием не один десяток лет и давно избавился от заблуждения, которое в грубой формулировке звучит примерно так: «если при написании кода можно выпендриться, то нужно выпендриться».


                  1. stack_trace
                    09.01.2016 10:10

                    Да причём тут «выпендриться»? Вы о чём вообще?


                    1. zelserg
                      09.01.2016 10:27

                      Похоже, у нас разный менталитет и мы не понимаем друг друга. Вы в тегах С++ ищете то, что будет работать только на С++, а я в тегах С++ ищу то, что будет работать на С++.


                      1. stack_trace
                        09.01.2016 10:34
                        +1

                        Ну, хабр это же не pastbin, тут люди обычно не код ищут а статьи на конкретную тему, узнать что-то новое про конкретную область. Так вот, к теме С++ данная статья имеет крайне слабое отношение, и ничего нового про С++ из этой статьи узнать не получиться, равно как и про Objective C. Про С — возможно, про алгоритмы — наверняка. ИМХО, правильными хабами для этой статьи были бы C и Алгоритмы, но никак не C++.

                        Только не обижайтесь, ради бога. Никак не хотел вас задеть своим замечанием.


                        1. zelserg
                          09.01.2016 11:57
                          -5

                          А разве не вы мне тут минусов наставили? Или какой-то идиот c чувством неполноценности влез в чужое обсуждение?


                          1. zelserg
                            09.01.2016 12:25
                            -9

                            Мне даже жалко этого идиота. То, что ему вернется, явно будет несопоставимо с какими-то «минусами». Хотя он, скорее всего, не свяжет свои мелкие гадости с этим возвратом. Что тут скажешь — сам делал, никто не принуждал…


                          1. stack_trace
                            09.01.2016 16:52
                            +3

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


    1. semenyakinVS
      08.01.2016 19:52
      +3

      Вот-вот. Вот если бы "<название статьи> на этапе компиляции" — тогда другое дело


      1. zelserg
        08.01.2016 20:31
        -1

        Т.е «Расчет биномиальных коэффициентов на Си (С++) на этапе компиляции»? Мне это кажется странным, мягко говоря.


        1. Eivind
          08.01.2016 22:37
          +9

          Нет ничего проще:

          #include <type_traits>
          #include <limits>
          
          template <class K, class N, bool, bool>
          struct bci_helper {
              static constexpr int value = 1;
          };
          
          template <class T, T K, T N>
          using bci = bci_helper<std::integral_constant<T,K>, std::integral_constant<T,N>, K!=N, K!=0>;
          
          template <class T, T K, T N>
          struct bci_helper<std::integral_constant<T,K>, std::integral_constant<T,N>, true, true> {
              static_assert(bci<T, K, N-1>::value <= std::numeric_limits<T>::max() - bci<T, K-1, N-1>::value,"Integer overflow");
              static constexpr T value = bci<T, K, N-1>::value + bci<T,K-1,N-1>::value;
          };
          


          1. semenyakinVS
            08.01.2016 23:04

            … и вот как-то так были устранены претензии к названию статьи


          1. Eivind
            09.01.2016 01:34
            +5

            Немного хардкора с длинной арифметикой
            #include <iostream>
            #include <type_traits>
            
            template <char ... Chars>
            struct number {};
            
            std::ostream& operator<< (std::ostream& os, number<>){
                return os;
            }
            
            template <char C, char ... Chars>
            std::ostream& operator<< (std::ostream& os, number<C, Chars...>){
                os << C << number<Chars...>{};
                return os;
            }
            
            template <class A, class B>
            struct append;
            
            template <char ... As, char ... Bs>
            struct append<number<As...>, number<Bs...>> {
                using type = number<As..., Bs...>;
            };
            
            template <class Source, class Result = number<>>
            struct reverse;
            
            template <char A, char ... As, char ... Bs>
            struct reverse<number<A,As...>, number<Bs...>> {
                using type = typename reverse<number<As...>,number<A,Bs...>>::type;
            };
            
            template <char ... Bs>
            struct reverse<number<>,number<Bs...>> {
                using type = number<Bs...>;
            };
            
            template <class T>
            struct remove_zeroes {
                using type = T;
            };
            
            template <>
            struct remove_zeroes<number<'0'>> {
                using type = number<'0'>;
            };
            
            template <char ... As>
            struct remove_zeroes<number<'0',As...>> {
                using type = typename remove_zeroes<number<As...>>::type;
            };
            
            template <char A, char B, char C>
            struct basic_sum {
                static constexpr char H = '0' + ((A-'0') + (B-'0') + (C -'0'))/10;
                static constexpr char L = '0' + ((A-'0') + (B-'0') + (C -'0'))%10;
            };
            
            template <class A, class B, char C>
            struct partial_sum;
            
            template <char A, char ... As, char B, char ... Bs, char C>
            struct partial_sum<number<A,As...>, number<B, Bs...>, C> {
                using L = number<basic_sum<A,B,C>::L>;
                using H = typename partial_sum<number<As...>,number<Bs...>,basic_sum<A,B,C>::H>::type;
                using type = typename append<L,H>::type;
            };
            template <char A, char ... As, char C>
            struct partial_sum<number<A,As...>,number<>,C> {
                using L = number<basic_sum<A,'0',C>::L>;
                using H = typename partial_sum<number<As...>,number<>,basic_sum<A,'0',C>::H>::type;
                using type = typename append<L,H>::type;
            };
            
            template <char B, char ... Bs, char C>
            struct partial_sum<number<>,number<B,Bs...>,C> {
                using L = number<basic_sum<'0',B,C>::L>;
                using H = typename partial_sum<number<>,number<Bs...>,basic_sum<B,'0',C>::H>::type;
                using type = typename append<L,H>::type;
            };
            
            template <char C>
            struct partial_sum<number<>,number<>,C> {
                using L = number<C>;
                using H = number<>;
                using type = typename append<L,H>::type;
            };
            
            template <class A, class B>
            using sum = typename remove_zeroes<typename reverse<typename partial_sum<typename reverse<A>::type,typename reverse<B>::type,'0'>::type>::type>::type;
            
            template <class>
            struct decrement_helper;
            
            template <char A, char... As>
            struct decrement_helper<number<A,As...>> {
                using L = number<(A=='0')?'9':(A-1)>;
                using H = typename std::conditional<A=='0',typename decrement_helper<number<As...>>::type,number<As...>>::type;
                using type = typename append<L,H>::type;
            };
            
            template <>
            struct decrement_helper<number<>> {
                using type = number<>;
            };
            
            template <class T>
            using decrement = typename remove_zeroes<typename reverse<typename decrement_helper<typename reverse<T>::type>::type>::type>::type;
            
            template <class K, class N>
            struct bcl_helper {
                using type = sum<typename bcl_helper<K,decrement<N>>::type,typename bcl_helper<decrement<K>,decrement<N>>::type>;
            };
            
            template <class N>
            struct bcl_helper<N,N> {
                using type = number<'1'>;
            };
            
            template <class N>
            struct bcl_helper<number<'0'>,N> {
                using type = number<'1'>;
            };
            
            template <typename CharT, CharT ...String> auto operator "" _n() {
                return number<String...>();
            }
            
            template <class K, class N>
            auto bcl(K,N) {
                return typename bcl_helper<K,N>::type();
            }
            
            int main(int /*argc*/, char** /*argv*/)
            {
                std::cout << bcl("50"_n,"100"_n) << std::endl;
            }
            


  1. encyclopedist
    08.01.2016 19:59

    Вот такая функция считает в 32 битных числах правильно до 33 включительно (проверяем, делится ли, и если да то сначала делим а потом умножаем):

    unsigned bci2(int n, int k) 
    {
        if (k>n/2) k=n-k; // возьмем минимальное из k, n-k.. В силу симметричность C(n,k)=C(n,n-k)
        if (k==1)  return n;
        if (k==0)  return 1;
        unsigned r=1;
        for (int i=1; i<=k;++i) {
            if(r % i == 0)
            {
        	    r/=i;
        	    r*=n-i+1;
            }
            else
            {
                r*=n-i+1;
                r/=i;
            }
        }
        return r;
    }
    


    1. zelserg
      08.01.2016 20:24
      +1

      Интересное замечание, но хочу заметить:
      1. Возможно, при каких-то k и n<=33 все же возникнет переполнение
      2. Граница отодвинулась, но для n=34 все же осталось переполнение.
      3. Переход на 64 бита кардинально отодвигает эту границу
      Можно, конечно, в числителе и знаменмтеле поискать общие множители и посокращать их, но я не стал двигаиться в этом напралении.


      1. encyclopedist
        08.01.2016 20:26

        Можно, конечно, в числителе и знаменмтеле поискать общие множители и посокращать их, но я не стал двигаиться в этом напралении.
        — См. обновление habrahabr.ru/post/274689/#comment_8729927


    1. encyclopedist
      08.01.2016 20:25

      И ещё лучший выриант (Live):

      template<typename T>
      T gcd(T a, T b)
      {
          while (b > 0)
          {
              a = a % b;
              std::swap(a, b);
          }
          return a;
      }
      
      template <typename T>
      T bci2(T n, T k) 
      {
          if (k > n/2)
              k = n - k;
          if (k == 1)
              return n;
          if (k == 0)
              return 1;
          T r = 1;
          for (T i = 1; i <= k; ++i) {
              T f = gcd(r, i);
              r /= f;
              r *= n - i + 1;
              r /= (i/f);
          }
          return r;
      }
      
      
      С unsigned int работает до 33, с unsigned long работает до 66.


      1. encyclopedist
        08.01.2016 20:40

        Обновление. Теперь точно все множители сокращает:

        template <typename T>
        T bci2(T n, T k) 
        {
            if (k > n/2)
                k = n - k;
            if (k == 1)
                return n;
            if (k == 0)
                return 1;
            T r = 1;
            for (T i = 1; i <= k; ++i) {
                T a = n - i + 1;
                T b = i;
                // compute r = r * a / b;
                T f = gcd(r, i);
                r /= f;
                b /= f;
                T f1 = gcd(a, b);
                r *= a/f1;
                r /= b/f1;
            }
            return r;
        }
        
        Работает до 34 и 67 соответственно.


      1. zelserg
        08.01.2016 20:46

        «Простая задача часто имеет простое и понятное решение. Увы, неправильное...» Как увидеть эту неправильность и довести до ума — вот в чем вопрос. Способы могут быть разными. У меня работает и при n=67 и не тратит время на вычисление НОДов.


        1. encyclopedist
          08.01.2016 20:57

          Все зависит от требований. У меня без доп памяти. С доп памятью надо ещё посмотреть что лучше (на вскидку и моё решение и динамическое программирование работают за O(n^2)).


          1. encyclopedist
            08.01.2016 21:09

            Моё даже лучше: gcd считается за логарифмическое время, поэтому O(n log n).


        1. encyclopedist
          09.01.2016 03:11
          +1

          Вот без НОДов (трюк r*a/b = (r/b)*a + (r%b)*a/b из этого комментария Mrrl ). Считает без переполнения пока это теоретически возможно. (на Coliru)

          Заголовок спойлера
          template <typename T>
          T bci2(T n, T k) 
          {
              if (k > n/2)
                  k = n - k;
              T r = 1;
              for (T i = 1; i <= k; ++i)
              {
                  // r * a / b = (r/b)*a + (r%b)*a/b
                  T a = n - i + 1;
                  r = (r/i)*a + ((r%i)*a)/i;
              }
              return r;
          }
          


          1. zelserg
            09.01.2016 08:48
            +2

            Спасибо за трюк и за ссылку. Надо же — перед написанием статьи искал на хабре «биномиальные», а надо было «мультиномиальные»


  1. a_batyr
    08.01.2016 23:47
    +1

    Естественно, пришлось использовать «длинную арифметику» (свою). Для этой задачи я написал «рекурсию с памятью»: вычисленные коэффициенты запоминались в массиве и экспоненциального роста времени расчета не было.
    Во-первых стоило сразу привести в статье длинную арифметику. Это грустно, когда оперируют с double при расчёте целочисленных значений
    return ceil(r-0.2); // округлим до ближайшего целого, отбросив дробную часть
    А во-вторых, можно привести вычисление коэффициентов без факториалов, а динамически оперируя только суммой.


    1. zelserg
      09.01.2016 00:20
      +1

      Напишите, как. Кроме приведенной в статье рекурсии, естественно.


      1. a_batyr
        09.01.2016 10:44
        +1

        Расчёт БК без умножения, динамически.
        Храним массив БК для элементов C(i,0), C(i,1), ..., C(i,i). Инициализируем для i=0 как {1}. Каждую следующую итерацию вычисляем через предыдущие значения C(i,k) = C(i-1,k)+C(i-1,k-1) вплоть до заданного C(n,k).
        Вы написали что-то похожее, только рекурсивно, что слишком прожорливо. Вам не нужно повторно вычислять промежуточные значения, а в случае рекурсии вы это делаете. Храня уже известные значения вы на порядок улучшите производительность.


        1. zelserg
          09.01.2016 12:09

          Я сделал добавление в статье — рекурсивная функция bcr с запоминанием.
          Она, конечно, самая быстрая, но значимого выигрыша в скорости для int и long long не дает. Значимого — т.е. чтобы bci/bcl, были узким местом в какой-то задаче, а новая bcr расшивала это узкое место.


          1. chersanya
            09.01.2016 18:42

            Странное у вас понимание «значимости» ускорения :) Если функция работает например в несколько раз быстрее — это уже значимое ускорение, хотя при правильном решении на олимпиаде это обычно не повлияет, т.к. таймлимит обычно ставится значительно больше необходимого. Измерили бы скорости всех вариантов да указали в статье, было бы интересно сравнить.


    1. zelserg
      09.01.2016 09:35
      +1

      «Во-первых стоило сразу привести в статье длинную арифметику. Это грустно, когда оперируют с double при расчёте целочисленных значений»
      Сначала о грусти. Для практических расчетов биномиальных коэффициентов (БК) double — самое простое и эффективное решение. Если вы моделируете случайные процессы, то точности в 16 знаков «за глаза» хватает и выборки из 1000 элементов, скорее всего, тоже хватит. Кроме того, БК в реальных расчетах живут не сами по себе, а умножаются на вероятности, которые сами по себе double. Грустно городить длинную арифметику для расчета БК, а потом умножать их на double.
      Длинная арифметика нужна при расчете БК только в одном случае — при решении олимпиадных задач и это тема для отдельной статьи (не моей). Я считаю, что каждый потенциальный олимпиец должен написать свою длинную арифметику, не такая уж она и сложная. А коль не может — значит олимпиады не для него.


      1. a_batyr
        09.01.2016 11:01

        Дальнейшее повышение точности и расчет при n>67
        … используем double ...
        Расхождение расчета bcd с точным значением начинается с n=57.
        Повысили точность?
        Использование double это костыль, который может быть оправдан лишь в некоторых случаях. Тогда вам стоило написать длинную арифметику и лишь вскольз упомянуть про double (типа если вам не нужна точность, используйте double). Но я хотел сказать, что не приведя решение с длинной арифметикой и динамикой вы не до конца раскрыли тему статьи. Вы приводите заголовок
        Для экстремалов и «олимпийцев»
        и совершенно не раскрываете тему статьи для олимпийцев. И уберите кавычки с этого слова ради бога, олимпийцы это живые люди!
        Что ж, некоторые вообще считают нормальным снимать вертикальное видео и я на это никак не могу повлиять :-(


        1. zelserg
          09.01.2016 13:28

          «И уберите кавычки с этого слова ради бога». Облимпийцы без кавычек — это Олимпийские боги (уж они то не одобрят убирание кавычек) в греческой мифологии и спортсмены-участники Олимпийских игр. К участникам прочих олимпиад слово олимпийцы не относится.

          В моей статье я сделал 2 небольших замечания для желающих заниматься олимпиадным программированием.
          1. Привел улучшенный вариант функции bcl, который правильно считает все БК, попадающие в unsigned long long
          2. Для тех, кому потребуется точное вычисления очень больших биномиальных коэффициентов («длинная арифметика»), посоветовал использовать рекурсивный расчет с запоминанием. Это все, я хотел сказать и сказал. Кто не понял — я не виноват.


  1. zelserg
    09.01.2016 00:18
    +1

    12 человек добавили в избранное, но только 2 плюсанули. Итог — минус 2. Пожалуй, не стоит писать подобные статьи (это был пробный камень. так сказать).


    1. samo-delkin
      09.01.2016 11:03

      Ну, вот ты написал

      		r=bcl(n-1,k);
      		r+=+bcl(n-1,k-1);
      

      это же можно в одну строку записать.
      То есть не знаешь ни C, ни C++. Алгоритм тоже неоптимальный.
      На олимпиады ссылаешься, а зачем они нужны? Пользы от них никакой нет.


      1. a_batyr
        09.01.2016 11:27
        +1

        Это тот случай когда в комментариях вопрос раскрыт намного лучше, чем в самой статье.


        1. zelserg
          09.01.2016 13:13

          Каждый видит и понимает «по способностям».


      1. zelserg
        09.01.2016 13:11

        В каждой статье есть шероховатости. Раздельный расчет нужен был для анализа, где переходить к рекурсивному расчету.

        Критиковать гораздо легче, чем написать свою статью. На любую, самую замечательныую статью найдутся критики и недоброжелатели. Не навится — не читайте и не комментируйте. Значит она не для вас. Кто-то комментирует, чтобы статья стала лучше (и я вношу исправления по ходу обсуждения), а кто-то с целью нагадить и выпятить свое Я.

        В данному моменту 26 человек поставили ее в избранное, значит посчитали ее интересной и/или полезной. Вот для таких она и написана. А любителям выискивать соринки в чужих глазах посоветую заняться своими.

        «На олимпиады ссылаешься, а зачем они нужны? Пользы от них никакой нет.» Вопрос спорный. Не смог решить задачу, тебя не взяли в команду и обида гложет?


        1. samo-delkin
          10.01.2016 03:44
          -1

          «На олимпиады ссылаешься, а зачем они нужны? Пользы от них никакой нет.» Вопрос спорный. Не смог решить задачу, тебя не взяли в команду и обида гложет?

          Не, я просто решал такие задачи и не получил от этого ничего. Зря время только потратил.
          Можно, конечно, сидеть и ждать, когда тебя в команду возьмут, а можно просто сидеть и делать программы, которые потом работают вместо тебя месяцами. (Хотя я пользуюсь уже несколько лет.)


  1. KvanTTT
    09.01.2016 02:23
    +1

    1. zelserg
      09.01.2016 09:08
      +1

      Хорошая статья, добавил ее в избранное. Метод рекурсивного расчета с запоминанием я когда-то делал и для int и для long long. Он быстрый, ест не так уж много памяти. Но в результате остановился на приведенной к конце статьи bcl. Запоминание осталось только для длинной арифметики, когда надо было посчитать очень много очень больших коэффицентов (да и операция деления в длинной арифмиетике — то еще «удовольствие»).

      Треугольник Паскаля интересная штука — очень быстрая, если запоминать промежуточные результаты и очень медленная, если не запоминать.


  1. middle
    09.01.2016 08:05

    А почему вы не считаете треугольник Паскаля? Простое быстрое итеративное решение.


    1. zelserg
      09.01.2016 08:43

      Что вы имеете в виду под «почему вы не считаете треугольник Паскаля»?

      Я написал в статье функцию bcr, поторая считает по треугольнику паскаля. И написал, что элемент из 34 строки считался около минуты. Также в конце статьи написал, что при расчете коэффициентов с длинной арифметикой использовал рекурсию (т.е. треугольник паскаля) с запоминанием посчитанных коэффициентов.


      1. middle
        09.01.2016 09:01

        Я имел в виду, почему вы не используете итеративный алгоритм построения треугольника Паскаля, храня строку в массиве? 70 строк у меня на ноуте считаются за долю секунды.

        Upd: и это при том, что я считаю каждый элемент отдельно — чтобы жизнь мёдом не казалась. А если печатать всю посчитанную строку целиком — будет вообще миллисекунды.


        1. zelserg
          09.01.2016 09:18

          Я писал процедуры расчета по треугольнику с запоминанием, но не стал приводить их в статье.
          1. Для int и long long заметной глазу разницы в скорости нет (ну не замечает мой глаз миллисекунды).
          2. Для олимпиадных задач, в которых надо было считать коэффициенты разницы тоже не было.
          3. Код с запоминанием не такой простой и чуть более объемный (зачем искать/приводить путь в два шага, если есть путь в один шаг).


          1. chersanya
            09.01.2016 10:14

            1. Для int и long long заметной глазу разницы в скорости нет (ну не замечает мой глаз миллисекунды).

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

            Странно видеть рядом с упоминанием олимпиадных задач оценку скорости на глаз :)

            3. Код с запоминанием не такой простой и чуть более объемный (зачем искать/приводить путь в два шага, если есть путь в один шаг)

            А по-моему, он почти не отличается от кода без запоминания, если будем хранить весь треугольник. Заодно автоматически получается кеширование — если несколько раз попросим одно и то же значение, оно не будет пересчитываться.

            Для сравнения — ваш код из статьи:

            unsigned bcr(int n,int k) 
            {
            	if (k>n/2) k=n-k;
            	if (k==1)  return n;
            	if (k==0)  return 1;
            	return bcr(n-1,k)+bcr(n-1,k-1);
            }
            


            И простейший вариант с запоминанием (не компилировал, но должно работать):
            unsigned bcr_cache[N_MAX][K_MAX] = {0};
            
            unsigned bcr(int n,int k) 
            {
            	if (k>n/2) k=n-k;
            	if (k==1)  return n;
            	if (k==0)  return 1;
                    if (bcr_cache[n][k] == 0)
                        bcr_cache[n][k] = bcr(n-1,k)+bcr(n-1,k-1);
                    return bcr_cache[n][k];
            }
            


            Кстати, в вашей строчке
            return ceil(r-0.2); // округлим до ближайшего целого, отбросив дробную часть
            

            неправильный либо код, либо комментарий.


            1. zelserg
              09.01.2016 10:51

              1. Я не сравниваю «на глаз» с олимпиадным программированианием. «На глаз» относится к замечанию «на моем ноутбуке», про миллисекунды — типа юмор. Про олимпиадное — то что тесты проходят и оптимизации скорости расчета БК не требуется.
              2. При запоминании фиксированные размеры мне как-то не очень… Но вариант вполне простой и рабочий.
              return ceil(r-0.2); // округлим до ближайшего целого, отбросив дробную часть
              неправильный либо код, либо комментарий.


              Комментарий мог быть таким: я сравнивал double с точными расчетами и «экспериментально» нашел, что это значение подходит (чтобы расхождение наступило как можно позже). Наверно, можно заменить на 0.4 или другую константу, но лучше не будет.

              Кстати, в статье есть еще одно «экспериментально найденная» константа: if(n+k)>90 для перехода к рекурсивной ветке.


              1. chersanya
                09.01.2016 11:09

                Ну фиксированные размеры я поставил только для простоты, и чтобы код оставался кодом на Си (у вас как я понял старательно не используется ничего из С++). Можно сделать вектором и добавлять туда по надобности, хотя раз N всё равно < 70, то можно независимо от задачи делать размер 70х70.

                С точки зрения производительности это будет намного быстрее, когда на нужно посчитать большое количество коэффициентов: они будут просто браться из таблицы. Это может пройти тесты и в какой-нибудь задаче, где ваше решение не проходит. Если вам такая не попалась, не значит что её не может быть.

                Комментарий в таком случае вводит в заблуждение — видя округление к ближайшему целому читатель ожидает ceil(r — 0.5) или floor(r + 0.5), или лучше всего round. И кстати не понятно, зачем это вообще — если N такие, что переполнения нет, то можно считать с int64. Если же N большие и мы используем double, то значит точность до единицы не нужна — она всё равно не будет получена при больших N.

                Кстати, если так убрать ваш ceil и возвращать просто r, то функцию можно сразу сделать шаблонной — чтобы не было такого, что «заменим unsigned на double», а одна функция работала и для int32, и для int64, и для double и для длинной арифметики.


                1. zelserg
                  09.01.2016 11:14
                  +1

                  Если просто возращать r, то у биномиальных коэффициентов появляются дробные хвосты и мне это не понравилось (все же БК — по определению целые).


                1. zelserg
                  09.01.2016 11:30
                  +1

                  Изменил комментарий в статье.


              1. chersanya
                09.01.2016 11:15

                И ещё, для как вы называете «олимпийцев» вычисление биномиальных коэффициентов неплохо написано на e-maxx.ru/algo/binomial_coeff, причём в разных вариантах.


                1. zelserg
                  09.01.2016 11:42
                  +1

                  Интересная стстья. При «длинных» расчетах запоминать последние две строки приходилсь, а вот запоминать факториалы — никогда. Как-то выкручивался или мне просто не попалась задача, где это необходимо


              1. zelserg
                09.01.2016 11:46

                Интересно, кто и за что заминусовал этот комментарий. Похоже, кто-то просто наставил минусов из желания нагадить. Ладно, они ему так или иначе вернутся.


          1. middle
            09.01.2016 10:47

            Ну хоть упомяньте их в статье, а то складывается впечатление, что либо быстро, но с переполнением, либо медленно, но без него. Разбирать задачу и даже не упомянуть характеристики столь важного варианта — как-то удивительно.

            Про int и long long я ничего не говорил :)


            1. middle
              09.01.2016 10:51

              Собственно, о сложности столь простого куска кода и рассуждать-то странно:

                // typedef uint32_t bino_int;
                // typedef uint64_t bino_int;
              
                buffer[0] = 1;
              
                for (size = 1; size <= n; ++size) {
                  bino_int prev = 1; // buffer[0]
                  for (size_t off = 1; off < size; ++off) {
                    bino_int tmp = buffer[off];
                    buffer[off] += prev;
                    prev = tmp;
                  }
                  buffer[size] = 1;
                }
              


            1. zelserg
              09.01.2016 11:12
              +1

              Написал добавление в статье


  1. cybrid
    09.01.2016 10:55
    +2

    Достаточно странная статья. Рассмотрены только «кривые» решения, а рекурсивное решение с запоминанием и прямое решение с подсчетом простых множителей не приведены.


    1. zelserg
      09.01.2016 11:34
      +1

      Привел рекурсивное с запоминанием, а подсчет множителей я не считаю «прямым».


  1. dprotopopov
    09.01.2016 13:40

    Мой ответ на Вашу публикацию habrahabr.ru/post/274729 — Расчет биномиальных коэффициентов с использованием Фурье-преобразований


    1. zelserg
      09.01.2016 14:32
      +1

      Давайте сравним скорость расчета C(67,33). Можете кинуть проект, который считает этот коэффицент и который можно открыть в VS2010?


    1. zelserg
      09.01.2016 18:33
      +1

      Или сами сравните. Полагаю, перевести bcr с запоминанием на c# несложно.


      1. dprotopopov
        09.01.2016 18:50

        взятое возвращаю кармой.
        Можете сравнить и описать на Хабре эксперимент для разных типов данных, для C++ и C# — многих заинтересует.


  1. Ryder95
    09.01.2016 16:21

    мне кажется, первый код работать не будет, так как переменная r в цикле инициализируется, то после цикла её нет, поэтому вернуть r невозможно


    1. zelserg
      09.01.2016 18:29
      +1

      Спасибо, поправил. Намудрил, когда переводил переделал факториал из рекурсии в цикл.


  1. ShashkovS
    09.01.2016 19:18
    +6

    Вообще говоря, совсем несложно сделать реализацию на основе разложения числа n! на простые множители (по известной формуле из википедии).

    Если известны ограничения на n, то простые числа можно захардкодить, иначе добыварь решетом Эратосфена.
    После этого останется только вычесть показатели степеней и перемножить. При этом гарантируется, что самое большое число, которое встретится в вычислениях — это ответ.

    Заодно такая техника позволит вычислять очень большие Cnk

    Вот код на Python:

    from timeit import timeit
    
    def pow_binomial(n, k):
        def eratosthenes(N):  # Возвращает список простых чисел от 2 до N
            simp = [2]  # Начинаем с 2
            nonsimp = set()  # Составные числа. Пока пусто
            for i in range(3, N + 1, 2):  # Проходим все нечётные числа
                if i not in nonsimp:  # Если оно не составное, значит, новое простое
                    nonsimp |= {j for j in range(i * i, N + 1, 2 * i)}  # Добавляем все кратные
                    simp.append(i)  # А само число в список простых
            return simp
        def calc_pow_in_factorial(a, p):  # Вычисляет кратность вхождения простого числа p в число a!
            res = 0
            while a:
                a //= p
                res += a
            return res
        # Не паримся и используем формулу n!/(k!(n-k)!)
        simple = eratosthenes(n+1)  # Берём список простых
        n_fact_pows = {p: calc_pow_in_factorial(n, p) for p in simple}
        k_fact_pows = {p: calc_pow_in_factorial(k, p) for p in simple}
        nmk_fact_pows = {p: calc_pow_in_factorial(n - k, p) for p in simple}
        ans = 1
        for p in simple:
            ans *= p ** (n_fact_pows[p] - k_fact_pows[p] - nmk_fact_pows[p])
        return ans
    
    
    def naive_factorial(n):
        res = 1
        for i in range(2, n + 1):
            res *= i
        return res
    
    
    def naive_binomial(n, k):
        return naive_factorial(n) // naive_factorial(k) // naive_factorial(n - k)
    
    
    REPEATS = 10
    N = 50000
    K = 10000
    setup = 'from __main__ import N, K, naive_binomial, pow_binomial'
    print(timeit(stmt='pow_binomial(N, K)', setup=setup, number=REPEATS)/REPEATS)
    print(timeit(stmt='naive_binomial(N, K)', setup=setup, number=REPEATS)/REPEATS)
    
    >>> 
    0.02103983737743497
    1.1220947057368764
    


    То есть C5000010000 вычисляется за 0.02 секунды, а там ещё есть, что ускорять.

    При n <= 71 если убрать комментарии и сократить сопли, получается 11 строк:
    def pow_binomial(n, k):
        def calc_pow(a, p):
            res = 0
            while a:
                a //= p
                res += a
            return res
        ans = 1
        for p in [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71]:
            ans *= p ** (calc_pow(n, p) - calc_pow(k, p) - calc_pow(n - k, p))
        return ans
    


  1. sci_nov
    10.01.2016 16:16

    Дальнейшее повышение точности и расчет при n>67

    Для экстремалов и «олимпийцев»


    www.artlebedev.ru/kovodstvo/sections/136
    Абзацы 07 и 08.