第2回は, 約数系包除を使うある問題について, 典型的な方法と, 包除原理を用いたもう1通りの解法を解説します.
次の問題を2通りで解いてみます.
例題2(想定Diff 1500)
自然数が与えられます. 集合の要素の数が以上である部分集合について, の最大公約数がであるものは何通りありますか?ただし, 答えは非常に大きくなる可能性があるのでで割った余りを求めてください.
時間制限:秒, メモリ制限:GB
制約:
入力:4 → 出力:10
の通りあります.
まずは, DP(動的計画法)を使う方法を考えてみます. 典型テクニックとして,
:の部分集合であって, 要素の数が以上で, そのすべての要素の最大公約数がちょうどであるものの個数
とします.
1つ目の解法は「約数系包除」と呼ばれているものです.
まず, の部分集合であって, 要素の数が以上で, 最大公約数がの倍数のものを数えることを考えます.
最大公約数がの倍数になるための必要十分条件は, の要素がすべての倍数になることです. これを示します.
まず, 最大公約数がの倍数ならば, 最大公約数は整数を用いてと表すことができます. 任意のの要素について, となる整数があります. これは, がの倍数であることを示します.
次に, の要素がすべての倍数ならば, はのすべての要素を割り切ります. よって, 最大公約数はの倍数になります.
今, の要素であって, の倍数であるものは個存在します.
そのうち, 要素の数が以上の部分集合は, とすると, 要素数の部分集合は個で, まず空集合個を除き, 次に要素数の集合個を除きます.
残ったのは 個で, これがの部分集合であって, 要素の数が以上で, 最大公約数がの倍数であるものの個数です.
さて, を求めます. 今, ですから, 上から順番に更新することで で更新できます.(もちろん, 再帰関数を使ってもよいです.)
このとき, でも偶然かうまくいってます. なので場合分けせずにそのまま計算します.
なので, 未満を考慮すればよいので有限回でプログラムを終わらせることができます.
ここで, 各について, を計算するのにそれぞれかかります. で, についてこれを計算するので, です. を計算すればよいのですが, です. これが主にボトルネックになります.
よって, 結局を求めるのにかかります.
mod = 1000000007 n = int(input()) #dp[i] = gcd(X)がiであるようなものの数. t = n//2 dp = [0 for i in range(t+1)] for i in range(t,0,-1): dp[i] = (pow(2,n//i,mod)-n//i-1)%mod for j in range(2*i,t+1,i): dp[i] = (dp[i] - dp[j]) % mod print(dp[1])
次に, 包除原理を使って問題を解きます. 包除原理は, いくつかの部分集合の和集合の要素数について, それらの積集合の要素数を利用して求めるテクニックで, ダブルカウントを防ぎたいときに有利です.
高校数学の数Aで集合の要素をやった方は, 次の式に見覚えがあるかもしれません.
これで2個の場合は分かるのではないかと思います. しかし一般の場合に拡張しようとすると大変です. 個々のエリアの要素について, その要素を含む集合をすべて着目します.
たとえば, 2個の集合の場合, の要素については, しか寄与しません . それに対し の要素については, の2つが寄与します. 普通に足すと, の各要素が重複してカウントされ, 求めた結果がでません.
そこで, をマイナスの寄与と考えることで, 寄与が個分になり, ダブルカウントを防ぐことができます.
いま, がちょうど個の集合に含まれる要素であるとして, 寄与をちょうど1個にする方法を考えます.
1個の積集合はの個です. ただし, は二項係数です.
2個の積集合は などの 個です. 個の集合から個選ぶことを考えるとわかりやすいと思います.
同様にして, 個の積集合は全部で 個です.
ここで, が偶数のときプラスに寄与, が奇数のときマイナスに寄与すると考えます. このとき, がカウントされる回数は
となります. 二項定理より となります.
よって, であり, なんとすべての寄与を合計するととなります. これが包除原理の根本にある仕組みです.
これで, の式 で の寄与がプラスである理由が分かったと思います. 同様に4個の場合の式も作れて,
となります.
そろそろ本題として, もとの問題の答えを求めてみましょう. すべての要素数以上の部分集合の個数から, 最大公約数が以上であり, 要素数以上の部分集合の個数を除けばよいです.
後者を数え上げることを考えてみましょう. 1は素数を約数に持ちませんが, 2以上の整数は必ず素数を約数に持ちます. 最大公約数が素数の倍数(すなわち, 要素すべてがの倍数)であり, 要素数以上の部分集合すべての集合をとします.
求めたいのはです. なら, ですので, 実際は有限個の和集合となります.
実際に包除原理を愚直に適用すると, 以下の最大の素数を, 素数の個数をとして, の空でない部分集合をすべて考える必要があるので, かかり間に合いません. TLEコードは以下です. 前半ではエラトステネスのふるいをしていて, 後半ではDFSのように探索しています.
n = int(input()) sieve = [] flag = [0] * (n+1) for i in range(2, n+1): if flag[i] == 0: sieve.append(i) for j in range(i, n+1, i): flag[j] = 1 mod = 10**9 + 7 ans = (pow(2, n, mod) - n - 1) % mod size = len(sieve) mada = [(1, 0, 0, 1)] #(現在の値, sieveをここまで見た, 探索するか, パリティ) while mada: i, ind, tosearch, parity = mada.pop() if tosearch: ans = (ans + parity * (pow(2, n//i, mod) - n//i - 1)) % mod if ind < size: mada.append((i * sieve[ind], ind + 1, 1, - parity)) mada.append((i, ind + 1, 0, parity)) print(ans)
そこで, は要素の数が以上で, 最大公約数がの倍数であるものの個数です. よって, の場合はとなり, 考慮する必要がありません.
よって, の積集合をだんだん探索していき, その積がを超えた瞬間にストップすることを考えます. これを枝刈りといいます.
また, で, 正整数に対し, ならばが成立します. よって積がを超えた瞬間,「を選択しないという選択肢」も排除可能です. これで時間計算量をさらに短縮することができます.
これを踏まえると, 次のようになります. 時間計算量はわかりませんが, エラトステネスのふるいで, DFSでおそらく程度です.
n = int(input()) sieve = [] flag = [0] * (n+1) for i in range(2, n+1): if flag[i] == 0: sieve.append(i) for j in range(i, n+1, i): flag[j] = 1 mod = 10**9 + 7 ans = (pow(2, n, mod) - n - 1) % mod size = len(sieve) mada = [(1, 0, 0, 1)] #(現在の値, sieveをここまで見た, 探索するか, パリティ) while mada: i, ind, tosearch, parity = mada.pop() if tosearch: ans = (ans + parity * (pow(2, n//i, mod) - n//i - 1)) % mod if ind < size: if i * sieve[ind] <= n: mada.append((i * sieve[ind], ind + 1, 1, - parity)) mada.append((i, ind + 1, 0, parity)) print(ans)
かなり速いものができました.
今回, 1つの問題に対して2つの解法を紹介しましたが, 1つ目の解法は「約数系包除」と呼ばれていますが, 包除原理を学んだあとだと, 違う感じで聞こえますね.