Trait nekolib::math::DLog

source ·
pub trait DLog: Sized {
    // Required method
    fn dlog(self, a: Self, n: Self) -> Option<Self>;
}
Expand description

離散対数。

nn を法とする dlogb(a)\mathrm{dlog}_b(a)bza(modn)b^z \equiv a \pmod{n} なる z0z\ge 0 が存在すれば、そのうち最小のものを返す。 0b,a<n0\le b, a\lt n とする。 また、00=10^0 = 1 とする。

コーナーケースに注意。 n=1n=1 なら 00=100^0=1\equiv 0 より z=0z=0 を返す。 以下、n>1n\gt 1 とする。

  • dlog(1)=0\mathrm{dlog}_\bullet(1) = 0
  • dlog0(0)=1\mathrm{dlog}_0(0) = 1

上記に該当しないとき、dlog0()\mathrm{dlog}_0(\bullet)dlog1()\mathrm{dlog}_1(\bullet) は存在しない。残りのケースについては、以下の方針に基づいて求める。

Idea

1bb2(modn)1\to b\to b^2\to\dots\pmod{n} でできる functional graph を考えると、ρ\rho の字状になっていることに気づく。しっぽの長さ μ\mu は高々 log(n)\log(n)、頭の長さ λ\lambdaφ(n)\varphi(n) の約数になっていることが示せる。

そこで、まずしっぽの部分に解があるかを愚直に O(μ)O(\mu) 時間で調べる。 見つからなければ、頭の部分に解があるかを o(λ)o(\lambda) 時間で調べる。 O(λlog(λ))O(\sqrt{\lambda}\log(\lambda)) 時間のアルゴリズムを紹介する。

ρ\rho の頭の部分は bμbμ+1bμ+λbμb^\mu\to b^{\mu+1}\to\dots\to b^{\mu+\lambda}\equiv b^\mu である。このとき、iμ{}^\forall i\ge\mu について bi+1bλ1bi(modn)b^{i+1}\cdot b^{\lambda-1} \equiv b^i \pmod{n} が成り立つ。 すなわち、bλ1b^{\lambda-1} を掛けることで、\to をひとつ戻ることができる1

また、頭の部分に aa が含まれるためには gcd(bμ,n)=gcd(a,n)\gcd(b^\mu, n)=\gcd(a, n) が必要であり、これが成り立たなければ解なしを報告すればよい。 よって、以下ではこれが成り立つとする。

z=μ+iλ+jz = \mu+i\cdot\lfloor\sqrt{\lambda}\rfloor+j (0j<λ0\le j\lt\sqrt{\lambda}) とし、(i,j)(i, j) を探すことを考える。 bμ+iλ+jabμ+iλ+j(bλ1)ja(bλ1)jbμ+iλa(bλ1)j \begin{aligned} b^{\mu+i\cdot\lfloor\sqrt{\lambda}\rfloor+j} &\equiv a \\ b^{\mu+i\cdot\lfloor\sqrt{\lambda}\rfloor+j}\cdot (b^{\lambda-1})^j &\equiv a\cdot (b^{\lambda-1})^j \\ b^{\mu+i\cdot\lfloor\sqrt{\lambda}\rfloor} &\equiv a\cdot (b^{\lambda-1})^j \\ \end{aligned} これより、iijj を分離でき、a(bλ1)jja\cdot(b^{\lambda-1})^j\mapsto j の連想配列を作っておくことで、bμ+iλb^{\mu+i\cdot\lfloor\sqrt{\lambda}\rfloor} に対応する要素があれば返せる。ただし、複数の jj について a(bλ1)ja\cdot(b^{\lambda-1})^j が同じ値を取りうるので注意する必要がある。 jj による小さい幅で連想配列を作り、ii による大きい幅でそれにアクセスする様子から baby-step giant-step algorithm と呼ばれている。

About μ\mu and λ\lambda

