えびちゃんの日記

えびちゃん(競プロ)の日記です。

n 番目の素数を求めるよ

軽めの記事です。

\(n\) 番目の素数を \(p_n\)、\(n\) 以下の素数を \(\pi(n)\) と書くとします。 このとき、\(p_n\) は \(\pi(p_n) \ge n\) かつ \(\pi(p_n-1) \lt n\) を満たします。 よって、これは二分探索で求められます。

上限を決めるのが大変そうですが、指数探索っぽくすればいいです。 指数探索というのは、上限を \(2, 4, 8, \dots\) と倍々に増やしていき、true/false の境界を跨いだらその範囲で二分探索を行う方法です。 今回は、自明に \(p_n \gt n\) なので、上限の初期値はそれに基づいて決めてよいです(別に \(2\) から始めてもよいです)。

fn main() {
    assert_eq!(nth_prime(3), 5);
    assert_eq!(nth_prime(4118054813), 99999999977);
}

fn nth_prime(n: usize) -> usize {
    let mut lo = n;
    let mut hi = 2 * n;
    let small = |i| prime_pi(i) < n;
    while small(hi) {
        hi *= 2;
    }
    while hi - lo > 1 {
        let mid = lo + (hi - lo) / 2;
        if small(mid) {
            lo = mid;
        } else {
            hi = mid;
        }
    }
    hi
}

ここで、prime_pi別の記事 で書いたやつを使うと、\(O(n^{3/4}/\log(n))\) time です。 また、\(p_n = O(n\log(n))\) が知られているので、全体で \(O( (n\log(n))^{3/4}/\log(n\log(n))\cdot \log(n\log(n)) = O( (n\log(n))^{3/4})\) time です。

お粗末さまでした。

眠れない夜は素数の個数でも数えましょう

別に、おふとんから出たくない朝とかに数えてもいいと思います。

\(n\) 以下の素数の個数を数えたいときどうしますか? 各整数 \(2\le i\le n\) に対して素数判定して数えます? 素数判定を試し割り法でやると \(\Theta(n\sqrt{n})\)、線形篩*1を使えば \(\Theta(n)\) とかですね。 これを \(o(n)\) でできたらうれしくないですか?という話です。

今回は \(O(\sqrt{n})\) space, \(O(n^{3/4}/\log(n))\) time の話がメインです。 定数倍があまり軽くない \(O(n^{2/3})\) space, \(O(n^{2/3})\) time の話も少し触れます。

(追記:2021/06/05)少し難しめかもですが、アイディア自体は水色もあれば理解できると思います。

(追記:2022/06/29)勉強会用に作ったスライドを公開しました:https://rsk0315.github.io/slides/prime-counting.pdf

前提知識

以下、\(n\) 以下の個数の素数を \(\pi(n)\) と書きます。

導入

素数の個数を数えるにあたって、素数を実際に列挙する必要はないというのが重要です。 実際に列挙していては、「与えられた素数の次の素数はなに?」というのに \(\Theta(1)\) 時間で答えられても、\(\Theta(n/\log(n))\) 時間かかってしまいます*2

ここで、Eratosthenes の篩について考え直します。次のようなアルゴリズムです。

  1. \(1\) から \(n\) の bool 配列 \(a_i\) を用意する。\(a_1\) のみ false とする。
  2. \(i = 2, 3, \dots, \lfloor\sqrt{n}\rfloor\) に対して、次の処理をする。
    1. \(a_i\) が false なら次の \(i\) に移る。
    2. \(j = i, i+1, \dots, \lfloor n/i\rfloor\) に対して、\(a_{ij}\) を false にする。
let mut is_prime = vec![true; n + 1];
is_prime[1] = false;
for i in (2..=n).take_while(|&i| i * i <= n) {
    if !is_prime[i] { continue; }
    for j in i..=n / i {
        is_prime[i * j] = false;
    }
}

ここで、各 \(i\) に対して 2.2 のステップでいくつの \(a_{ij}\) が true から false になるかについて考えます。 この個数をうまく管理できれば、篩い終えた後にいくつ true になっているか、すなわち素数の個数がわかることになります。

この手法は、界隈では Lucy DP などと呼ばれることもあるようです*3

解説

(追記 2021/05/19)画像とかをつけるとわかりやすくなる気がしたので、そのうちつけると思います。

\(S(v, p)\) を、\(i=p\) まで処理を終えた後に \(a_j\) が true である \(2\le j\le v\) の個数と定義します。 \(p\cdot p \gt v\) であれば \(i=p-1\) のときから配列は変化しないので、\(S(v, p) = S(v, p-1)\) とわかります。\(p\) が素数でない場合も同じです。

\(p\) が \(p\cdot p\le v\) なる素数の場合はむずかしいのでちょっと考えます。 \(j = p, p+1, \dots, \lfloor v/p\rfloor\) に対して \(a_{pj}\) が false になりますが、\(j\) が \(p\) 未満の素数で割り切れるものはすでに篩われています。よって、篩われる個数は「\(p, p+1, \dots, \lfloor v/p\rfloor\) のうちで篩われていないもの」であり、これは \(S(\lfloor v/p\rfloor, p-1)-S(p-1, p-1)\) です。すなわち、次が成り立ちます。 \[S(v, p) = S(v, p-1) - (S(\lfloor v/p\rfloor, p-1) - \pi(p-1) ).\]

さて、\(\pi(n)\) は \(S(n, \lfloor\sqrt{n}\rfloor)\) と等しいので、これを求めたいわけですが、\(S(v, \bullet)\) を求めるためには(その時点で見つかっている素数の個数と)\(S(\lfloor v/\bullet\rfloor, \bullet)\) の形で表せるものさえわかっていれば十分とわかります。 \(\lfloor \lfloor n / p\rfloor / q\rfloor = \lfloor n / pq\rfloor\) が重要です*4

よって、\(v = 1, 2, \dots, \lfloor\sqrt{n}\rfloor\) と \(v = \lfloor n/1\rfloor, \lfloor n/2\rfloor, \dots, \lfloor n/\lfloor\sqrt{n}\rfloor\rfloor\) についてのみ管理します(サイズ \(O(\sqrt{n})\) の配列ふたつで管理できます)。 以下のコード中では \(\texttt{smalls}[i] = S(i, p)\)、\(\texttt{larges}[i] = S(\lfloor n/i\rfloor, p)\) とします。

fn main() {
    assert_eq!(prime_pi(10), 4);
    assert_eq!(prime_pi(100), 25);
    assert_eq!(prime_pi(100000000000), 4118054813);
}

fn prime_pi(n: usize) -> usize {
    let n_sqrt = floor_sqrt(n);

    let mut larges: Vec<_> = (0..=n_sqrt).collect();
    for i in 1..=n_sqrt { larges[i] = n / i - 1; }
    let mut smalls: Vec<_> =
        (0..n / n_sqrt).map(|x| x.saturating_sub(1)).collect();

    for p in 2..=n_sqrt {
        if p < smalls.len() {
            if smalls[p] <= smalls[p - 1] { continue; }
        } else {
            if larges[n / p] <= smalls[p - 1] { continue; }
        }
        let pc = smalls[p - 1];
        let q = p * p;
        for i in (1..=n_sqrt).take_while(|&i| n / i >= q) {
            // vi = n / i
            // dp[n / i] -= dp[n / i / p] - pc;
            let ip = i * p;
            let cur = *larges.get(ip).unwrap_or_else(|| &smalls[n / ip]) - pc;
            *larges.get_mut(i).unwrap_or_else(|| &mut smalls[n / i]) -= cur;
        }
        for i in (1..smalls.len()).rev().take_while(|&i| i >= q) {
            // vi = i
            // dp[i] -= dp[i / p] - pc;
            let cur = smalls[i / p] - pc;
            smalls[i] -= cur;
        }
    }
    larges[1]
}

fn floor_sqrt(n: usize) -> usize {
    if n <= 1 {
        return n;
    }
    let mut lo = 1;
    let mut hi = n;
    while hi - lo > 1 {
        let mid = lo + (hi - lo) / 2;
        match mid.overflowing_mul(mid) {
            (x, false) if x <= n => lo = mid,
            _ => hi = mid,
        }
    }
    lo
}

計算量解析

i に関するループがふたつありますが、ひとつめの計算量は次の値で抑えられます。以下の \(i\) は p に対応します。 \[ \begin{aligned} \int_2^{\sqrt{n}} \min\{\sqrt{n}, n/i^2\}\, di &= \int_2^{\sqrt[4]{n}} \sqrt{n}\,di + \int_{\sqrt[4]{n}}^{\sqrt{n}} n/i^2\,di\\ &= \sqrt{n}\cdot\sqrt[4]{n} + O(\sqrt{n}) + (\sqrt[4]{n}-1)\cdot\sqrt{n} \\ &= O(n^{3/4}). \end{aligned} \]

ふたつめも。 \[ \begin{aligned} \int_2^{\sqrt{n}} \max\{\sqrt{n}-i^2, 0\}\, di &= \int_2^{\sqrt[4]{n}} (\sqrt{n}-i^2)\, di + \int_{\sqrt[4]{n}}^{\sqrt{n}} 0\,di \\ &= 1/3\cdot (2n^{3/4} - 3\sqrt{n} + 1) \\ &= O(n^{3/4}). \end{aligned} \]

合わせて \(O(n^{3/4})\) です。実際には、素数の分布から \(O(n^{3/4}/\log(n))\) になっている気がします。

工夫

オーダーの改善ではないはずで、定数倍高速化だと思います。

上記のアルゴリズムで、篩う \(i\) を \(\lfloor\sqrt{n}\rfloor\) までではなく \(\lfloor \sqrt[4]{n}\rfloor\) までにしてみます。 このとき、篩われずに残っている合成数は、たかだか 3 つの素数の積で表せることがわかります*5。4 つかけると \(n\) を越えちゃうので。 このことから、全体をふたつの処理に分けて行うことが考えられます。

  • \(\lfloor\sqrt[4]{n}\rfloor\) 以下の素数で篩い、篩われた個数を引く。
  • たかだか 3 つの素数の積で表せる合成数の個数を引く。

合成数の個数を数える方について考えます。 篩われずに残っている整数(以前篩うのに使ったのは除く)を昇順に見ていきます。\(i_1, i_2, \dots\) とします。 各 \(k\) に対して、\(i_k\) の倍数である \(n\) 以下の整数の個数を足します。 また、\(k'\lt k\) に対して \(i_k\cdot i_{k'}\) の倍数である整数はすでに数えられているので、引きます。

