裏紙

ほぼ競プロ、たまに日記

CF 900 D - Unusual Sequences

問題

Problem - D - Codeforces

問題概要

数列a_1 , a_2 , \ldots , a_n (a_i \ge 1)について、 gcd( a_1 , a_2 , \ldots , a_n ) = xかつ\sum_{i=1}^{n} a_i = yを満たすような数列aが何種類存在するか、10^9 + 7で割った余りを求めよ。

  •  1 \le x,y \le 10^9

イデア

まず、数列の全てのgcdがxであるということは、数列の全ての要素はxの倍数であるということになる。よってその和yxの倍数でないと数列aは存在し得ない。

つまり、yxで割り切れない時、答えは0であり、割り切れる時、条件は gcd( a_1 , a_2 , \ldots , a_n ) = 1かつ\sum_{i=1}^{n} a_i = \frac{y}{x}と言い換えられる。

では、数列の全要素の和がtで全要素のgcdが1であるような数列の個数をf(t)で表す。つまり、この問題で求めたいものはf( \frac{y}{x} )である。

また、数列の全要素の和がtである(gcdは問わない)ような数列の個数をg(t)とすると、fgには以下のような関係がある:

 \displaystyle g(t) = \sum_{d|t} f( \frac{t}{d} )

上の式からf(t)だけをΣの外に出して移項すると、

 \displaystyle f(t) = g(t) - \sum_{d|t, d \ge 2} f( \frac{t}{d} )

また、g(t)は直接計算することが出来る。t個の1が並んでいる状況を想定して、t-1個の間にそれぞれカンマを入れるか否かによって全ての数列を対応付ける事できる。よって、g(t) = 2^{t-1}である。以上のことから、

 \displaystyle f(t) = 2^{t-1} - \sum_{d|t, d \ge 2} f( \frac{t}{d} )

となる。tの約数の個数はO( \sqrt[3 ]{t} )であるから、後はこれを再帰的にメモ化しながら計算すれば十分間に合う(本当に十分速かった(15ms)(submission))。

メビウスの反転公式による高速化

この式に注目しよう:

 \displaystyle g(t) = \sum_{d|t} f( \frac{t}{d} )

tの約数dを列挙して、 f( \frac{t}{d} )の和を考えているが、dtの約数全てを動く時、\frac{t}{d}によって列挙されるものもまたtの約数全てということになるので、以下の用に書いても差し支えないことに気づく:

 \displaystyle g(t) = \sum_{d|t} f(d)

上の式にはメビウスの反転公式が適用できる:

 \displaystyle f(t) = \sum_{d|t} \mu (\frac{t}{d}) g(d)

これによって、tの約数を列挙し、直接fを計算できる(solve2で実装)。

実装(C++)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define rep(i,n) for(int (i)=0;(i)<(int)(n);++(i))
#define all(x) (x).begin(),(x).end()
#define pb push_back
#define fi first
#define se second

const ll mod = 1e9+7;

ll mod_pow(ll x, ll n){
    ll ret = 1;
    while(n){
        if(n&1) (ret*=x)%=mod;
        (x*=x)%=mod;
        n>>=1;
    }
    return ret;
}

// nの約数におけるメビウス関数の値 O(sqrt(n))
map<int,int> moebius(int n){
    vector<int> primes;
    // nの素因数を列挙
    for(int i=2; i*i<=n; ++i){
        if(n%i==0) primes.push_back(i);
        while(n%i==0) n/=i;
    }
    if(n>1) primes.push_back(n);

    map<int,int> ret;
    int SZ = primes.size();
    // 2^SZ通りを試す(nの約数の個数よりは少ない)
    rep(i,1<<SZ){
        int mu = 1, d = 1;
        rep(j,SZ){
            if(i>>j&1){
                mu *= -1;
                d *= primes[j];
            }
        }
        ret[d] = mu;
    }
    return ret;
}

map<ll,ll> m1;
ll solve1(ll n){
    if(m1.count(n)) return m1[n];

    ll ret = mod_pow(2,n-1);
    for(int i=1; i*i<=n; ++i){
        if(n%i==0){
            if(i!=n) ret = (ret - solve1(i) + mod)%mod;
            if(n/i!=n && n/i!=i) ret = (ret - solve1(n/i) + mod)%mod;
        }
    }
    return m1[n] = ret;
}

ll solve2(ll n){
    ll ret = 0;
    map<int,int> m = moebius(n);
    for(int i=1; i*i<=n; ++i){
        if(n%i==0){
            (ret += m[n/i]*mod_pow(2,i-1)+mod) %= mod;
            if(n/i!=i) (ret += m[i]*mod_pow(2,n/i-1)+mod) %= mod;
        }
    }
    return ret;
}

int main(){
    ll x,y;
    cin >>x >>y;

    ll ans = 0;
    if(y%x==0){
        // ans = solve1(y/x);
        ans = solve2(y/x);
    }
    cout << ans << endl;
    return 0;
}