modular_binomial_small_k_faster.pl 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. #!/usr/bin/perl
  2. # Author: Trizen
  3. # Date: 27 April 2022
  4. # https://github.com/trizen
  5. # A decently efficient algorithm for computing `binomial(n, k) mod m`, where `k` is small (<~ 10^6).
  6. # Implemented using the identity:
  7. # binomial(n, k) = Product_{r = n-k+1..n}(r) / k!
  8. # And also using the identitiy:
  9. # binomial(n, k) = Prod_{j=0..k-1} (n-j)/(j+1)
  10. # See also:
  11. # https://en.wikipedia.org/wiki/Lucas%27s_theorem
  12. use 5.020;
  13. use strict;
  14. use warnings;
  15. use ntheory qw(:all);
  16. use List::Util qw(uniq);
  17. use experimental qw(signatures);
  18. sub factorial_power ($n, $p) {
  19. divint($n - vecsum(todigits($n, $p)), $p - 1);
  20. }
  21. sub modular_binomial_small_k ($n, $k, $m) {
  22. my %kp;
  23. my $prod = 1;
  24. if ($n - $k < $k) {
  25. $k = $n - $k;
  26. }
  27. if (is_prime($m)) {
  28. foreach my $j (0 .. $k - 1) {
  29. $prod = mulmod($prod, $n - $j, $m);
  30. $prod = divmod($prod, $j + 1, $m);
  31. }
  32. return $prod;
  33. }
  34. forfactored {
  35. my $r = $_;
  36. my @factors = uniq(@_);
  37. foreach my $p (@factors) {
  38. if ($p <= $k) {
  39. next if ((my $t = ($kp{$p} //= factorial_power($k, $p))) == 0);
  40. my $v = valuation($r, $p);
  41. if ($v >= $t) {
  42. $v = $t;
  43. $kp{$p} = 0;
  44. }
  45. else {
  46. $kp{$p} -= $v;
  47. }
  48. $r = divint($r, powint($p, $v));
  49. last if ($r == 1);
  50. }
  51. else {
  52. last;
  53. }
  54. }
  55. $prod = mulmod($prod, $r, $m);
  56. } $n - $k + 1, $n;
  57. return $prod;
  58. }
  59. sub lucas_theorem ($n, $k, $p) {
  60. if ($n < $k) {
  61. return 0;
  62. }
  63. my $res = 1;
  64. while ($k > 0) {
  65. my ($Nr, $Kr) = (modint($n, $p), modint($k, $p));
  66. if ($Nr < $Kr) {
  67. return 0;
  68. }
  69. ($n, $k) = (divint($n, $p), divint($k, $p));
  70. $res = mulmod($res, modular_binomial_small_k($Nr, $Kr, $p), $p);
  71. }
  72. return $res;
  73. }
  74. sub modular_binomial ($n, $k, $m) {
  75. if ($m == 0) {
  76. return undef;
  77. }
  78. if ($m == 1) {
  79. return 0;
  80. }
  81. if ($k < 0) {
  82. $k = subint($n, $k);
  83. }
  84. if ($k < 0) {
  85. return 0;
  86. }
  87. if ($n < 0) {
  88. return modint(mulint(powint(-1, $k), __SUB__->(subint($k, $n) - 1, $k, $m)), $m);
  89. }
  90. if ($k > $n) {
  91. return 0;
  92. }
  93. if ($k == 0 or $k == $n) {
  94. return modint(1, $m);
  95. }
  96. if ($n - $k < $k) {
  97. $k = $n - $k;
  98. }
  99. is_square_free(absint($m))
  100. || return modint(modular_binomial_small_k($n, $k, absint($m)), $m);
  101. my @congruences;
  102. foreach my $pp (factor_exp(absint($m))) {
  103. my ($p, $e) = @$pp;
  104. my $pk = powint($p, $e);
  105. if ($e == 1) {
  106. push @congruences, [lucas_theorem($n, $k, $p), $p];
  107. }
  108. else {
  109. push @congruences, [modular_binomial_small_k($n, $k, $pk), $pk];
  110. }
  111. }
  112. modint(chinese(@congruences), $m);
  113. }
  114. say modular_binomial(12, 5, 100000); #=> 792
  115. say modular_binomial(16, 4, 100000); #=> 1820
  116. say modular_binomial(100, 50, 139); #=> 71
  117. say modular_binomial(1000, 10, 1243); #=> 848
  118. say modular_binomial(124, 42, 1234567); #=> 395154
  119. say modular_binomial(1e9, 1e4, 1234567); #=> 833120
  120. say modular_binomial(1e10, 1e5, 1234567); #=> 589372
  121. say modular_binomial(1e10, 1e6, 1234567); #=> 456887
  122. say modular_binomial(1e9, 1e4, 123456791); #=> 106271399
  123. say modular_binomial(1e10, 1e5, 123456791); #=> 20609240
  124. __END__
  125. use Test::More tests => 8820;
  126. my $upto = 10;
  127. foreach my $n (-$upto .. $upto) {
  128. foreach my $k (-$upto .. $upto) {
  129. foreach my $m (-$upto .. $upto) {
  130. next if ($m == 0);
  131. say "Testing: binomial($n, $k, $m)";
  132. is(modular_binomial($n, $k, $m), binomial($n, $k) % $m);
  133. }
  134. }
  135. }