ここで、\(i_k\) の倍数である \(n\) 以下の整数は、\(S(\lfloor n/i_k\rfloor, \lfloor\sqrt[4]{n}\rfloor) - \pi(i_k -1)\) と表せます。\(i_k i_{k'}\) の方は \(S(\lfloor n/i_k i_{k'} \rfloor, \lfloor\sqrt[4]{n}\rfloor) - \pi(i_{k'} -1)\) です。

実装

アイディアは上記の通りですが、実装上の工夫が少しあるので下で説明します。

変数の概要は次の通りです。

  • \(\texttt{smalls}[i] = S(i, p)\)
  • \(\texttt{roughs}[i]\):消されていない \(i\) 番目の整数
    • \(1\) は残る、篩うのに使った素数は消える
    • サイズは \(\texttt{s}\) で管理される
  • \(\texttt{larges}[i] = S(\lfloor n/\texttt{roughs}[i]\rfloor, p)\)
  • \(\texttt{pc} = \pi(p-1)\)
  • \(\texttt{skip}[i]\):\(i\) が篩われていれば true
    • ただし偶数については更新しない

\(p\) の変化に伴って更新されるので場所によって ±1 がありますが、各ループの最後で上記の定義に合うように管理します。 最初のループの開始前においては、\(p = 2\) での値です。

fn main() {
    assert_eq!(prime_pi_fast(100000000000), 4118054813);
}

fn prime_pi_fast(n: usize) -> usize {
    if n <= 3 {
        return n.saturating_sub(1);
    }
    let v = floor_sqrt(n);
    let mut smalls: Vec<_> = (0..=v).map(|i| (i + 1) / 2).collect();
    let mut s = (v + 1) / 2;
    let mut roughs: Vec<_> = (0..s).map(|i| 2 * i + 1).collect();
    let mut larges: Vec<_> =
        (0..s).map(|i| (n / (2 * i + 1) + 1) / 2).collect();
    let mut skip = vec![false; v + 1];

    let mut pc = 0;
    for p in (3..=v).step_by(2) {
        if skip[p] { continue; }
        let q = p * p;
        pc += 1;
        if q * q > n { break; }
        skip[p] = true;
        for i in (q..=v).step_by(2 * p) {
            skip[i] = true;
        }
        let mut ns = 0;
        for k in 0..s {
            let i = roughs[k];
            if skip[i] { continue; }
            let d = i * p;
            let x = if d <= v {
                larges[smalls[d] - pc]
            } else {
                smalls[n / d]
            };
            larges[ns] = larges[k] + pc - x;
            roughs[ns] = i;
            ns += 1;
        }
        s = ns;
        let mut i = v;
        for j in (p..=v / p).rev() {
            let c = smalls[j] - pc;
            let e = j * p;
            while i >= e {
                smalls[i] -= c;
                i -= 1;
            }
        }
    }

    let roughs = roughs;
    let pc = pc;

    let mut res: usize = larges[0] + (s + 2 * (pc - 1)) * (s - 1) / 2
        - larges[1..s].iter().sum::<usize>();

    for l in 1..s {
        let q = roughs[l];
        let m = n / q;
        let e = smalls[m / q] - pc;
        if e <= l { break; }
        let t: usize = roughs[l + 1..=e].iter().map(|&r| smalls[m / r]).sum();
        res += t - (e - l) * (pc + l - 1);
    }
    res
}

fn floor_sqrt(n: usize) -> usize { /* 上と同じなので省略 */ }

実装の説明

冷静になると自明かも。わからなければ、下記の説明を読みつつ、各箇所で値を適宜出力したりしながら確かめるとよいと思います。

roughs[ns] の更新

roughs[..s] が篩われていない整数です。 ns が次のループにおける s です。 ns = 0 で初期化しておき、roughs[ns] に値が入ったら ns += 1 していきます。前から詰め直しているということですね。

larges[smalls[d] - pc] についての説明

smalls[d] - pc は「篩われずに残っている整数のうち d は何番目か?」を意味します*6

smalls[d] - pc は「d 以下にある篩われていない整数」から「篩うのに使った整数」を除いたものの個数です。 ただし、この段階ではまだ pc は \(\pi(p -1)-1\) であることに注意です。これに、\(1\) のぶんの個数を足して、所望の値であることがわかります。 roughs[それ] が篩われていない d 番目の整数であることと larges の定義から、結局 \(n/d\) 以下で篩われていない整数の個数になります。

smalls[i] の更新

j = i / p を保って更新されるようにします。 i >= j * p である間 smalls[i] -= smalls[j] - pc で更新され、smalls[i] -= smalls[i / p] - pc になっています。

res の初期化

初期化の際に、(上で説明した)\(i_k\) の倍数を先に引いています。等差数列の公式などを思い出すと吉です。

res の更新

  1. q (\(i_{k'}\)) を固定
  2. q より大きい r (\(i_k\)) を固定
  3. n / (q * r) として表せる篩われていない整数の個数を求める

\(\sqrt[4]{n} \lt i_{k'} \lt i_k\) から、\(\lfloor n / i_k i_{k'}\rfloor \le \lfloor\sqrt{n}\rfloor\) であり、m / r = n / (q * r)smalls にアクセスしても問題ありません。

3. で求めた個数から、(前半パートですでに数えた)\(\sqrt[4]{n}\) 以下の素数を含む個数を除く処理が -= (e - l) * (pc + l - 1) です。 \(\lfloor n / i_k i_{k'}\rfloor\) として表せるうちで数えたい最小値は \(i_k'\) なので、2. で固定するのは \(\lfloor n / i_{k'}^2\rfloor\) 以下の範囲です。

計算量解析

前半パートでは、各 \(i \le n^{1/4}\) の素数に対して \(O(\sqrt{n})\) 程度の処理をするので \(O(n^{3/4}/\log(n))\) time です。篩の部分も \(O(\sqrt{n}\log(\log(n))) \subset o(n^{3/4}/\log(n))\) です。 後半パートでは、雑に見積もって \(n^{1/4} \lt i \le n^{1/2}\) の各素数に対して \(O(n/i^2)\) 程度の処理をしています。 \[ \int_{n^{1/4}}^{n^{1/2}} \frac{n}{i^2}\, di = n^{3/4} - n^{1/2}. \] であり、素数の分布から \(O(n^{3/4}/\log(n))\) になりそうです。

\(n = 10^{13}\) でも 1 秒前後くらいで終わりました。 \(\log(n)\) で割れる部分のちゃんとした(お気持ちでない)証明ができたら追記します。

その他

\(\sqrt[3]{n}\) とかで区切って \(O(n^{2/3})\) 時間にできるのかな? 区切り方を変えれば \(O(n^{3/4-\varepsilon})\) にはできそうな気もするけど、定数倍とか諸々のことはわかりません。

Dirichlet 積などを使った方法 とかもあります。軽めの定数倍では実装できませんでした。

参考文献

おわり

おやすみなさいにゃ。

(記事を書いていたら眠れなくなりました)

*1:実装はこれとか素因数分解の一意性をうまく使いつつ配る DP をします。

*2:素数定理から。

*3:Project Euler での Lucy_Hedgehog 氏の投稿 に由来するようです。

*4:ABC 141 D の解説 とかに証明があります。

*5:\(\lfloor\sqrt[3]{n}\rfloor\) までにすればたかだか 2 つにできます。

*6:rank/select 演算を知っている人は、それに似たことを考えるとよいと思います。

intXX_t に関して

どうしてこんなことに...

まえおき

C++ には次の型があります:

  • signed char
  • unsigned char
  • char
  • short signed int (= short)
  • short unsigned int (= unsigned short)
  • signed int (= int)
  • unsigned int (= unsigned)
  • signed long int (= long)
  • unsigned long int (= unsigned long)
  • signed long long int (= long long)
  • unsigned long long int (= unsigned long long)

char の signedness(符号つきかどうか)は処理系定義なのですが、signed char とも unsigned char とも異なる型であることが規定されています。

また、signed long long int などについては、順番はどうでもよくて、たとえば long int signed long なども同じ型として扱われます。

ともかく、signedness などを無視すると、次の 5 つがあることになります。

  • char
  • short
  • int
  • long
  • long long

ところで、扱えるビット長は 8, 16, 32, 64 は 4 種類です*1鳩ノ巣原理から、上記の型について、少なくとも 1 組はビット長が同じものが存在しますね。

よくある環境では、次のようになっているはずです。

  • char → 8 bit
  • short → 16 bit
  • int → 32 bit
  • long → 32 bit OR 64 bit
  • long long → 64 bit

char が 64 bit の処理系とか、short が 32 bit の処理系とかも規格上は許されてはいますね。これらの型同士の大小関係とか、最低限のビット長くらいしか規定されていません。)

long が 64 bit の環境をみんなが使っていれば「64 bit 整数を使いたいなら long を使いましょう。おわり」にできるんですが...

多くのジャッジ環境(Codeforces は別)は long が 64 bit なので、手元環境が Windows でない人は long と書けばいい気がします。

本題

「そもそも 64 bit 整数を使っている」みたいなことを明示したいのに「long」とかの名前を使いたくないという感情はあるので、ビット長で名付けられた型を使いたい気分になります。

そこで、std::int64_t みたいなのを使うようになるのですが、これは罠が多いので、それに関する記事です。

まず、intXX_t は上記の型のどれかの typedef である(あれらと別の型として存在しているわけではない)ことに注意です。

前述の通り、long が 64 bit の処理系とそうでない処理系があるので、int64_tlong だったり long long だったりします。 このことは、意外と厄介です。

リテラルのつくりかた

では、int64_t の定数を自分で定義するときはどうすればいいでしょう。

unsigned0u とか、long0L のような suffix は用意されていません。

auto x = INT64_C(0);

INT64_C というマクロが定義されており、これを使うとよいです(めんどくさいね)。 このマクロは処理系が定義しておいてくれているもので、適宜 0L とか 0LL とかに置き換えてくれます。

int64_t(0)(int64_t)0 のようなキャストでも別にいいです。

多用するなら、リテラルを定義しちゃいましょう。 ユーザ定義リテラル_ で始めないと未定義動作(予約されている識別子を勝手に使ったことになる)なので、そういう感じにします*2

int64_t operator ""_i64(unsigned long long) { return n; }

これを定義しておけば、0_i64 のようにして作ることができます。

次のように適当に決め打ちしていいじゃんと侮っていると、面倒なことになります。

int64_t x = 0LL;

std::max

もちろんこれに限るわけではないですが、ありがちなので。

int64_t x = /* 計算で出てきた値 */;
int64_t y = std::max(0L, x);  // ???

int64_tlong の処理系では 0Lx の型が同じなのでコンパイルが通りますが、long long の処理系では異なるので通りません。

Note: std::max は 2 つの引数の型が同じである必要があります。

scanf / printf

std::cin を何らかの理由で使いたがらない人向けです。

int64_t x;
scanf("%ld", &x);

これは、int64_tlong long である環境では未定義動作になるはずです(scanf はフォーマット文字列で指定される型と実際の型が異なると未定義動作になるので)。

そこで、これもやっぱりマクロが用意されていて、次のようにすると安全です。 受け取った 2 整数を順番を入れ替えてスペース区切りで出力する例です。

#include <cinttypes>
int main() {
    int64_t x, y;
    scanf("%" SCNd64 "%" SCNd64, &x, &y);
    printf("%" PRId64 " %" PRId64 "\n", y, x);
}

SCNd64 などが "ld" とかに展開されるように処理系が定義してくれています。 "%ld" となっていないのは、"%02ld" のようなのを作りやすいようにするためだと思います。

Note: C++ の文字列リテラルはくっつけると文字列の意味で連結することができ、"%" "ld" "%" "ld" と書けば "%ld%ld" と書いたのと同じことになります。

どうせ競プロで使うぶんにはジャッジ環境であってさえいればいいので、"%ld" とか書いてしまえばいいんですが、気持ち悪さを抱えながら書くことになります。 そもそも long とかを考えたくないからこういう型を使っているのに... という気分になります。

余談:intmax_t という型もあって、これについては %jd というのが使えます。

int8_t

これは std::cin / std::cout で入出力する人向けです。

前述のように、これらの型は typedef であって、「専用に用意された整数型」ではないです。 つまるところ、int8_tchar だったりします。

int8_t x = 65;
std::cout << x << '\n';

のように書くと...? A とかが出ちゃいます。あーあ。

ACL の話

これ忘れてました。追記です。

ACLint64_t をサポートしていないというか、long をサポートしていないというのが正しい気はします(ジャッジ環境で int64_t == long なので)。

そもそも定義されない処理系もある

こんな処理系を触ることはないでしょうので、これは気にしません。

そのほか

int_fast32_t とか int_least64_t とかについても同じような問題があります。 これらはそもそもどのビット長なのかすらわからないので、参りがちです。

int_fast32_t は別に「普段の 32 bit 整数とは異なるすごい整数型」ではなく、「処理系が適当に決めた最低 32 bit ある整数型」なので、必ずしも常に速いとすら限りません。

どうせ人々は自作ライブラリをぺたりしてるのでしょうから、自分で定義する方がいいんじゃないかなと思います。好きな名前をつけられますし。

using i32 = int;
using u64 = unsigned long long;
...

むかしの

*1:すごい処理系であれば別にこれに縛られはしないと思うのですが、とりあえず現在よくある環境たちについての話ということにします。

*2:グローバルでアンダースコアで始めるのは、リテラルなら許されるんですかね?

ICPC 2020 Asia Yokohama Regional 参加記

あー、おわりです。

えびちゃんの他の参加記は ここから*1

えびちゃんは B3 の頃から ICPC に出ていました。 four-t で 3 回と、今回 tsutaj で出ました。チームメイトに大変恵まれていたなぁというのをとても思います。 今回で老害化です。

tsutaj(チーム)はえびちゃんと、たぶきゅんと、monkukui からなるチームです。tsutaj(個人名)は個人です。

いろいろ思い出はありますが、それは別の機会に書くことにします*2

Day -10 くらい

システムテストコンテスト

先々週にあったやつです。

オタクなので、verdict 集めをしました*3

f:id:rsk0315:20210317221111p:plain
verdict 集め

オタクなので、OUTPUT-LIMIT の境界を探る遊びをしました。 こういうのはどうせ 2 べき bytes だと思うので、指数部をにぶたんしました。

雑に出力すると TIMELIMIT になったり、前に作った大量出力用のコードがバグっていて期待通りでないバイト数を出力したりで少しわたわたしました。

223 bytes ではセーフで、224 bytes ではアウトでした。 224-1 ならセーフなのか、223+1 からアウトなのか、中間(10 MB ± 1 とか?)なのかで無駄な探索をしたりしましたが、結局、8 MB がセーフの上限だと突き止めました。 ジャッジに負荷がかかるかな?とか、攻撃と見なされたら嫌だな〜とか思いつつも、提出数自体が少なかったので、いいかなと思ってやっちゃいました。

f:id:rsk0315:20210317221752p:plain
一応チーム slack に報告する人

そんなことより static_assert(__cplusplus >= 201703L); とかを調べるべきなんですが、完全に忘れていました(1 敗)。

Day 1

受付

任意文字列発声脆弱性のコーナー。

文脈がないと伝わらないね(The Atama チームの受付が終わったときの様子)。

開会式

コンテストシステムのチュートリアルのコーナー。

これ言われなかった(例年、ログインの際に P-A-S-S-W-O-R-D と言いながらパスワードを打つのをやっていた気がします)。

WA を投げて clar で不満などを言うコーナーです。\(ax^2+bx+c=0\) を解いてねという問題です。

a=b=c=0 について触れられていなかったり、それにも関わらず CORRECT になったり、それで人々がわくわくしていたりして、面白かったです。

ラクティス

この前の日くらいに Judging Details が公開されて、OLE が > 4 MB と書かれていて、ほんとかな〜と思っていました。 念のため OLE 境界のコードふたつを投げて確認しました。

clar を投げたら対応してもらえてうれしかったです。

f:id:rsk0315:20210317222356p:plain
なけなしの英語力さん

f:id:rsk0315:20210317222439p:plain
A new announcement about this issue

別に深刻な問題ではないと思うんですが、不備を報告して対応してもらうとうれしくなりますね。

うれしくなっていたら static_assert(__cplusplus >= 201703L); のことを忘れていました(合わせて 1 勝 2 敗)。

Day 2

本番

A 困っていましたね。

B 貪欲? いや、できなくない? DP しますか? 一生バグらせた後たぶきゅんに丸投げ。たぶきゅんも一生バグらせた後、えびちゃんが貪欲っぽいのを書いて CORRECT。う〜〜ん、ごめん。

monkukui が J を通していてえらい。

たぶきゅんが、「(概要の説明)〜〜(考察)〜〜(ここまで数秒)...ので、G を書きます。」とシームレスに書き始めていたのがよかったです。かっこよし。

この次たぶきゅんが H を書いてたかも? なんかペースが速い。

たぶきゅんが書いている間に slack で E の概要を伝えると、たぶきゅんが「2 年前の JAG 夏で見覚えある」と言っていたので、探して貼ったりしました。E も通していました。

monkukui が I の DP をがんばっていて、デバッグとかをいろいろがんばったけど、そこで終わり。

後々聞くところによれば、C に手をつけておく方がよかったかも。う〜〜ん、大くやしい。

YES_No

f:id:rsk0315:20210317223818p:plain
YES_No 表記の元ネタ(?)

eat_ice、盛り上げ界の tourist。

任意文字列発声の脆弱性(失敗しがち)イベント。つたじ(かなしい)。

Yes を言わせられなかったのくやしいね。

お顔大公開のコーナーで、お顔が大公開されて恥ずかしかったです。 んわーとなってタブを閉じてしまいました。 後で懇親会のときに、かわいいと言ってもらえたのでうれしかったです。

えらい先生が「Yes/No おじさん」「___ KING ___ 様」などと呼んでいたのが面白かったです。 優勝コメントも面白かったですね。

オタクなので気になるんですが、___KING___ ではなく ___ KING ___ な気がします。

結局 6 完 20 位で、中央値よりは真に上でしたが、そういう意味では去年と同じなので、うむむむという感じです。

Closing Party

___ KING ___ 様がスタッフ様と話しているのを聞いていると、(たぶん F 問題?)「あれが難しいつもりというのがかなりびっくりで〜」のようなことを言っていて、ひえーとなりました。

いろいろな人と話したり、いつもの勢と話したり、びーとくんや olphe をつんつんしたりしました。

みさわさんにつんつんしてもあまり話してくれなかったりした*4のですが(マイクが不調だったりしたのかな?)、なんやかんやで、フローの講義をしてくれました。

climpet さんがえびちゃんについてきて「あなたと JAG」とだけ言ってきたのがちょっと怖かったです。

この辺からねむくなったり、おなかが空いたりしていました。

「朝早かったからね〜」と言われたのでつい「バカがつくったスケジュール」と口走ったら、なんかきゃっきゃされました。

近くにえらい先生がいて危ない気がしたので、念のためフォロー(?)のつもりで元ネタの紹介をしました。 これが最古じゃなかったらすみません。

楽しい時間はすぐ終わり。

おわり

あーー。じわじわ実感してきています。

えびちゃんと組んでくれた人たちありがとうです。 えびちゃんの力不足やオタク言動などで負担をかけたかと思いますが、長いこと組んでくれてうれしいです。 えびちゃん的には、このチーム(four-t / tsutaj)に身を置けたのが幸せです。

忙しさから、チーム練が疎かになったりしがちだったので申し訳ないです。

tsutaj は全員今年で引退なのですが、来年以降も弊学の若い人たちにがんばってほしいです。 チーム名に困ったら rsk0315 を使ってもいいですよ(??)。 と思ったんですが、Yes/No おじさんが読むのに苦労しそうなので、TAB や monkukui などを使う方がいい気がします。

ステマ

JAG に入るなどして、ちゃんと恩返しをしなきゃですね。

*1:他のと言いつつたぶんこの記事も含まれるんですが。

*2:書いているうちに今日の記憶が曖昧になると困るので。

*3:TOO-LATE は別で集めました。

*4:みさわさんが好きだけど, 好かれてはいない. か?

自分で未定義動作を書いて逆ギレする人かわいそう

お店に入るときにドアが開いているか確認せずに歩いていって、ドアにぶつかって怒っている人がいたらおかしい話じゃないですか*1

未定義動作を起こして怒るのはこれと似ている気がします。 競プロでよくあるコーナーケースなども同じです。

「いま書いている処理は、ありえる入力のどれに対しても適切に動くか?」というのをちゃんと意識するといい気がします。

昔の記事 では、未定義動作を書いたらどうなりうるかがメインだった気がします。 今回は、競プロの人々がよくやっている or やりそうな未定義動作の紹介集みたいなのに近いつもりです。

「複雑な書き方をするとこわれます」というのはまた今度書くとして、「簡単にこわれます」というのがメインです。

以下では、int は 32 bit の 2 の補数表現ということで書きます。AtCoder など多くの環境と同じです。

演算子

多くの演算子、入力によってはすぐ未定義動作を起こしてしまう...(かなしい)

+

#include <climits>
int main() {
  int bad = INT_MAX + 1;
}

オーバーフローはだめ、それはそう。

-

#include <climits>
int main() {
  int bad = INT_MIN - 1;
}

負のオーバーフローも気をつけましょう。

*

#include <climits>
int main() {
  int bad = INT_MAX * 2;
  int bad2 = INT_MIN * (-1);
}

オーバーフロー定期。

/

#include <climits>
int main() {
  int bad = 1 / 0;
  int bad2 = INT_MIN / (-1);
}

ゼロ除算は当然気をつけましょう。 オーバーフローにも気をつけましょう。

%

#include <climits>
int main() {
  int bad = 1 % 0;
  int bad2 = INT_MIN % (-1);
}

INT_MIN % (-1)0 でしょうと思うんですが、x / y がオーバーフローするときは x % y も未定義です。

<<

int main() {
  int bad = 1 << 100;
  int bad2 = 1 << (-5);
}

x << y は、y が負の場合や、x の(汎整数拡張後の)ビット幅以上の場合は未定義です。

[]

int main() {
  int a[5] = {31, 41, 59, 26, 53};
  int bad = a[5];
}

配列外参照もだめ。

標準関数

<cmath> の関数とかは予想に反しがちなときがあるかも? 適宜調べておきましょう。

en.cppreference.com

<algorithm> の関数、特に sort など順序が絡むものは誤る人が多いので気をつけましょう。 「順序としてこわれているものを第三引数に渡しちゃお〜」とすると、無限ループになったり Segfault になったりします*2

GCC 拡張の関数

gcc.gnu.org

__builtin_clz __builtin_ctz

x のうち先頭・末尾の 0 のビットの個数を数えるものです。 x == 0 のとき(すなわち 1 が現れないとき)の動作は未定義です。

ビット幅を返してくれるとは言っていません。

飽きたよね

えびちゃんも飽きました。

何をやるにしても未定義動作が絡んできて、覚えておくのが大変すぎてつらいというのは、えびちゃんもわかります。 「こんな処理が未定義じゃなかったら、やばいだろ」というような感覚を養うといいかもしれません。

「未定義動作なんて非本質すぎて考えたくない」と思ってしまうタイプの人には、C++ 以外の言語が向いているかもしれません?

ねこ

典型コーナーケースに対する「自分で場合分けを放棄して WA に逆ギレする人かわいそう」という記事や、「自分で定数倍の遅いコードを書いて TLE に逆ギレする人かわいそう」のような記事も書こうとしていました。「誤読して逆ギレする人かわいそう」という気持ちはありますが、記事に書くことはあまりないような気もします?

人々が何らかの対象に逆ギレしているのを見て、お気持ち表明したくなったらまた書きます。

*1:え、もしかして実在しますか?

*2:未定義なので、もちろん、こうなることは保証されていませんが。

Re: オーバーフロー判定が分からない人のためのスライド

経緯

以下ではあんまり元ネタっぽい感じではなくなります。すみません。

紹介

こういう記事 があります。

紹介 2

GCC 拡張の話をします。 競プロの人々は平気で GCC 拡張を使うので、今さら避ける必要もないと思って書きます。 __int128__builtin_popcount などはよく使われていますよね。

__builtin_mul_overflow という関数があります。掛け算してオーバーフローするとき真です。 次のような感じで使えます。

long f(long x, long y) {
  long z;
  if (__builtin_mul_overflow(x, y, &z)) {
    // x * y がオーバーフローしたときの処理
  } else {
    // x * y がオーバーフローしなかったときの処理
    // このとき、z == x * y になっている
  }
}

便利ですね。

gcc.gnu.org

もちろん、mul 以外もあります。

注意

「オーバーフローしたらその型の最大値を返すような wrapper クラスを作りましょう」という発想に至ることはあります。 ただ、そのクラスで雑に計算すると、(多倍長で計算してから、その型の最大値と min をとったときの)結果と合わないことがあるので注意です。

次のような問題を考えます。

\(a, b, c\) が与えられます。 \(a\cdot b - c\) が \(2^{63}\) 未満ならその値を、そうでなければ overflow を出力してください。

  • \(0\le a, b, c \lt 2^{63}\)

たとえば、\( (a, b, c) = (2^{62}+1 ,2, 3)\) としてみると、\(a\cdot b \ge 2^{63}\) です。

\(2^{63}-1\) と min をとりながら計算するクラスでそのまま計算すると、\(2^{63}-4\) になってしまいます。 あるいは、\(a\cdot b\) がオーバーフローした時点で overflow と出力しても誤りです。

紹介 3

Rust、えらくて、整数に a.overflowing_mul(b) とか a.saturating_mul(b) のようなメソッドが用意されています。 前者は、計算結果と「オーバーフローしていたら true」の bool 値のペアが返されます。 後者は、オーバーフローしなければ計算結果、していたらその型の最大値(あるいは最小値)が返されます。

他にも checked_mul とか wrapping_mul とかいろいろあります。

doc.rust-lang.org

「大人しく Rust でも使ってろ」という思想が漏れていないか?

ACL の math の解説をするよ

ACL (AtCoder Library) の内部実装を知りたい人向けの記事です。

お友だちに「ねーね、ACL のこの関数ってどういう仕組みなの? 知ってたりしない?」と聞かれたとき、「え... なんかほら、わかんないけど、魔法で動くからいいんだよ」としか言えないと情けない気がしません? しました。なので書きます。

こういうシチュエーションはなくても、何かを実装したいときに「あ、これ ACL の実装のやつと同じ発想じゃん」となることはありえるので、知っていて損はないかなと思います。

めちゃくちゃ長くなったので、一度に全部読むのには適さないかもしれません。 数式部分の LaTeX コードなども含めて数えられていますが、26000 文字を超えています。

全体像

github.com

math.hpp にある関数たちです。

#include "atcoder/internal_math"

namespace atcoder {

long long pow_mod(long long x, long long n, int m);
long long inv_mod(long long x, long long m);
std::pair<long long, long long> crt(const std::vector<long long>& r,
                                    const std::vector<long long>& m);
long long floor_sum(long long n, long long m, long long a, long long b);

}  // namespace atcoder

github.com

atcoder/internal_math(を経由して #include される atcoder/internal_math.hpp)にある関数たちです。

namespace atcoder {

namespace internal {

constexpr long long safe_mod(long long x, long long m);

struct barrett {
    unsigned int _m;
    unsigned long long im;

    barrett(unsigned int m);
    unsigned int umod() const;
    unsigned int mul(unsigned int a, unsigned int b) const;
};

constexpr long long pow_mod_constexpr(long long x, long long n, int m);

constexpr bool is_prime_constexpr(int n);
template <int n>
constexpr bool is_prime = is_prime_constexpr(n);

constexpr std::pair<long long, long long> inv_gcd(long long a, long long b);

constexpr int primitive_root_constexpr(int m);
template <int m>
constexpr int primitive_root = primitive_root_constexpr(m);

unsigned long long floor_sum_unsigned(unsigned long long n,
                                      unsigned long long m,
                                      unsigned long long a,
                                      unsigned long long b)

}  // namespace internal

}  // namespace atcoder

よくある実装と比べて、高速化などの工夫がされています。 これを読んでいる人がびっくりするであろう点を挙げます。

  • is_prime_constexpr の最悪計算量が \(\Theta(\sqrt{n})\) ではなく \(\Theta(\log(n))\)。
    • この関数はコンパイル時だけでなく実行時にも呼べます。
  • pow_mod において、除算・剰余算を高々 2 回しか行わない。
    • 除算・剰余算は重いので、なるべく避けたいです。
  • crt において、計算途中でオーバーフローしない。
    • 法たちの最小公倍数がオーバーフローしない前提です。
    • 下手にやるとオーバーフローすると思います。

また、人によっては実装自体が思いつかない場合がありそうなものもあります。

  • inv_mod
    • 法が素数であることを仮定する実装しか知らない人もいそうです。
    • ↑ Fermat の小定理により \(m-2\) 乗するやつです。
  • floor_sum
    • ACL 登場前にこれを求める問題が出ていたら、500, 600 点くらいはつきそうです。
  • primitive_root_constexpr
    • 最小の原始根を求めます。

これらを当然に感じる人にとっては、目新しいことは書いていないかもしれません。 \[ \gdef\M{m_{\text{inv}}} \]

個別の説明

説明をしていきます。 「この操作は非自明ですが後述する〇〇によってできます」のような説明が増えると、その時点で何が非自明かわからなくなりそうなので、予め武器は揃えておきたいです。 そういう理由で、internal_math の方から説明していきます。

(追記 2021/01/22:これらはあくまで math の内部利用のための関数で、ドキュメントには含まれていません。 なので、今後のバージョンにおいては実装が変わることもありえると思います。 たとえば、未来のバージョンにおいて atcoder::internal::is_prime_constexpr を呼んだら、ここで書かれていた実装と違って困った(or 存在しなかった)ということはありえそうな気はします。 適宜、この記事の時点でのバージョン などを GitHub で探して、コピペしたりするのもいいと思います。ライセンスが CC0 なので大丈夫だと思います。)

internal::safe_mod

\(x\) と \(m \ge 1\) を受け取り、\(x \bmod m\) を返します。 これはほぼ自明なんですが、C++ の除算の仕様で、\(x \lt 0\) の場合には注意が必要です。

internal::barrett

はい出ました。

法 \(1\le m\lt 2^{31}\) と \(0\le a\lt m\), \(0\le b\lt m\) に対して \(a\cdot b\bmod m\) を高速に求めるためのクラスです。

まず前提として、除算は他の演算(加算や乗算、ビット演算など)と比べて重いです。 なるべくなら他の演算に置き換えてしまいたいです。

他の演算に変換する方法はいくつか知られており、その一つがこの Barrett reduction です*1。 実は、コンパイル時定数であれば、この変換(最適化)はコンパイラちゃんがしてくれます(えらい)。

(実際には、いくつかの種類から状況に応じて効率のよいものをやってくれます。 実装は この辺 に書いてある気がします。あんまりちゃんと読めていませんが、ここで説明するものとは違うことをしているかもしれません。)

long long MOD = 998244353;
int main() { /* がんばる */ }

よりも

long long const MOD = 998244353;
int main() { /* がんばる */ }

の方が速くなったという経験があるという人もいると思いますが、これはそういう事情によるものです。 グローバルに const でない MOD が置かれていると、コンパイラちゃん的にはそれが定数なのか判断しかねるので、最適化してくれません*2const をつけることで除算の部分が最適化され、速くなったわけですね。

コンパイラちゃんが最適化してくれるのはコンパイル時定数だけなので、実行時に mod が決まるような場合にはつらいです。 前置きが長めになりましたが、この変換を実行時に自前でやっちゃおうねというのがこのクラスです。

Barrett reduction の話

Wikipedia が結構わかりやすいですが、ACL のコード中にコメントもあるので、それに従って説明していきます。

Barrett reduction は、法 \(m\) と \(0\le z \lt m^2\) に対して \(z\bmod m\) を求める方法です。 \(m\) に対する前処理で一度除算を行うのみで、各クエリ \(z\) に対しては 除算を行わずに 求めることができます。

\(0\le a\lt m\) と \(0\le b\lt m\) に対して \(0\le a\cdot b \lt m^2\) ですから、これを用いることで、乗算 \(a\cdot b\bmod m\) を除算なしで求めることができるわけです。

以下、\(z = a\cdot b \lt m^2\) ということにして話を進めます。 求めたいのは \(z \bmod m\) ですが、これは \(z - \lfloor z/m\rfloor \cdot m\) と等しいので、\(\lfloor z/m\rfloor\) を考えます。

基本的な方針は次のような感じです。

  1. 前計算で \(2^{64}/m\) くらいの値 \(\M\) を求めておく。
  2. クエリに対して \(z\cdot \M\) を計算すると、\(z\cdot 2^{64}/m\) くらいの値になる
  3. なので、\(\lfloor z\cdot \M/2^{64}\rfloor\) は \(z/m\) くらいの値になる
    • ここの除算については補足あり。
  4. 誤差を修正する
    • さっきから「くらい」と言っている部分のこと
    • この誤差は、これから証明するように高々 \(1\) である
    • → 正しい値ならそのまま、そうでなければ \(1\) ぶん補正すればよい

3 の除算について補足(クリックで展開) 「64 bit の整数 \(z\), \(\M\) の積の、上位 64 bit(1 ワードぶん)」が欲しいという状況です。 実装の際は、以下のようにすればよいです。

((unsigned __int128)z * m_inv) >> 64

シフト演算は軽そうですが 128 bit 整数の乗算はどうなの? という声も聞こえます。 実際には、コンパイラちゃんは賢くて、「64 bit 整数同士の積の上位ワードが欲しいのね」というのをわかっているような最適化をしてくれるので、助かります。

unsigned __int128 が使えない環境もあるんですが、同様のことをする命令 _umul128 を使うことで対処していますね。 See here.

3 の除算についての話おわり。

さて、ACL では、\(\M = \lceil 2^{64}/m\rceil\) としているので、以下そうします。 切り上げ除算は (a+b-1)/b がよく知られていますが、次のようにはできません。

m_inv = ((1ull << 64) + m-1) / m;

1ull << 64 の時点で未定義動作になってしまうので。(((unsigned __int128)1 << 64) + m-1) / m とかするといいのかも? 重そうかも? ということで、少し頭をひねります。

m_inv = ((1ull << 64) - 1) / m + 1;  // まだだめ
m_inv = (-1ull) / m + 1;  // よい

符号なし整数はオーバーフローした場合の動作が定義されている(循環する (wrapping))ので、こういうことができるんですね。 Rust とかで実装する場合は気をつけましょう。 特に、m == 1 の場合は -1ull + 1 なので、そこの wrapping にも注意です(0 になってくれればよい)。

正当性の話

誤差が高々 \(1\) であることと、その検出方法を示して終わりです。 コード 中にもコメントがあります。

まず \(m = 1\) の場合、\(a = b = 0\) なので、\(z = 0\) です。また、\(\M = 0\) です。 \(z\cdot\M/2^{64} = 0\) なので、誤差はなくて、よいです。

残りは \(m \gt 1\) の場合です。 \(m\cdot\M = 2^{64} + r\) (\(0\le r \lt m\)) と書けます。\(r\) の範囲は天井関数の性質からきています。

また \(z \lt m^2\) なので、ある \(0\le c, d\lt m\) が存在して \(z = cm+d\) と書けます。 このあまり \(d\) が最終目標ですが、商 \(c\) に注目します。 商の方が求めやすくて、正しい商を求めればあまりは求められるので。

\[ \begin{aligned} z\cdot \M &= (c\cdot m+d)\cdot \M \\ &= c\cdot (m\cdot\M) + d\cdot \M \\ &= c\cdot (2^{64}+r) + d\cdot \M\\ &= c\cdot 2^{64} + c\cdot r + d\cdot \M. \end{aligned} \]

この \(c\cdot r + d\cdot \M\) について見ていきます。

\[ \begin{aligned} c\cdot r + d\cdot \M &\lt m\cdot m + m\cdot \M \\ &\lt m\cdot m + 2^{64} + m \\ &= 2^{64} + m\cdot(m+1) \\ &\lt 2^{64} + 2^{64} \\ &= 2\cdot 2^{64}. \end{aligned} \]

これにより、結局次のことがわかります。

\[ c\cdot 2^{64} \le z\cdot \M \lt (c+2)\cdot 2^{64}. \]

要するに、\(\lfloor z\cdot \M/2^{64}\rfloor \in \{c, c+1\}\) ということです。

長いので、\(v = \lfloor z \cdot \M/2^{64}\rfloor\) としておきます。

\(v = c\) ならば \(z-v\cdot m = z-c\cdot m = (z\bmod m)\) で、\(0\le z-v\cdot m\lt m\) が成り立ちます。 \(v = c+1\) ならば \(z-v\cdot m = z-(c+1)\cdot m = (z\bmod m)-m\) で、\(-m \le z-v\cdot m\lt 0\) が成り立ちます。

よって、\(z-v\cdot m\lt 0\) のときに補正を行えばよいです。

長かったです、お疲れさまでした。

ACL の話

barrettメンバ関数の説明をしていきます。

barrett::barrett(コンストラクタ)では、上で言う \(\M\) を求めています。 barrett::umodunsigned int 型で法 \(m\) を返すだけです。 barrett::mul は、上の方法で \(z\bmod m\) を求めます。

ちょっと横道の話

ここではあまりを求めるために Barrett reduction を使っていますが、途中で商を求めたことから分かる通り、除算の高速化としても使えます。

せっかくなので例でも見て遊びましょうか。 100, 3141592, 123456 などを 3 で割ってみましょう。

m_inv = (0xffffffffffffffff / 3 + 1);
// == 0x5555555555555556

(3 * m_inv) >> 64  // == 1

(3141592 * m_inv) >> 64  // == 1047197
3141592 / 3              // == 1047197

(123456 * m_inv) >> 64  // == 41152
123456 / 3              // == 41152

コンパイル時定数であればコンパイラちゃんがやってくれますが、実行時に決まる値で何度も割り算しなきゃな状況では、こういう方法もあるということですね。

正直な話、この定数倍高速化によって TLE/AC が分かれるようなコンテストには出たくないですが。 fastest を取りたい人や、想定より重いオーダーの解法をがんばって通したい人向けくらいの話だと思います。

そういえば、rolling hash で実行時に法を決めたい人は、これを使うことで高速化が見込める気がします。 それでも、自前実装は必要ないはずで、ACLdynamic_modint を使えばいいとは思います。

internal::pow_mod_constexpr

\(x\), \(n \ge 0\), \(m \ge 1\) を受け取って \(x^n \bmod m\) を返します。 よくある繰り返し二乗法です。

\( (x, n, m) = (0, 0, 1)\) の場合にうっかり \(1\) を返さないように注意です。

ここ、完全に説明を省くつもりだったんですが、冷静になると知らない人もいるかもしれないので書きます。

愚直にやると \(\Theta(n)\) 時間かかりますが、これを \(O(\log(n))\) 時間で行います。 答えを \(r = 1\) で初期化して、これに適切な値を掛けることを繰り返します。

\(n\) を 2 進数で表したときの、下から \(i\) 桁目が \(1\) なら \(r\) に \(x^{2^i}\) を掛けます。 \(0\) なら何もしません(説明上は \(1\) を掛けると見なす方がいいかも)。 また、\(x^{2^i}\) が得られている状態では、\(x^{2^{i+1}} = x^{2^i} \cdot x^{2^i}\) と計算できます。 \(x^{2^0} = x\) とわかることから、順に計算していけます。

\(n\) の 2 進数での下から \(i\) 桁目は \(\lfloor n/2^i\rfloor \bmod 2\) として分かります。 実際には、処理ごとに \(n \gets \lfloor n/2\rfloor\) と更新していけば、\(n \bmod 2\) で判定できます。 (さらに実際には、n >>= 1n & 1 をしています。)

数式で確認しておいてみます。\(n\) がある \(k\), \(b_i \in\{0, 1\}\) (\(0\le i\lt k\)) に対して次のように表せるとします。 \[ n = \sum_{i=0}^k b_i\cdot 2^i. \] このとき、\(r\) は次のようになりますね。 \[ \begin{aligned} r &= \prod_{i=0}^k x^{b_i\cdot 2^i} \\ &= x^{\sum_{i=0}^k b_i\cdot 2^i} \\ &= x^n. \end{aligned} \]

internal::is_prime_constexpr

はい出ました。 \(n \ge 0\) を受け取って、素数なら true、そうでなければ false を返します。

よくある試し割り法だと最悪時 \(\Theta(\sqrt{n})\) 時間になってしまいますが、\(O(\log(n))\) 時間のアルゴリズムが用いられています*3

Miller–Rabin のアルゴリズムが関係していて、それを知らないことには無理なので、まずそれの話をします。

Miller–Rabin の話

\(3\) 以上の奇数 \(n\) を受け取り、それが素数か判定するアルゴリズムです。 偶数または \(1\) の場合は簡単に分かるので、簡単にやってください。

大まかなアイディアは次のような感じです。

  • 整数 \(a\) を \(a\not\equiv 0 \pmod{n}\) からランダムに選ぶ。
  • 「これを満たさなければ合成数である」知られている条件 \(P_a(n)\) を調べる。
    • 残念ながら、「これを満たせば素数である」とは限りません。

十分な個数の \(a\) を試し、それでも「合成数である」とならなければ、それなりの確率で素数でしょうというものです。 「それなりの確率」は適当ではなくて、個数に対して見積もられていますが、この記事の範囲外なのでスキップします。 ACL で使われているアルゴリズムでは、この確率を \(1\) にする工夫がされているためです(次のお話が楽しみですね)。

さて、まずは \(P_a(n)\) について考えていきます。

う〜んう〜ん(考えている)。

Fermat の小定理から、素数 \(p\) と \(1\le a\lt p\) に対して \(a^{p-1}\equiv 1\pmod{p}\) が成り立ちます。 素数 \(p\) を法とする \(1\) の平方根は \(1\) と \(p-1\) しかありません*4。 よって、\(1\) でも \(p-1\) でもないものを \(2\) 乗して \(1\) になると、合成数だとわかります。

奇数 \(d\) と整数 \(s \ge 1\) を用いて \(n = d\cdot 2^s+1\) と表します。 法 \(n\) の下で \(a^d, a^{d\cdot 2}, \dots, a^{d\cdot 2^s} = a^{n-1}\) を順に計算していきます。 このとき、\(1\) または \(p-1\) が現れたらそこで終了し、そのときの値を \(y=a^{d\cdot 2^i}\) とします。

(追記 2021/02/16:現れなければ最後まで計算して、\(y=a^{n-1}\) とします。)

\(i = 0\) で \(y\equiv \pm 1\) だった場合は許します。 \(i\gt 0\) で \(y \not\equiv p-1\) だった場合は「\(\pm 1\) 以外の平方根が存在した*5」か「\(a^{d\cdot 2^s}\) まで計算しても \(1\) にならなかった」ことになり、合成数であることになります。 これを条件 \(P_a(n)\) とします。すなわち、\(P_a(n) = ( (i=0) \vee (y\equiv -1))\) です。\(P_a(n)\) が偽のとき、\(n\) は合成数です。

頭が暗黙に素数 mod を仮定してしまって想像できない人*6のために例を挙げてみます。 法 \(n\) を \(21 = 5\cdot 2^2+1\) とします。\(a = 8\) とすると \(a^d = 8^5 \equiv 8\)、\(a^{d\cdot 2} \equiv 1\) となります。 また、\(a = 3\) とすると \( (a^d, a^{d\cdot 2}, a^{d\cdot 2^2}) \equiv (12, 18, 9)\) となり、\(1\) になりません。

このような真偽判定の証拠に使われる \(a\) は証人 (witness) と呼ばれたりします。

証人の \(a\) ちゃん「自分は \(n\) が合成数であることを証明できます」

...ということですね。 他にも、SAT(充足可能性問題)は一般に解くのが難しいですが、「この割り当てなら充足可能だよ」という証拠となる割り当てのことを witness と呼んだりしますね。 certificate とも呼ばれるかも。

確率的じゃなくする話

上記のアルゴリズムは基数 \(a\) を選ぶところで乱択を行っています。 \(n \le 2^{32}\) であれば、これは \(\{2, 7, 61\}\) のみを使えば十分であることが知られています。 なので、それをします。

\(n\in\{2, 7, 61\}\) なら素数なのは知っているので、そのときはすぐ true を返しています。 \(a\equiv 0\) になってしまい、上記の判定法では false になってしまって都合が悪いためです。 \(7\) や \(61\) の倍数だった(かつ \(7\) や \(61\) でない)場合も怪しい感じはありますが、結果的には false になってくれるのでよいです。

また、入力に応じて適切に法を選択することで、1 つの \(a\) のみで判定する方法も知られています(提出コード (Rust))。 64-bit 整数においては、7 つの基数 \(\{2, 325, 9375, 28178, 450775, 9780504, 1795265022\}\) を用いる方法と、入力に応じて 3 つの基数を選ぶ方法が知られています。 オーバーヘッドが大きいものの、入力に応じて 2 つの基数を選ぶ方法も知られています。

7 つの基数の方は、合成数も含まれているため、互いに素などの条件を気にしないとこわれる気がします。

それらの(論文の著者によると思われる)コードは ここ で公開されています。

internal::is_prime

is_prime<173> のような書き方ができます。テンプレート定数ですね。 コンパイル時に素数判定が必要になるとき用のものです。

modint の法が素数かどうかの判定に使われています。

internal::inv_gcd

\(a\) と \(b \ge 1\) を受け取り、以下の条件を満たす \( (g, x)\) を返します。

  • \(g = \gcd(a, b)\), and
  • \(a\cdot x\equiv g \pmod{b}\), and
  • \(0\le x\lt b/g\).

\(a\) を適当に処理して、\(0\le a\lt b\) とします。 なお、\(a = 0\) の場合は \( (b, 0)\) を返しちゃいます。

互除法をベースにしつつ、適切な不変条件を管理しながらループします。

\(a\) と \(b\) の最大公約数 \(g = \gcd(a, b)\) について、\(x\), \(y\) が存在して次の等式が成り立ちます。 \[ ax + by = g. \] いま欲しいのはこの \(x\) で、\(y\) はいらないので、以下では \(b\) を法として考えます。 \[ \begin{aligned} ax&\equiv g\pmod{b}; \\ g-x\cdot a&\equiv 0 \pmod{b}. \end{aligned} \]

互除法は知っていると仮定していいですか? \( (a, b) \gets (b\bmod a, a)\) で更新していくと最終的に \( (0, g)\) になります。 それを踏まえつつ、上の式を目標として変形を繰り返します。 初期値は後述するとして、次の合同式を管理します。 \[ \begin{aligned} \begin{cases} s - m_0\cdot a\equiv 0,\\ t - m_1\cdot a\equiv 0. \end{cases} \end{aligned} \] また、\(0\le x\lt b/g\) に関連して、次の不等式も考えておきます。 \[ s\cdot|m_1| + t\cdot|m_0| \le b. \] 最終的に \( (s, t, m_0) = (g, 0, x)\) となってほしいのを考えると少し気持ちがわかるかも?しれません? 天下りかも。

\( (s, t) = (b, a)\) で初期化しておきます。 このとき、\( (m_0, m_1) = (0, 1)\) は合同式と不等式を満たします。 \(s\gt t\) に注意しておきます。

さて、互除法を考えると \(s \bmod t\) に関する合同式が欲しいです。 \[ (s\bmod t)-m_{\text{?}}\cdot a\equiv 0. \] ここで、\(s\bmod t = s-\lfloor s/t\rfloor\cdot t\) を思い出すと、\(s\) の式から \(t\) の式の \(\lfloor s/t\rfloor\) 倍を引きたくなります。 \[ (s\bmod t)-(m_0-m_1\cdot \lfloor s/t\rfloor)\cdot a\equiv 0. \] よって、\( (s, t) \gets (t, s\bmod t)\) と \( (m_0, m_1) \gets (m_1, m_0-m_1\cdot\lfloor s/t\rfloor)\) で置き換えるのを繰り返せば、以下の式が得られます。 \[ \begin{aligned} \begin{cases} g - m_0\cdot a\equiv 0,\\ 0 - m_1\cdot a\equiv 0. \end{cases} \end{aligned} \]

置き換えの際に不等式が保たれていることを確認しておきます。 \(s\cdot|m_1|+t\cdot|m_0|\le b\) を仮定します。 また、\(u = \lfloor s/t\rfloor\) とします。 \[ \begin{aligned} &\phantom{{}\le} (s-t\cdot u)\cdot|m_1|+t\cdot|m_0-m_1\cdot u| \\ &\le s\cdot|m_1|-t\cdot u\cdot|m_1| + t\cdot|m_0|+t\cdot|m_1|\cdot u \\ &= s\cdot|m_1| + t\cdot|m_0| \le b. \end{aligned} \] \(u\) が非負であることと三角不等式をうまく使いましょう。

また、このときにオーバーフローしないことも確認しておきます。 \[ \begin{aligned} |m_1\cdot\lfloor s/t\rfloor| &\le |m_1|\cdot s \\ &\le b, \\ |m_0-m_1\cdot\lfloor s/t\rfloor| &\le |m_0|\cdot t+|m_1|\cdot s \\ &\le b. \end{aligned} \]

さて、\(s = g \gt 0\), \(m_0 = x\) が得られたとしましょう。 この \(x\) の範囲について考えます。

ループ終了時に \(s = g\), \(t = 0\) になることから、その前のステップにおいて \(s \bmod t = 0\)(つまり \(s\) は \(t=g\) の倍数)だったことがわかります。 よって、適当な整数 \(k\), \(m_0\) を用いて次のように書けます(ここでの \(m_0\) は \(m_0=x\) のものではなく、前のステップのそれです)。 \[ \begin{cases} kg - m_0\cdot a \equiv 0, \\ g - x\cdot a \equiv 0. \\ \end{cases} \] このとき、例の不等式に対して、次のようになります。 \[ kg\cdot|x|+g\cdot|m_0| \le b. \] \(g\cdot|m_0|\ge 0\) なので、 \[ kg\cdot|x| \le b. \] それから、元々 \(b\gt a\) だったので、互除法の過程から \(kg\gt g\) です(\(kg=g\) なら \(b=a\) じゃないとやばくない?)。 \[ g\cdot|x| \lt kg\cdot|x| \le b. \] 長かったですが、結局、次のことがわかります。 \[ |x|\lt b/g. \]

以上より、\(-b/g\lt x\lt 0\) または \(0\le x\lt b/g\) とわかるので、所望の範囲 \(0\le x\lt b/g\) になるように(条件 \(ax\equiv g\) を満たしつつ)調整しておわりです。 \(x\lt 0\) なら \(x + b/g\)、\(x\ge 0\) なら \(x\) が所望のものです。

長かったですね。

internal::primitive_root_constexpr

素数 \(m\) に対して、法 \(m\) での最小の原始根 \(g\) を返します。

整数 \(g\) が次の条件を満たすとき、\(g\) は法 \(n\) における原始根と言います: \(\gcd(a, n)=1\) なる任意の \(a\) に対して、整数 \(k\) が存在して \(g^k\equiv a\pmod{n}\) が成り立つ。

原始根が存在する条件は、\(n\) が以下の形式で書けることと同値です。

  • \(n = 1, 2, 4\), or
  • \(n = p^k\) for some prime \(p\), or
  • \(n = 2p^k\) for some prime \(p\).

ACL では一般の場合ではなく、素数の場合のみに限定しています。 方針自体は単純で、\(g = 2, 3, \dots\) に対して「\(g\) は原始根である?」を高速に試し、yes であるものが見つかれば返すことをしています。

以下、\(m\) を素数として話します。 \(g\) が原始根であることは、\(\{g^0, g^1, \dots, g^{m -2}\} = \{1, 2, \dots, m -1\}\) であることと同値です。 Fermat の小定理から \(g^{m -1}\equiv 1\) ですが、指数部が \(1\) 以上 \(m -2\) 以下では \(1\equiv g^0\) にならないということですね。

判定の話

「\(g\) は原始根である?」を高速に行う方法を考えていきます。 直前に書いた話から、\(\Theta(m)\) 時間掛ければ簡単に判定できそうですが、もっと速くしてほしいです。

ある整数 \(k \lt m -1\) で初めて \(g^k \equiv 1\) になってしまった状況を考えます(すなわち、\(1\le k'\lt k\) について \(g^{k'}\not\equiv 1\) です)。 このとき、\(k\) は \(m -1\) の約数になっていることを示します。

\(k=1\) のときは自明なので除外して、\(k\gt 1\) とします。 背理法を使ってみましょうか。\(k\) が \(m -1\) の約数でないことは整数 \(i\ge 0\), \(1\le k'\lt k\) を用いて \(m -1 = i\cdot k+k'\) と書けます。 \[ \begin{aligned} g^{m -1} &= g^{i\cdot k+k'} \\ &\equiv (g^k)^i \cdot g^{k'} \\ &\equiv 1^i \cdot g^{k'} \\ &\equiv g^{k'} \not\equiv 1. \end{aligned} \]

これは \(g^{m -1}\equiv 1\) に反しています。おわり。

ということで、\(m -1\) の各約数 \(d\lt m -1\) に対して \(g^d\not\equiv 1\) を試せばいいのですが、もう少しよくできます。 \(m -1 = p_1^{a_1} \cdots p_s^{a_s}\) と素因数分解できたとき、\(1\le j\le s\) に対して \( (m -1)/p_j\) の形のものだけ試せばよいです。

\(m -1\) のある約数 \(d\lt m -1\) を考えます。 ある \(j\) に対して \(d\) の倍数であるような \( (m -1)/p_j\) が存在します*7。 すなわち、\(d\cdot k = (m -1)/p_j\) と書けます。

いま、\(g^d\equiv 1\) が成り立つとすると、次のようになります。 \[ \begin{aligned} g^{(m -1)/p_j} &= g^{d\cdot k} \\ &= (g^d)^k \\ &\equiv 1^k = 1. \end{aligned} \] よって、\( (m -1)/p_j\) の形のものさえ調べれば十分とわかりました。

ここまでをまとめると、次のような方法になります。

  1. 素因数分解 \(m -1 = p_1^{a_1}\cdots p_s^{a_s}\) を求める。
  2. \(g = 2, 3, \dots\) に対して、以下を試す。
    • ある \(j\) について \(g^{(m -1)/p_j} \equiv 1\) であれば no
    • そうでなければ yes で、その \(g\) が答えとなる

補足の話

ACL 中に \(m\) が素数であることを要求する旨が書かれているため、それを仮定したことを書きました。 実際には、\(m -1\) の部分を Euler's totient function \(\phi(m)\) で置き換えることで似たような議論ができる箇所が多くあります。

Note:素数 \(m\) に対して \(\phi(m)=m -1\) です。

詳しくは ここ を見るとよいと思います。

計算量の話

整数 \(n\) の素因数の種類数を \(\omega(n)\) と書く*8ことにし、\(r = \omega(m -1)\) とおきます。

各判定には \(O(r\cdot \log(m))\) かかります。

一般化された Riemann 予想の下では、\(g = O(r^4\cdot(\log(r)+1)^4\cdot \log(m)^2)\) であることが知られているようです。 また \(\omega(n) = O(\log(n)/\log(\log(n)))\) が成り立つようなので、\(g = O(\log(m)^6)\) です。

\(m -1\) の素因数分解にかかる時間を \(F(m -1)\) とすると、全体の計算量は \(O(F(m -1)+\log(m)^8/\log(\log(m)))\) 時間です...?? \(g\) はかなり loose な見積もりなはずなので、実際にはこんなにはかからないはずです...?

実際には \(F(m -1) = O(\sqrt{m})\) の部分がボトルネックとなりそうです。 \(O(\sqrt{\sqrt{m}})\) の素因数分解も知られています(こことか)が、どうでしょうね。

\(g\) の見積もりについては この論文 にあります。 読めていないです。

internal::primitive_root

こちらもテンプレート定数です。

internal::floor_sum_unsigned

(追記 2021/01/19:これ により追加されました)

\(m\) の制約を見落としていて恥ずかしいコメントをしてしまいました......

floor_sum の入力を非負整数に限ったバージョンです。 floor_sum については後述しますが、以下の値を求める関数です。

\[ f(n, m, a, b) = \sum_{i=0}^{n-1} \left\lfloor\frac{ai+b}{m}\right\rfloor. \]

元々は \(a\) と \(b\) の範囲に制約があったのですが、それを取り払うために追加されました。 行っていることは、後で説明する(元々の)floor_sum と同じなので、ここでは省略します。

追記おわり。

これで internal_math は終わりです。

pow_mod

はい出ました。

barrett を用いることで、除算の回数を抑えます。 Barrett reduction を用いる以外は普通の繰り返し二乗法です。

internal::pow_mod_constexpr では barrett を用いていませんが、この関数はコンパイル時に呼ばれるのが主なはずだからかな?と思っています。 barrett は実行時の高速化用なので*9

internal::is_prime_constexpr も同じ事情だと思います。

inv_mod

\(x\) と \(m \ge 1\) を受け取り、\(x^{-1}\) が存在すると仮定してそれを返します。 \(x^{-1}\) が存在する条件は \(\gcd(x, m) = 1\) です。 これが成り立たない場合は、assert(false) です*10

internal::inv_gcd を使います。 これの返り値が(仮定の下で)\( (1, x^{-1})\) です。

crt

ある未知の整数 \(y\) があります。 条件 \(y\equiv r_i\pmod{m_i}\) を満たす \( (r_i, m_i)\) (\(1\le i\le n\)) を与えます。 このとき \(m = \lcm_i m_i\) として、\(y\equiv r \pmod{m}\) なる \(r\) を探し、\( (r, m)\) を返します。 存在しなければその旨を返します。\(O(n)\) 時間です。

こうした \(0\le r\lt m\) が一意であることが中国剰余定理 (Chinese Remainder Theorem; CRT) から言えます。 元々の CRT は「\(m_0\), \(m_1\) が互いに素ならば \(r\) が一意に存在する」と言っていると思います。 ここでは、互いに素の制約が無く、存在判定も行う状況を考えます*11

\(n = 2\) の場合のみ考えます。 一般の場合には、\( (r_1, m_1), (r_2, m_2)\) に対する答えを \( (r, m)\) として \( (r, m), (r_3, m_3)\) を求め、... というのを順に繰り返していけばよいです。 ACL の実装では \( (r_0, m_0) = (0, 1)\) とおいてループしていますね*12

雑に実装すると、計算途中の値が大きくなってオーバーフローしがちなんですが、\(m=\lcm_i m_i\) を超えないように注意がなされています。 なので、それに気をつけながら説明していきます。

コード 中にコメントがあるので、それを読んでもいいと思います。

解の構成の話

さて、以下の説明では、\( (r_0, m_0), (r_1, m_1)\) を受け取り \( (r, m)\) を返すものとして説明します。 適当な前処理により \(0\le r_0\lt m_0\)、\(0\le r_1\lt m_1\)、\(m_0\ge m_1\) としておきます。

まず、\(m_0\) が \(m_1\) の倍数である場合を考えます。 \(m = m_0\) であり、\(m_0\) に関する条件は自明に成り立ちます。 \(r_0\not\equiv r_1\pmod{m_1}\) なら解なしです。 そうでないとき、\( (r, m) = (r_0, m_0)\) です。

残りの場合を考えます。 求めたい値 \(r\) に対して次の式が成り立っていてほしいです(そういう話をしているので)。 \[ \begin{cases} r\bmod m_0 = r_0, \\ r\bmod m_1 = r_1. \end{cases} \] 一つ目の式から、\(x\) が存在して \(r = x\cdot m_0+r_0\) と書けます。 これと二つ目の式から、次のことが言えます。 \[ (x\cdot m_0+r_0)\bmod m_1 = r_1. \] これを \(x\) について解くのが目標です。 \[ (x\cdot m_0) \bmod m_1 = (r_1 - r_0) \bmod m_1. \] 右辺も \(m_1\) で割ったあまりとしている理由は、\(r_1-r_0\ge 0\) とは限らないためです。

気持ちとしては、\(m_0\) で両辺を割りたいです。 \(g = \gcd(m_0, m_1)\) として、\(m_0 = u_0 g\)、\(m_1 = u_1 g\) とします。 \[ (x\cdot u_0 g)\bmod u_1 g = (r_1-r_0) \bmod u_1 g. \] 左辺は \(g\) の倍数であることがわかるので、右辺も \(g\) の倍数である必要があります。 すなわち、\(r_1\not\equiv r_0\pmod{g}\) なら解なしです。

\(r_1\equiv r_0\) なら \(r_1-r_0\) は \(g\) で割れるので、次のようになります。 \[ (x\cdot u_0)\bmod u_1 = \left(\frac{r_1-r_0}{g}\right) \bmod u_1. \] 右辺は、「\( (r_1-r_0)/g\) を \(u_1\) で割ったあまり」であって「\(r_1-r_0\) を法 \(u_1\) の下で \(g\) で割ったもの」ではないことに注意してください。 そもそも \(g^{-1}\pmod{u_1}\) は存在するとは限らないので。

ここまできたら、もう合同式にしちゃっていいです。 \[ x\cdot u_0 \equiv \left(\frac{r_1-r_0}{g}\right) \pmod{u_1}. \\ \] \(u_0\) と \(u_1\) は互いに素なので \(u_0^{-1}\pmod{u_1}\) は存在します。 実装の際は、\(g = \gcd(m_0, m_1)\) を求める際に internal::inv_gcd を使うことで \( (g, u_0^{-1})\) をまとめて得られます。 \[ x \equiv \left(\frac{r_1-r_0}{g}\right)\cdot u_0^{-1} \pmod{u_1}. \] この \(x\) のうち \(0\le x\lt u_1\) のものを使って \(r = x\cdot m_0+r_0\) を計算すればよいです。 また、\(u_1 = m_1/g\) より、\(m = m_0\cdot u_1\) です。

値の範囲の話

ここまでは計算方法の話をしていました。 あとは、絶対値が \(m = \lcm(m_0, m_1)\) 以下の値の演算で済ませられることを示します。 これにより、\(m\) がオーバーフローしない限り、オーバーフローが起きないことが言えます。

実際に計算する必要がある値は次の通りです。

  • \(r_1-r_0\),
  • \(x = (r_1-r_0)/g\cdot u_0^{-1} \bmod u_1\),
  • \(r = x\cdot m_0+r_0\),
  • \(m = m_0\cdot u_1\).

いま \(m_0\gt m_1\ge 1\) であり、以下の式が成り立つことに注意しておきます。 \[ \lcm(m_0, m_1)\ge 2\cdot\max\{m_0, m_1\}. \]

三角不等式とか、この大小関係とかを使います。 \[ \begin{aligned} |r_1-r_0| &\le |r_1|+|r_0| \\ &\lt m_1 + m_0 \\ &\lt 2\cdot\max\{m_0, m_1\} \\ &\le \lcm(m_0, m_1) = m. \end{aligned} \]

\(|r_1-r_0|\lt m\) より \(|r_1-r_0|/g\lt m\) はすぐわかります。 また、次が成り立ちます(掛け算の際にいちいち \(\bmod u_1\) してもよいといういつもの)。 \[ \left(\frac{r_1-r_0}{g}\right)\cdot u_0^{-1} \bmod u_1 = \left(\left(\frac{r_1-r_0}{g}\right)\bmod u_1\right) \cdot u_0^{-1} \bmod u_1 \] \( ( (r_1-r_0)/g)\bmod u_1\) と \(u_0^{-1}\) はそれぞれ \(u_1\) 未満なので、\(u_1^2\lt m\) を示せば十分です。 \[ \begin{aligned} u_1^2 &= (m_1/g)^2 \\ &\lt (m_0\cdot m_1)/g^2 \\ &\le (m_0\cdot m_1)/g \\ &= \lcm(m_0, m_1) = m. \end{aligned} \]

次いきます。 \[ \begin{aligned} |r| &\le |r_0| + |m_0\cdot x| \\ &\lt m_0 + m_0\cdot |x| \\ &\le m_0 + m_0\cdot (u_1-1) \\ &= m_0 + m_0\cdot u_1 - m_0 \\ &= m. \end{aligned} \]

\(m = m_0\cdot u_1\) は特にいいですね。 正整数同士の乗算なので、右辺の各々より左辺は小さくならないです。

以上により、示したいことたちが示せました。

floor_sum

以下のものを求めます。

\[ f(n, m, a, b) = \sum_{i=0}^{n-1} \left\lfloor \frac{ai+b}{m}\right\rfloor. \]

詳しい解説は ここ に書きました。 大まかな流れは次のような感じです。

まず、\(a\gets a\bmod m\), \(b\gets b\bmod m\) とし、\(0\le a, b\lt m\) の場合に帰着させます。 このとき、\(0\le \lfloor (ai+b)/m\rfloor\le \lfloor(an+b)/m\rfloor\) であり、いろいろすると、次のような形に帰着できます。 \[ \sum_{i=0}^{\lfloor(an+b)/m\rfloor-1} \left\lfloor \frac{a'i+b'}{m'}\right\rfloor. \] この \( (a', m')\) が \( (m, a\bmod m)\) であり、\(f(n, m, a, b)\) が \(f(\lfloor(an+b)/m\rfloor, a\bmod m, m, b')\) に帰着されます。

\(m\) と \(a\) に対して互除法のようになっているので、それに関する計算量で抑えられます。 \(n\) も \(m\) や \(a\) と共に減っていき、\(O(\log(\min\{a, m, n\}))\) くらいの計算量です。

(追記 2021/01/19:\(a\), \(b\) の制約がなくなった件について補足)

上のリンク先で \(0\le a\lt m\) などに帰着させるパートがありますが、\(a\lt 0\) の場合も同様に帰着できます。 ただし、C++ の除算の仕様に関して注意が必要です。 internal::safe_mod を用いることで、\(\lfloor a/m\rfloor\) を \( (a-(a\bmod m))/m\) として計算しています。

余談

実は、ACLfloor_sumはえびちゃんが 関与 しています(うれしいね)。

Improve floor_sum
Improve floor_sum [Merged]

おわり

おわりです。お疲れさまでした。

*1:他にも Montgomery reduction などがあります。

*2:ローカルに置いた場合は最適化してくれた記憶があります。

*3:入力に上限があることを仮定した方法であり、漸近記法は適切ではないかもしれません?

*4:実はここ自明ではないかも。

*5:\(a^{d\cdot 2^{i-1}}\equiv \pm 1\) ならそこでループが終わっているはずなので。

*6:えびちゃんも該当します。

*7:\(m -1\) にならないように適当な素因数を掛けていくことを考えればよいです。

*8:漸近記法の \(\omega(n)\) とは別です。ややこしい。

*9:コンパイル時間を定数倍高速化したい局面があれば話は変わるかもしれません?

*10:マクロの状況によっては実行時エラーになったり、嘘が返されたりします。

*11:CRT は一意に存在することを言う定理であって、解の構成には触れていないはずなので、名前が適切かはあまりわかりません。代替案もわかりませんが。

*12:ここいるのかな? ループ一回ぶん損してそう? とも思うけど、事前条件の確認漏れとかを防ぐのには適していそうです。