ホームに戻る
 TopCoder の傾向と対策4(C++編:数え上げ)

0、はじめに

Div2 の hard は数え上げの問題がでます。
単に数え上げという視点で考えてみます。

1、1000000007 で割った余りを答えよ問題対策

 TopCoder では整数の64ビット制限を考慮してくれる。
よって、long long より大きな数値を考えなくても良い。
この数値を超える答えが出る場合には、
 答えを 1000000007 で割った余りを答えよ、とされる。
足し算においては足し算を行うごとに % 1000000007 する。

引き算のときは先に引かれる数に 1000000007 を足しておく。
そこから引く数を引いて % 1000000007 する。
こうすると結果がマイナスにならず結果がおかしくならない。

掛け算する場合は足し算と同様である。

A * B をするとき、
A、B それぞれを 1000000007 で割ったときの商と余りを
商A、余A、商B、余Bとする。

A * B = (商A * 1000000007 + 余A) * (商B * 1000000007 + 余B)
      = (商A * 商B * 1000000007 + (商A * 余B + 商B * 余A))
          * 1000000007 + 余A * 余B

よって、

(A * B) % 1000000007 = (A % 1000000007) * (B % 1000000007)

ゆえに、掛け算のときもそのたびに % 1000000007 すればよい。

和、差、積においては 1000000007 のような素数以外の数字でも成り立つ。

割り算の場合は複雑である。

まず、余りについての標記の説明をする。

7を4で割ると余りは3である。
これを数式で書くといかのようになる。

7 ≡ 3 (mod 4)

さて、フェルマーの小定理は以下のようである。

a^(p-1) ≡ 1 (mod p) // ただし、p は素数で、a は p で割りきれない

m/n (m は n で割り切れる)の計算をする場合、
フェルマーの小定理より次のように書ける。

n^(p - 2) * n ≡ 1 (mod p)

このとき n の逆元は 1 / n^(p - 2) である。
よって、p = 1000000007 のとき、

(m / n) % 1000000007 = m * n^(1000000007 - 2) % 1000000007

簡単にいうと割り切れる m / n を 1000000007 で割った余りは、
(m * n^1000000005) % 1000000007 とできる。

これは 1000000007 が素数であるからできることである。
素数でなければ計算式の中で割り算を使うことができない。

さて、n^1000000005 のところを考えてみる。
単純に 1000000005 回掛け算をすると時間が足りない。

このとき次のようにすると圧倒的に計算量を縮めることができる。 
1000000005 は2進数で 111011100110101100101000000101 と書ける。
よって、

n 、 n^2 、 n^4 、 n^8 ... を先に計算しておき、

そのうちの 1000000005 の2進数を下の桁から 1 であるものを見ていき、

n * n^4 * n^512 * n^2048 * ...

と 1 であるもののみ掛け算すると30回ほどのループで計算できる。

コードで書くと次のようになる。

long long bp[40];

long long binpow(long long a, long long p){
  bp[0] = 1;
  bp[1] = a;
  for(int i = 2; i << 40; ++i){
    bp[i] = (bp[i - 1] * bp[i - 1]) % 1000000007;
  }
  int d = 1;
  long long res = 1;

  while(p){
    if(p & 1){
      res = (res * bp[d]) % 1000000007;
    }
    d++;
    p = p >> 1;
  }

  return res;
}

次に、a^b を 1000000007 で割った余りを考える。
b が 1000000005 のような値であれば上記までの説明で計算できる。
しかし、b が100!のような値であった場合には使えない。
単純に((a%1000000007)^(100!%1000000007))%1000000007は間違いである。
b がとても大きな値である場合もフェルマーの小定理を元に考えることができる。

例えば、a^2000000000 を考える。

a^(p-1) ≡ 1 (mod p)

であるから、

a^2000000000 = a^(1000000006 + 999999994)
= a^(1000000007-1)*a^999999994 ≡ 1 (mod 1000000007)*a^999999994

つまり、

a^bを1000000007で割った余りは、

((a%1000000007)^(b%1000000006))%1000000007

とできる。

もちろんフェルマーの小定理が元なので1000000007が素数であること。
a は p で割りきれないことが必要である。
また、1000000006 は素数で無いので、b の中で割り算ができないことに注意する。

まとめ、

・和と積はそのつど p で割った余りを使えば良い。(p は素数でなくても良い)
・差はマイナスにならないように先に p を足しておく。(p は素数でなくても良い)
・商は割り切れる場合のみ、割る数の p-2 乗を掛ける。(p は素数であること!)
・累乗の指数部分は、p-1 で割った余りを使う。(基数がpで割れない!p は素数であること!)

