Skip to main content

nekolib/math/
dlog.rs

1//! 離散対数。
2
3use super::carmichael_lambda;
4use super::divisors;
5use super::factors;
6use super::gcd;
7use super::mod_pow;
8
9use std::collections::HashMap;
10
11use carmichael_lambda::CarmichaelLambda;
12use divisors::Divisors;
13use factors::Factors;
14use gcd::Gcd;
15use mod_pow::ModPow;
16
17/// 離散対数。
18///
19/// $n$ を法とする $\\mathrm{dlog}\_b(a)$。
20/// $b^z \\equiv a \\pmod{n}$ なる $z\\ge 0$ が存在すれば、そのうち最小のものを返す。
21/// $0\\le b, a\\lt n$ とする。
22/// また、$0^0 = 1$ とする。
23///
24/// コーナーケースに注意。
25/// $n=1$ なら $0^0=1\\equiv 0$ より $z=0$ を返す。
26/// 以下、$n\\gt 1$ とする。
27/// - $\\mathrm{dlog}_\\bullet(1) = 0$
28/// - $\\mathrm{dlog}_0(0) = 1$
29///
30/// 上記に該当しないとき、$\\mathrm{dlog}_0(\\bullet)$ と $\\mathrm{dlog}_1(\\bullet)$
31/// は存在しない。残りのケースについては、以下の方針に基づいて求める。
32///
33/// # Idea
34/// $1\\to b\\to b^2\\to\\dots\\pmod{n}$ でできる functional graph を考えると、$\\rho$
35/// の字状になっていることに気づく。しっぽの長さ $\\mu$ は高々 $\\log(n)$、頭の長さ
36/// $\\lambda$ は $\\varphi(n)$ の約数になっていることが示せる。
37///
38/// そこで、まずしっぽの部分に解があるかを愚直に $O(\\mu)$ 時間で調べる。
39/// 見つからなければ、頭の部分に解があるかを $o(\\lambda)$ 時間で調べる。
40/// $O(\\sqrt{\\lambda}\\log(\\lambda))$ 時間のアルゴリズムを紹介する。
41///
42/// $\\rho$ の頭の部分は $b^\\mu\\to b^{\\mu+1}\\to\\dots\\to b^{\\mu+\\lambda}\\equiv b^\\mu$
43/// である。このとき、${}^\\forall i\\ge\\mu$ について
44/// $b^{i+1}\\cdot b^{\\lambda-1} \\equiv b^i \\pmod{n}$ が成り立つ。
45/// すなわち、$b^{\\lambda-1}$ を掛けることで、$\\to$ をひとつ戻ることができる[^1]。
46///
47/// [^1]: $\\mu\\gt 0$ なら、 $b^\\mu$ は $b^{\\mu-1}$ と $b^{\\mu+\\lambda-1}$
48/// ($\\neq b^{\\mu-1}$) のふたつから $\\to$ が入ってくるが、$\\rho$
49/// の頭に含まれる後者に戻る。
50///
51/// また、頭の部分に $a$ が含まれるためには $\\gcd(b^\\mu, n)=\\gcd(a, n)$
52/// が必要であり、これが成り立たなければ解なしを報告すればよい。
53/// よって、以下ではこれが成り立つとする。
54///
55/// $z = \\mu+i\\cdot\\lfloor\\sqrt{\\lambda}\\rfloor+j$ ($0\\le j\\lt\\sqrt{\\lambda}$)
56/// とし、$(i, j)$ を探すことを考える。
57/// $$ \\begin{aligned}
58/// b^{\\mu+i\\cdot\\lfloor\\sqrt{\\lambda}\\rfloor+j} &\\equiv a \\\\
59/// b^{\\mu+i\\cdot\\lfloor\\sqrt{\\lambda}\\rfloor+j}\\cdot (b^{\\lambda-1})^j
60///  &\\equiv a\\cdot (b^{\\lambda-1})^j \\\\
61/// b^{\\mu+i\\cdot\\lfloor\\sqrt{\\lambda}\\rfloor}
62///  &\\equiv a\\cdot (b^{\\lambda-1})^j \\\\
63/// \\end{aligned} $$
64/// これより、$i$ と $j$ を分離でき、$a\\cdot(b^{\\lambda-1})^j\\mapsto j$
65/// の連想配列を作っておくことで、$b^{\\mu+i\\cdot\\lfloor\\sqrt{\\lambda}\\rfloor}$
66/// に対応する要素があれば返せる。ただし、複数の $j$ について
67/// $a\\cdot(b^{\\lambda-1})^j$ が同じ値を取りうるので注意する必要がある。
68/// $j$ による小さい幅で連想配列を作り、$i$ による大きい幅でそれにアクセスする様子から
69/// baby-step giant-step algorithm と呼ばれている。
70///
71/// ## About $\\mu$ and $\\lambda$
72///
73/// $\\mu$ と $\\lambda$ を求める方法について述べる。
74/// 自然数 $x$ が素数 $p$ で割り切れる回数、すなわち $x$ の素因数分解における $p$
75/// の指数を $\\pi\_p(x)$ で表すとする[^2]。
76/// $b = \\prod\_{p\_i:\\text{ prime}} p\_i^{e\_i}$ とすると、次が成り立つ。
77/// $$ \\mu = \\max\_i \\left\\lceil\\frac{\\pi\_{p\_i}(n)}{e\_i}\\right\\rceil. $$
78/// また、$p = \\prod\_i p\_i^{\\pi\_{p\_i}(n)}$, $n\' = n / p$ とする。すなわち、$n$
79/// を $b$ の各素因数で割り切れるだけ割ったものを $n\'$ とする。このとき、$\\lambda$
80/// は $\\varphi(n\')$ の約数[^3]であり、次の式に基づき愚直に求める[^4]。
81/// $$ \\lambda = \\min\_{i\\sqsubseteq \\varphi(n\')} i\\quad\\text{s.t.}\\quad
82/// b^\\mu\\cdot b^i \\equiv b^\\mu \\pmod{n}.  $$
83/// $\\varphi(n\')$ の約数の個数は $o(\\sqrt{n}/\\log(n))$
84/// なので、繰り返し二乗法を用いて $O(\\sqrt{n})$ 時間でできる。
85/// 実際には、解が見つかった時点で打ち切れるので、$O(\\lambda + \\sum\_{i\\sqsubseteq
86/// \\varphi(n\')}^{i\\le\\lambda} \\log(i))$ 時間になる。前半の $\\lambda$
87/// は試し割り法に由来する項なので、約数列挙を $O(1)$ delay でできるなら消去できる。
88///
89/// [^2]: 標準的な記法があったら知りたいです。
90///
91/// [^3]: $\\varphi(n)$ と $n\'$ の定義から、$\\varphi(n\')$ は $\\varphi(n)$ の約数であることに注意。
92///
93/// [^4]: $x\\sqsubseteq y$ で $x$ が $y$ を割り切ることを表す。$x\\mid y$
94/// は方向がわかりにくくて嫌い。素因数の多重集合の包含関係のつもり。$\\subseteq$ でもいいかも?
95///
96/// ## Proof
97/// まず、$\\lambda$ が $\\varphi(n\')$ の約数になることを示す。
98/// $b^\\mu$ における $p\_i$ の指数は $e\_i\\cdot\\max\_j \\lceil\\pi\_{p\_j}(n)/e\_j\\rceil$
99/// であり、特に $e\_i\\cdot \\lceil\\pi\_{p\_i}(n)/e\_i\\rceil \\ge \\pi\_{p\_i}(n)$ 以上である。
100/// よって、$b^\\mu$ は $p$ で割り切れ、$b^\\mu \\equiv 0\\pmod{p}$ である。これより、$b^{\\mu+i}
101/// \\equiv 0\\pmod{p}$ ($i\\ge 0$) である。
102///
103/// また、$b$ と $n\'$ は互いに素なので、Euler の定理より $b^{\\varphi(n\')} \\equiv 1\\pmod{n\'}$
104/// が成り立つ。よって、$b^{\\mu+\\varphi(n\')}\\equiv b^\\mu \\pmod{n\'}$
105/// である。上記の $p$ に関する結果と合わせて $b^{\\mu+\\varphi(n\')}\\equiv b^\\mu \\pmod{n}$
106/// を得る。よって、$\\lambda$ が $\\varphi(n\')$ の約数であることが示された。
107///
108/// 次に、$\\mu$ の値が正しいことを示す。$b^\\mu$ が頭の部分に含まれることは上で示した
109/// $b^{\\mu+\\varphi(n\')}\\equiv b^\\mu\\pmod{n}$ より従うので、$\\mu\\gt 0$ のとき
110/// $b^{\\mu-1}$ が頭の部分に含まれないことを示せば十分である。
111/// $\\mu$ の定義より、ある $i$ に対して $e\_i\\cdot(\\mu-1)\\lt \\pi\_{p\_i}(n)$ が成り立つ。
112/// よって $b^{\\mu-1}$ は $p$ は割り切れず、$b^{\\mu-1} \\not\\equiv 0\\pmod{n}$ である。
113/// これにより、頭の部分に含まれるどの要素とも等しくないことがわかる。
114///
115/// 最後に、$\\gcd(b^\\mu, n)=\\gcd(a, n)$ であることが、頭の部分に $a$ が存在する、すなわち
116/// ${}^\\exists i\\ge 0: b^{\\mu+i} \\equiv a\\pmod{n}$ であることの必要条件であることを示す。
117///
118/// - $\\gcd(b^{\\mu+i})\\equiv 0\\pmod{p}$ for $i\\ge 0$
119/// - $\\gcd(b, n\') = 1$ より $\\gcd(b\^{\\mu+i}, n\') = 1$ for $i\\ge 0$
120/// - $\\gcd(p, n\') = 1$
121/// - $n=p\\cdot n\'$
122///
123/// より、$\\gcd(b^{\\mu+i}, n) = p$ ($i\\ge 0$) で一定となる。
124/// すなわち、$\\gcd(a, n) = p$ が必要となる。
125///
126/// # Complexity
127/// $O(\\sqrt{n} + \\sqrt{\\lambda}\\cdot H(\\sqrt{\\lambda}))$ time.
128///
129/// ただし、$H(n)$ は要素数 $n$ の [`HashMap`] の [`insert`] と [`get`] にかかる時間とする。
130/// BS/GS パートでは [`BTreeMap`] を用いるよりも [`HashMap`]
131/// を用いた方が高速だったので、とりあえずそうした。
132///
133/// [`BTreeMap`]: https://doc.rust-lang.org/std/collections/struct.BTreeMap.html
134///
135/// [`HashMap`]: https://doc.rust-lang.org/std/collections/struct.HashMap.html
136/// [`insert`]: https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.insert
137/// [`get`]: https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.get
138///
139/// ここで、$\\lambda$ は最悪 $\\Theta(n)$ である。
140/// また、篩の前計算などで $n$, $b$ の素因数分解と $\\varphi(n)$ の約数列挙を $O(1)$ delay
141/// でできるなら $\\sqrt{n}$ の項をそれらの個数に落とせる。
142///
143/// # Implementation notes
144/// $\\lambda$ を求める際に $b^{\\mu+i}\\equiv b^\\mu$ なる最小の $i\\sqsubseteq \\varphi(n\')$
145/// を探したが、実際には Euler の定理ではなく Carmichael の定理に基づいて
146/// [$\\lambda(n\')$][`carmichael_lambda`]
147/// の約数を探せばよい。関数値を求めるの自体は $\\varphi(\\bullet)$ よりも $\\lambda(\\bullet)$
148/// の方が若干複雑だが、全体では高速に動作したので後者を選択した。
149///
150/// [`carmichael_lambda`]: fn.carmichael_lambda.html
151///
152/// # Examples
153/// ```
154/// use nekolib::math::DLog;
155///
156/// assert_eq!(6_u64.dlog(5, 13), Some(9));
157/// assert_eq!(27_u64.dlog(3, 30), Some(3));
158/// assert_eq!(2_u64.dlog(0, 4), Some(2));
159/// assert_eq!(0_u64.dlog(1, 2), Some(0));
160/// assert_eq!(2_u64.dlog(3, 10), None);
161/// ```
162///
163/// # References
164/// - <https://divinejk.hatenablog.com/entry/2021/02/07/133155>
165///
166/// # Suggestions
167/// 内部で ~~Euler の $\\varphi$ 関数~~ Carmichael の $\\lambda$
168/// 関数と約数列挙と素因数分解が必要となるので、
169/// 篩などを用いて高速化が図れる場合に対応しやすいようにしてみるとよい?
170///
171/// ```ignore
172/// pub fn dlog<I, J>(
173///     b: u64, a: u64, n: u64,
174///     // euler_phi: impl Fn(u64) -> u64,
175///     carmichael_lambda: impl Fn(u64) -> u64,
176///     divisors: impl Fn(u64) -> I,
177///     factors: impl Fn(u64) -> J,
178/// ) -> Option<u64>
179/// where
180///     I: Iterator<Item = u64>,
181///     J: Iterator<Item = (u64, u32)>,
182/// { ... }
183/// ```
184///
185/// 一方で、そうでない場合にわざわざ関数を渡す必要があるので単に面倒かも。
186/// お手軽パターンと二つ用意するといいかも?(デフォルト引数欲しいね)
187///
188/// とはいえ、結局 BS/GS パートが律速になってしまいそう。
189///
190/// # Naming
191/// 関数名は *d*iscrete *log*arithm、引数 $b$ は *b*ase、$a$ は *a*ntilogarithm から。
192pub trait DLog: Sized {
193    fn dlog(self, a: Self, n: Self) -> Option<Self>;
194}
195
196trait DLogInternal: Sized {
197    fn bs_gs(self, bb: Self, a: Self, n: Self, c: Self) -> Option<Self>;
198}
199
200macro_rules! impl_uint {
201    ($t:ty) => {
202        impl DLog for $t {
203            fn dlog(self, a: Self, n: Self) -> Option<Self> {
204                let b = match (self, a, n) {
205                    (_, _, 0) => panic!("modulo must be positive"),
206                    (_, _, 1) => return Some(0),
207                    (_, 1, _) => return Some(0),
208                    (0, 0, _) => return Some(1),
209                    (0, _, _) => return None,
210                    (1, _, _) => return None,
211                    _ => self,
212                };
213
214                let mut n_ = n;
215                let tail = b.factors()
216                    .map(|(p, e)| {
217                        let e = e as $t;
218                        let mut f = 0;
219                        while n_ % p == 0 {
220                            n_ /= p;
221                            f += 1;
222                        }
223                        (f + e - 1) / e
224                    }).max().unwrap();
225
226                let mut bpow = 1;
227                for i in 0..tail {
228                    if bpow == a {
229                        return Some(i);
230                    }
231                    bpow = bpow * b % n;
232                }
233
234                let bb = bpow;
235                if a == 0 {
236                    return (bb == 0).then(|| tail);
237                }
238                if n != n_ * a.gcd(n) {
239                    return None;
240                }
241
242                let c = n_
243                    .carmichael_lambda()
244                    .divisors()
245                    .find(|&c| (bb * b.mod_pow(c, n)) % n == bb)
246                    .unwrap();
247
248                b.bs_gs(bb, a, n, c).map(|head| tail + head)
249            }
250        }
251        impl DLogInternal for $t {
252            fn bs_gs(self, bb: Self, a: Self, n: Self, c: Self) -> Option<Self> {
253                let b = self;
254                let step = (1..).find(|&i| i * i * 2 >= c).unwrap();
255                let seen = {
256                    let mut seen = HashMap::new();
257                    let baby_recip = b.mod_pow(c - 1, n);
258                    let mut x = a;
259                    for i in 0..step {
260                        seen.insert(x, i);
261                        x = (x * baby_recip) % n;
262                    }
263                    seen
264                };
265                let giant = b.mod_pow(step, n);
266                let mut x = bb;
267                for i in 0..=c / step {
268                    if let Some(&e) = seen.get(&x) {
269                        return Some(i * step + e);
270                    }
271                    x = (x * giant) % n;
272                }
273                None
274            }
275        }
276    };
277    ( $($t:ty)* ) => { $(impl_uint!($t);)* };
278}
279
280impl_uint!(u8 u16 u32 u64 u128 usize);
281
282#[test]
283fn test() {
284    use std::collections::hash_map::Entry::{Occupied, Vacant};
285    let n_max = 200_u64;
286
287    for n in 1..=n_max {
288        for b in 0..n {
289            let expected = {
290                let mut expected = HashMap::new();
291                let mut x = 1 % n;
292                for i in 0.. {
293                    match expected.entry(x) {
294                        Vacant(v) => v.insert(i),
295                        Occupied(_) => break,
296                    };
297                    x = x * b % n;
298                }
299                expected
300            };
301
302            for a in 0..n {
303                let z = b.dlog(a, n);
304                assert_eq!(z, expected.get(&a).cloned());
305            }
306        }
307    }
308}