第3回 ドワンゴからの挑戦状 本戦 B - ニワンゴくんの約数
区間[l,r)の約数の数がわかっているとき、区間[l,r+1),[l,r−1)等について約数の数を求めるのは比較的容易なので、Moのアルゴリズムを使う。具体的な伸縮操作は、以下のようにする。
積xlxl+1...xr−1の素因数分解をp1e1...pkek、約数の総数をD (=(e1+1)...(ek+1))とし、新たにxr:=p1δ1...pkδkを掛けた時の約数の総数を求めたい。このとき、いちいち新しい総数(e1+δ1+1)...(ek+δk+1)を再計算する必要はない。例えばp1δ1を掛けると、約数の総数はD⋅(e1+1)−1(e1+δ1+1)に変わるので、xrの素因数として現れる素数だけ扱えばよい。区間を縮小する場合も同様である。
計算量について。xiの素因数の種類がもっとも多くなるのはxiが素数階乗の場合で、prime omega functionの記事によればこのとき素因数の種類は∼logxi/loglogxiらしい。また、snukeさんの記事によるとMoのアルゴリズムの伸縮の総数はO(NQ)なので、X=maxixiとすると全体の計算量はO(NQ⋅logX/loglogX)。
TLが2.5秒なのでこれで間に合ってほしいところだが、けっこうぎりぎりだった。以下は細かい部分。
- 各xiの素因数分解のテーブルは事前に作っておく。この際、素数のリストから素朴に試し割りする方法は遅いので、いわゆるosa_k法を使う。O(NlogX)。
- 逆元の計算も含むので、事前にmod109+7での逆元のテーブルを作っておく。 ひとつのxiにつき、同じ素因数は高々⌊log2105⌋=16しか増えないので、16⋅105までの逆元がわかればよい。オーダーで書くとO(XlogX)。
- Moのアルゴリズムは偶数番目ブロック/奇数番目ブロックでクエリの右端の昇順/降順になるようにソートすると定数倍がかなり良くなる。ei1333さんの記事 の「その 1: 定数倍改善」に載っている。
想定解とか
TBE
素因数として2だけ、あるいは2,、3だけ除外して事前に累積和をとって扱うと、伸縮操作が軽くなって速くなる。
もっと大胆に、Xまでの素数を累積和で扱ってよいらしい。 この場合、各xiはXより大きい素因数を高々1つしか持たないので、伸縮操作がO(1)になる。Xまでの素数の数がX/logXくらいだから、累積和のテーブルを作る事前処理にO(NX/logX)かかり、クエリで累積和を処理するのにO(QX/logX)かかる。全体の計算量はO(NQ+(N+Q)X/logX)。確かにこちらのほうがよさそう。
ただ、実際にはXよりも小さい数にとどめたほうが速いようだった。一般化して、X1/α (α≥1)までの素数を累積和で扱うことにする。各xiはX1/αより大きい素因数を高々α−1種類しか持たないので、伸縮操作はO(α)になる。X1/αまでの素数の数がX1/α/logXくらいだから、累積和のテーブルを作る事前処理にO(NX1/α/logX)かかり、クエリで累積和を処理するのにO(QX1/α/logX)かかる。全体の計算量はO(αNQ+(N+Q)X1/α/logX)。定数がわからないとαの最適値も決まらなそう。
SBCL メモ
- SBCLの
destructuring-bind
が遅い問題はある程度認識していたのだが、AtCoderの古いSBCL(1.1.14)ではさらに顕著に遅いらしく、TLEする原因の1つになっていた。1.2.14でいちおう解決したってことでいいのかな。それなら7月の言語更新でSBCLの最新版が入れば問題なさそう。