μ\muλ\lambda を求める方法について述べる。 自然数 xx が素数 pp で割り切れる回数、すなわち xx の素因数分解における pp の指数を πp(x)\pi_p(x) で表すとする2b=pi: primepieib = \prod_{p_i:\text{ prime}} p_i^{e_i} とすると、次が成り立つ。 μ=maxiπpi(n)ei. \mu = \max_i \left\lceil\frac{\pi_{p_i}(n)}{e_i}\right\rceil. また、p=ipiπpi(n)p = \prod_i p_i^{\pi_{p_i}(n)}, n=n/pn' = n / p とする。すなわち、nnbb の各素因数で割り切れるだけ割ったものを nn' とする。このとき、λ\lambdaφ(n)\varphi(n') の約数3であり、次の式に基づき愚直に求める4λ=miniφ(n)is.t.bμbibμ(modn). \lambda = \min_{i\sqsubseteq \varphi(n')} i\quad\text{s.t.}\quad b^\mu\cdot b^i \equiv b^\mu \pmod{n}. φ(n)\varphi(n') の約数の個数は o(n/log(n))o(\sqrt{n}/\log(n)) なので、繰り返し二乗法を用いて O(n)O(\sqrt{n}) 時間でできる。 実際には、解が見つかった時点で打ち切れるので、O(λ+iφ(n)iλlog(i))O(\lambda + \sum_{i\sqsubseteq \varphi(n')}^{i\le\lambda} \log(i)) 時間になる。前半の λ\lambda は試し割り法に由来する項なので、約数列挙を O(1)O(1) delay でできるなら消去できる。

Proof