Java の BigInteger を使うのもルール違反では無い。
また Python は特別なことをせずに大きな値を計算できる。
よって、時には BigInteger や Pyhton を使う戦略もアリである。
ただ、値が大きくなるたび計算が重たくなる。
やはり途中で適度に % 1000000007 をとったほうが良い。
もちろん最後に % 1000000007 するのを忘れずに。

2、場合の数

場合の数は k^n や n! や nPr や nCr を組み合わせる問題。
該当する場合を計数する問題は良く出る。
計数する数はほとんどの場合が巨大な数値になる。
よって、1000000007 で割った余りを返す場合の対策が必要である。

 問題

昇順の整数の数列がある。同じ値が続いても良い。
Xの部分は値が不明である。
Xに自由に整数を埋めていいとするとき、
ありえる数列の可能性は何通りか?
答えを1000000007で割った余りを求めよ。

1,3,3,X,45,87,X,X,X,2017,X,X,91021503

考え方

例えば最初のXは3から45の可能性があり43通りの可能性がある。
次の3つのXは87から2017の間に3つの値が入る。
これは1931のうちから重複組み合わせで3つの値を選ぶことになる。
よって、nCrの(1931+3-1)C3で計算できる。
次の2つのXは(91019487+2-1)C2で計算できる。
よって、43*(1932)C3*(91019488)C2通りが答えになる。
途中の計算では1000000007の余りを考慮した計算が必要。

3、行列累乗

 問題

f(0) = 1、f(1) = 1、f(x) = f(x - 1) + f(x - 2) のとき、
f(1000000000) を 1000000007 で割った余りを求めよ。

解を出すには 1000000000 回のループを必要としそうだが、
以下の方法を使うとループを 9 回にまで縮めることができる。

#define MOD 1000000007

vector<vector<long long> > mul(vector<vector<long long> > a, vector<vector<long long> > b){
  vector<vector<long long> > v(a.size(), vector<long long>(b[0].size(), 0));
  for(long long i = 0; i < a.size(); i++){
    for(long long k = 0; k < b.size(); k++){
      for(long long j = 0; j < b[0].size(); j++){
        v[i][j] = (v[i][j] + (a[i][k] * b[k][j]) % MOD) % MOD;
      }
    }
  }

  return v;
}

vector<vector<long long> > binpow(vector<vector<long long> > a, long long n){
  vector<vector<long long> > v(a.size(), vector<long long>(a.size(), 0));

  for(long long i = 0; i < a.size(); i++){
    v[i][i] = 1;
  }

  while(n > 0){
    if(n & 1){
      v = mul(v, a);
    }

    a = mul(a, a);
    n >>= 1;
  }

  return v;
}

long long n = 1000000000;

vector<vector<long long> > a(2, vector<long long>(2, 0));

a[0][0] = 1; a[0][1] = 1;
a[1][0] = 1; a[1][1] = 0;

a = binpow(a, n + 1);

std::cout << a[1][0] << std::endl;

この方法を使えば

f(0) = 1、f(1) = 2、f(2) = 3、
f(x) = f(x - 1) + 2 * f(x - 2) + 3 * f(x - 3) + 4

といったさらに複雑な計算も瞬時にできる。

4、包除原理

包除原理は事象の重複を取り除くために有効な考え方である。

例えば、AとBという2つの事象が起こりうる場合、
少なくともAかBのどちらかが起こる確率を求めよというとき。
Aの起きる確率とBの起きる確率を足し算するのはときに間違いになる。
なぜならAとBが同時に起きる場合が考慮されていない。
もしAとBが同時に起きる可能性があるのであれば、

Aの起きる確率+Bの起きる確率−AとBが同時に起きる確率

が少なくともAかBのどちらかが起こる確率になる。

これは事象がABCである場合

A+B+C−AB−AC−BC+ABC

になる。

事象がABCDである場合、

A+B+C+D−AB−AC−ADーBC−BD−CD
+ABC+ABD+ACD+BCD−ABCD

になる。

これは単に満たす事象が奇数であれば足し算し、
満たす事象が偶数であれば引き算をすればいいと考えられる。

 メビウス関数

n=60 のとき

メビウス関数はmapで次のようになる。

1->1,2*3->1,2*5->1,3*5->1
2->-1,3->-1,5->-1,2*3*5->-1
それ以外は0

実行時間はsqrt(n)である。
素因数分解したときの要素が奇数か偶数かの判定が可能。

map<int,int> moebius(int n){
  map<int,int> ret;
  vector<int> primes;

  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);

  int m = primes.size();
  for(int i = 0; i < (1<<m); i++){
    int mu = 1;
    int d = 1;
    for(int j = 0; j < m; j++){
      if(i>>j & 1){
        mu *= -1;
        d *= primes[j];
      }
    }
    ret[d] = mu;
  }
  return ret;
}

inserted by FC2 system