👊パンチ👊ゼータ変換👊パンチ👊
累積和はわかるけど、約数包除はややこしくてわからない!
競プロでgcd (最大公約数)・lcm (最小公倍数) をなんやかんやする問題が出たときに、何も考えずにゼータ変換のライブラリを貼って解く(「殴る」)ことができることがあるのを紹介します。ABC-EやABC-Fを速解きして暖色パフォが出せたらうれしいですね
※ ネタバレを多分に含むのでご注意ください
はじめに
約数集合での上位集合に対するゼータ変換 (以下ゼータ変換とします) とは、各素数が各添字に対応する多次元の累積和をとることで、添字gcd畳み込みなどの操作が高速にできたりする便利な変換です*1。愚直にやればになる色々な操作がになって速いです。これを使って、簡単な式変形で様々な問題を解くことができたりします。
方法・実装
ゼータ変換
具体的には、長さの数列に対して、
で定義される長さの数列に変換する操作がゼータ変換です。実装はエラトステネスの篩同様に
vector<bool> sieve(a.size(), true); for (size_t i = 2; i < a.size(); i++) { if (!sieve[i]) continue; for (size_t j = (a.size()-1)/i; j > 0; j--) { sieve[i*j] = false; a[j] += a[i*j]; } }
のように実装すると、計算量で変換することができます。
素数(≒次元)ごとに指数の大きい方から累積和をとるようなアルゴリズムになっていることが分かると思います。
参考: 高速ゼータ変換の約数版 O(N log(log(N))) - noshi91のメモ
参考: ゼータ変換・メビウス変換を理解する - Qiita
メビウス変換
メビウス変換 (ゼータ変換した配列を元に戻す変換) も累積和を元の配列に戻すように逆順に引いていくと
vector<bool> sieve(a.size(), true); for (size_t i = 2; i < a.size(); i++) { if (!sieve[i]) continue; for (size_t j = 1; i*j < a.size(); j++) { sieve[i*j] = false; a[j] -= a[i*j]; } }
のように実装できます。
GCD畳み込み
長さの配列があったときに、これをゼータ変換してそれぞれとします。の各成分をかけてメビウス変換 すると添字gcdで畳み込みができます。具体的には、
となるため、をメビウス変換すると配列が
のように得られます。この記事ではこれをgcd畳み込みと呼ぶことにします。
LCM畳み込み
ゼータ/メビウス変換をする際の累積和をとる向きを逆にすると添字lcmでの畳み込みができます。先ほどの配列が
のように得られます。しかし、lcmの値は大きくなって配列に収まりきらない場合が多く、あまり使われません。
実例
ABC106-B 105
atcoder.jp
を下位集合に対してゼータ変換することで以下のすべての正整数の約数の個数をで求められます。
ただ、制約がで全探索が通るのでゼータ変換を使うのは完全に👊パンチ👊です。制約がでも多分通ります。
ABC162-E Sum of gcd of Tuples (Hard)
atcoder.jp
なので、長さの配列を
のようにすると、をメビウス変換すればgcdがになる数列の数が求まります。そこでgcdをの倍数にするには各要素にの倍数を使えばいいので
が求まります。最後にをメビウス変換でに変換して
を求めて出力すればACです。の構築に繰り返し二乗法で、メビウス変換で、最後の答えがなのでで間に合います。
参考: [AtCoder 参加感想] 2020/04/12:ABC 162 | maspyのHP
参考: AtCoder頻出?貼るだけ高速ゼータ・メビウス変換&約数拡張 - ゆきノート(仮)
追記(2020/7/25):
熨斗袋さんからさらにパンチ力の高い解法を頂きました
Sum of gcd of Tuples は更に👊出来ますね
— 応用微積 (@noshi91) 2020年7月24日
まず 3 つの列 a, b, c を gcd 畳み込みすると、ゼータ変換を Z として
Z⁻¹(Z( Z⁻¹(Z(a)⚪︎Z(b)) )⚪︎Z(c))
になるんですがこれは
Z⁻¹(Z(a)⚪︎Z(b)⚪︎Z(c))
なので、結局複数の列を一気に畳めることが分かります
列 S を S[1] = S[2] = … S[K] = 1 とすると、
— 応用微積 (@noshi91) 2020年7月24日
Z⁻¹(Z(S)⚪︎Z(S)⚪︎ … (K 個) … ⚪︎Z(S))
を計算できればいいので、S をゼータ変換して各要素を k 乗してメビウス変換すると OK です
問題の式は
と変形できます。正整数のgcdは可換モノイドなので、gcd畳み込みも同様です。そうするとすべての項が1の配列をN個畳み込めば
が得られ、最初の変形した式に代入すれば値が求まることが分かります。
は、をゼータ変換したのち各項それぞれ乗してメビウス変換をすれば得られるので、解けました。計算量はです。
(追記おわり)
AGC038-C LCMs
atcoder.jp
まず、与えられた式を計算しやすいように変形すると
となります。二重のところがかかってしまうのでさらに変形します。
ここで、とすると、より
となります。ここで長さの配列をとしておき、同士のgcd畳み込みをすれば
となり、元の式が求まります。
以上を実装すると、の構築に、畳み込みに、最終的な値の計算にかかるので時間計算量はとなります。問題の制約はなので制限時間の2秒に間に合います。
参考: 添え字 gcd での畳み込みで AGC038-C を解く - noshi91のメモ
参考: agc038_c - wiki.kimiyuki.net
ABC020-D LCM Rush
atcoder.jp
これも少し工夫することでゼータ変換で解けます。求める値を式変形すると
となり、ここでが成立するようなはKの約数に限られることに注意すると、
ここでの部分をゼータ変換して考えると
これをすべてのについて求めておき、の約数だけを添字としてメビウス変換を行うとが求まります。これを元の式に代入すれば求まって、ACです。に含まれる相異な素因数の個数を、の約数の個数をとすると計算量はとなり、十分高速で2sに間に合います。
「みんなのプロコン 2018」決勝-A Uncommon
に対して
をそれぞれ求める問題です。これは、
で定義される配列のgcd畳み込みのの項をそれぞれ求めればいいです。ここでこれらをゼータ変換することを考えると、はに依存しないのでそのままゼータ変換してとすればいいのですが、がによって変わるのでこれをどうにかする必要があります。しかし、をゼータ変換してとすると、を満たすとき、そうでなければなので、先ほどのLCM Rush同様に添字の範囲をの約数に限定してをメビウス変換するとが得られます。ならば約数の個数は128を超えることがなく、また相異の素因数の個数は6を超えることがないため、でも2sに間に合います。
まとめ
包除がややこしくてわからなくても累積和の要領で殴れました。
いかがでしたか?かえってややこしくなってない?
参考
累積和を何も考えずに書けるようにする! - Qiita
ゼータ変換・メビウス変換を理解する - Qiita
高速ゼータ・メビウス変換 - Mister雑記
[数学] 畳み込み入門:Dirichlet積とゼータ変換・メビウス変換 | maspyのHP
ゼータ関数の定義と基本的な話 | 高校数学の美しい物語
fast zeta transform - wiki.kimiyuki.net
高速ゼータ変換/高速メビウス変換 - naoya_t@hatenablog
約数集合でのゼータ変換・メビウス変換的なやつと畳み込み - kazuma8128’s blog
高速ゼータ変換の約数版 O(N log(log(N))) - noshi91のメモ