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