ABC110 D - Factorization メモ
逆元の mod のとり方を知ったので自分用にメモ。
問題はこちらから
大まかな解き方
問題を解く上での考え方は単純。 の素因数の種類の数を として、
と素因数分解されるとします。
それぞれの素因数 が 個あるので、これらを 個のグループに分配します(0個のグループがあってもよい)。この分け方の総数は です(1列に並んだ 個の ◯ を 本の区切りで分けると考えればよいです、下図参照)。
この値を のそれぞれについて全てかけ合わせたものを 1e9+7 (= 1000000007)で割った余りが答えです。
要するに
が答えです。
問題点
を愚直に求めて mod をとろうとしても、値が long long でも扱えないほどクソでかくなるときが出てきます。 の時点でムリです。これを解決するにはどうすればいいでしょうか?
解法
の定義に除算が入っているので、通常では計算途中に mod 計算をすることができません。しかし、次のようにすれば除算でも mod をとることが出来ます。
ここからは の mod をとることについて考えましょう。
なので、 の mod をとることは の mod をとることと同義です。(ちなみに のことを の逆元と呼びます)
ここで、フェルマーの小定理より
は互いに素
この両辺に をかけて、
は互いに素
(p = 1e9+7 であり、1e9+7 は素数なので、a が p の倍数でない限り a, p は互いに素になります。今回の場合、制約上 a が p の倍数になることはないためフェルマーの小定理が適用できます)
以上より、 の答えを で割った余りは、 を で割った余りに等しいです。
の定義は次のように変形されます。
剰余を求めたいので、 を利用して
コード解説
ACコードは以下の通りです。(そこそこ長いです。もっと洗練できるかも)
#include <bits/stdc++.h> using namespace std; #define int long long // 簡略化のため longlong を int に const long long MOD = 1e9+7; int fact[150000], revfact[150000]; // fact[i]はiまでの階乗、revfact[i]はiまでの階乗の逆元 queue<int> prime_count; int powmod(int a, int p){ int res = 1, tmp = a; while(p != 0){ if(p % 2) res = (res*tmp) % MOD; tmp = (tmp*tmp) % MOD; p /= 2; } return res; } void factmod(){ fact[0] = revfact[0] = 1; for(int i = 1; i < 150000; i++){ fact[i] = (fact[i-1] * i) % MOD; revfact[i] = (revfact[i-1] * powmod(i, MOD-2)) % MOD; } } int nCrmod(int n, int r){ // nCr % MOD return (((fact[n] * revfact[r]) % MOD) * revfact[n-r]) % MOD; } signed main(){ // ここの signed を int にするとCEします int N, M; cin >> N >> M; for(int i = 2; i <= sqrt(M); i++){ // 素因数分解して指数を queue に突っ込む if(M % i == 0){ int cnt = 0; while(M % i == 0){ M /= i; cnt++; } prime_count.push(cnt); } if(M == 1) break; } if(M != 1) prime_count.push(1); // Mが素数の場合はqueueに1だけが入ることになります factmod(); int ans = 1; while(!prime_count.empty()){ int bi = prime_count.front(); ans *= nCrmod(N+bi-1, bi); ans %= MOD; prime_count.pop(); } cout << ans << endl; return 0; }
一部分ずつ解説していきます。
int powmod(int a, int p){ int res = 1, tmp = a; while(p != 0){ if(p % 2) res = (res*tmp) % MOD; tmp = (tmp*tmp) % MOD; p /= 2; } return res; }
(mod 1e9+7) を計算します。
今回の場合、 なので、通常の累乗の求め方(計算量 )ではTLEしてしまいます。
そこで、繰り返し二乗法という手法を使います。
例として について考えることにしましょう。 は次のように変形されます。
次に、指数である を2進数で表すと です。ここで、ビットが立っているところは を分解した時のそれぞれの指数に対応しています。
したがって、 は をそれぞれ計算すると求められます。
(僕の説明はわかりにくいので こちら を参考にすると良いと思います)
繰り返し二乗法を使うと、 が と高速に求められます。
void factmod(){ fact[0] = revfact[0] = 1; for(int i = 1; i < 150000; i++){ fact[i] = (fact[i-1] * i) % MOD; revfact[i] = (revfact[i-1] * powmod(i, MOD-2)) % MOD; } }
fact[i] は i までの階乗 (mod 1e9+7)の配列、revfact[i] は i までの階乗の逆元 (mod 1e9+7) の配列です。制約上、150000 ぐらいまで計算しておけば十分です。十分すぎるくらいです。
そのままの階乗の計算は特に言うことはありませんが、 の階乗の逆元の計算は
で求められます。
int nCrmod(int n, int r){ return (((fact[n] * revfact[r]) % MOD) * revfact[n-r]) % MOD; }
nCr (mod 1e9+7) を求めています。nCr の定義の通りに計算しています。
for(int i = 2; i <= sqrt(M); i++){ if(M % i == 0){ int cnt = 0; while(M % i == 0){ M /= i; cnt++; } prime_count.push(cnt); } if(M == 1) break; } if(M != 1) prime_count.push(1); // Mが素数の場合はqueueに1だけが入ることになります
を素因数分解し、指数のみを queue に突っ込んでいます(指数しか計算に利用しないためです。素因数分解をライブラリ化する場合は map で素因数を key にしておくとよさそう)。
の素因数分解では、 から までを調べれば十分です( が合成数の場合、 より大きい素因数は存在しないため)。 が素数かそうでないかは、for文を抜けた後の が であるかどうかで判断できます。
さいごに
今回は「フェルマーの小定理+繰り返し二乗法」で解きましたが、他にも解法はあります。
フェルマーの小定理+繰り返し二乗法 で逆元の mod をとる手法はよく出てくるので、ライブラリ化しておくと良いかも。
例えばこの問題だとライブラリコピペしてえい!するとささっと解けてしまいます。
atcoder.jp
<参考にしたサイト>