まず、λ\lambdaφ(n)\varphi(n') の約数になることを示す。 bμb^\mu における pip_i の指数は eimaxjπpj(n)/eje_i\cdot\max_j \lceil\pi_{p_j}(n)/e_j\rceil であり、特に eiπpi(n)/eiπpi(n)e_i\cdot \lceil\pi_{p_i}(n)/e_i\rceil \ge \pi_{p_i}(n) 以上である。 よって、bμb^\mupp で割り切れ、bμ0(modp)b^\mu \equiv 0\pmod{p} である。これより、bμ+i0(modp)b^{\mu+i} \equiv 0\pmod{p} (i0i\ge 0) である。

また、bbnn' は互いに素なので、Euler の定理より bφ(n)1(modn)b^{\varphi(n')} \equiv 1\pmod{n'} が成り立つ。よって、bμ+φ(n)bμ(modn)b^{\mu+\varphi(n')}\equiv b^\mu \pmod{n'} である。上記の pp に関する結果と合わせて bμ+φ(n)bμ(modn)b^{\mu+\varphi(n')}\equiv b^\mu \pmod{n} を得る。よって、λ\lambdaφ(n)\varphi(n') の約数であることが示された。

次に、μ\mu の値が正しいことを示す。bμb^\mu が頭の部分に含まれることは上で示した bμ+φ(n)bμ(modn)b^{\mu+\varphi(n')}\equiv b^\mu\pmod{n} より従うので、μ>0\mu\gt 0 のとき bμ1b^{\mu-1} が頭の部分に含まれないことを示せば十分である。 μ\mu の定義より、ある ii に対して ei(μ1)<πpi(n)e_i\cdot(\mu-1)\lt \pi_{p_i}(n) が成り立つ。 よって bμ1b^{\mu-1}pp は割り切れず、bμ1≢0(modn)b^{\mu-1} \not\equiv 0\pmod{n} である。 これにより、頭の部分に含まれるどの要素とも等しくないことがわかる。

最後に、gcd(bμ,n)=gcd(a,n)\gcd(b^\mu, n)=\gcd(a, n) であることが、頭の部分に aa が存在する、すなわち i0:bμ+ia(modn){}^\exists i\ge 0: b^{\mu+i} \equiv a\pmod{n} であることの必要条件であることを示す。

  • gcd(bμ+i)0(modp)\gcd(b^{\mu+i})\equiv 0\pmod{p} for i0i\ge 0
  • gcd(b,n)=1\gcd(b, n') = 1 より gcd(bμ+i,n)=1\gcd(b^{\mu+i}, n') = 1 for i0i\ge 0
  • gcd(p,n)=1\gcd(p, n') = 1
  • n=pnn=p\cdot n'

より、gcd(bμ+i,n)=p\gcd(b^{\mu+i}, n) = p (i0i\ge 0) で一定となる。 すなわち、gcd(a,n)=p\gcd(a, n) = p が必要となる。

Complexity

O(n+λH(λ))O(\sqrt{n} + \sqrt{\lambda}\cdot H(\sqrt{\lambda})) time.

ただし、H(n)H(n) は要素数 nnHashMapinsertget にかかる時間とする。 BS/GS パートでは BTreeMap を用いるよりも HashMap を用いた方が高速だったので、とりあえずそうした。

ここで、λ\lambda は最悪 Θ(n)\Theta(n) である。 また、篩の前計算などで nn, bb の素因数分解と φ(n)\varphi(n) の約数列挙を O(1)O(1) delay でできるなら n\sqrt{n} の項をそれらの個数に落とせる。

Implementation notes

λ\lambda を求める際に bμ+ibμb^{\mu+i}\equiv b^\mu なる最小の iφ(n)i\sqsubseteq \varphi(n') を探したが、実際には Euler の定理ではなく Carmichael の定理に基づいて λ(n)\lambda(n') の約数を探せばよい。関数値を求めるの自体は φ()\varphi(\bullet) よりも λ()\lambda(\bullet) の方が若干複雑だが、全体では高速に動作したので後者を選択した。

Examples

use nekolib::math::DLog;

assert_eq!(6_u64.dlog(5, 13), Some(9));
assert_eq!(27_u64.dlog(3, 30), Some(3));
assert_eq!(2_u64.dlog(0, 4), Some(2));
assert_eq!(0_u64.dlog(1, 2), Some(0));
assert_eq!(2_u64.dlog(3, 10), None);

References

Suggestions

内部で Euler の φ\varphi 関数 Carmichael の λ\lambda 関数と約数列挙と素因数分解が必要となるので、 篩などを用いて高速化が図れる場合に対応しやすいようにしてみるとよい?

pub fn dlog<I, J>(
    b: u64, a: u64, n: u64,
    // euler_phi: impl Fn(u64) -> u64,
    carmichael_lambda: impl Fn(u64) -> u64,
    divisors: impl Fn(u64) -> I,
    factors: impl Fn(u64) -> J,
) -> Option<u64>
where
    I: Iterator<Item = u64>,
    J: Iterator<Item = (u64, u32)>,
{ ... }

一方で、そうでない場合にわざわざ関数を渡す必要があるので単に面倒かも。 お手軽パターンと二つ用意するといいかも?(デフォルト引数欲しいね)

とはいえ、結局 BS/GS パートが律速になってしまいそう。

Naming

関数名は discrete logarithm、引数 bbbase、aaantilogarithm から。


  1. μ>0\mu\gt 0 なら、 bμb^\mubμ1b^{\mu-1}bμ+λ1b^{\mu+\lambda-1} (bμ1\neq b^{\mu-1}) のふたつから \to が入ってくるが、ρ\rho の頭に含まれる後者に戻る。 

  2. 標準的な記法があったら知りたいです。 

  3. φ(n)\varphi(n)nn' の定義から、φ(n)\varphi(n')φ(n)\varphi(n) の約数であることに注意。 

  4. xyx\sqsubseteq yxxyy を割り切ることを表す。xyx\mid y は方向がわかりにくくて嫌い。素因数の多重集合の包含関係のつもり。\subseteq でもいいかも? 

Required Methods§

source

fn dlog(self, a: Self, n: Self) -> Option<Self>

Implementations on Foreign Types§

source§

impl DLog for u32

source§

fn dlog(self, a: Self, n: Self) -> Option<Self>

source§

impl DLog for u8

source§

fn dlog(self, a: Self, n: Self) -> Option<Self>

source§

impl DLog for usize

source§

fn dlog(self, a: Self, n: Self) -> Option<Self>

source§

impl DLog for u128

source§

fn dlog(self, a: Self, n: Self) -> Option<Self>

source§

impl DLog for u64

source§

fn dlog(self, a: Self, n: Self) -> Option<Self>

source§

impl DLog for u16

source§

fn dlog(self, a: Self, n: Self) -> Option<Self>

Implementors§