楽しい問題だった!!!
問題概要
すぬけ君は黒板と 個の整数からなる集合 を持っています。すぬけ君は黒板に整数 を書いたのち、以下の操作を 回行います。
- から要素を 1 つ選んで取り除く。
- その後、現在黒板に書かれた数を 、 から取り除かれた整数を として、黒板に書かれた数を に書き換える。
から要素を取り除く順序は 通りのそれぞれの場合について、 回の操作後に黒板に書かれている数を求め、その総和を で割ったあまりを求めてください。
制約
考えたこと
まず、「 通りの順列それぞれについての〜の総和を求めよ」という設定の問題に対して、われわれが使えるテクノロジーがあんまりない!!!!!!!
- なんかうまい順序見つけてまとめて DP
とかくらいしかない感じ。「〜の最小値を求めよ」とかだったらフローがありうるけど、 で割ったあまりを求めるタイプの問題でフローも考えづらい。。。
そうすると結局は、
順列をいい感じにまとめ上げて数え上げるような DP
というのを探さざるをえないことになる。
問題の構造を見る
今回の問題で色々手を動かして見えて来ることは、
- 例えば で割ったあまりをとる操作を行った後に、 のような より大きい数で割ったあまりをとっても値は変化しない
ということ。これはかなり強い性質で、例えば
11, 19, 6, 8, 4, 13
という列に対してあまりを取る操作をするのは、
11, 6, 4
という列に対してあまりを取る操作をするのと同じことになる。これはいかにも活用できそうな構造である。
順列を走査する DP の代表: 挿入 DP
通りの順列をすべて調べ上げる DP として代表的なのは
- bitDP (今回は制約が なので無理)
- 挿入 DP
- 箱根駅伝みたいな DP
あたりがあると思う。今回は挿入 DP がマッチする感じ。考え方としては
- 最初の 個の順列は、最初の 個の順列に対して、 番目の要素をどこかに挿入したものである
という見方をする。
3, 1, 2, 5, 4
のような 5 個の要素の順列があったら、6 個の要素の順列は「6」の挿入箇所をすべて試すことで全部調べることができる。
6, 3, 1, 2, 5, 4 3, 6, 1, 2, 5, 4 3, 1, 6, 2, 5, 4 3, 1, 2, 6, 5, 4 3, 1, 2, 5, 6, 4 3, 1, 2, 5, 4, 6
という感じ。こういう発想をすると解ける典型的な DP として EDPC の以下の問題がある:
今回の問題について
今回の問題では以上の考え方に加えて、 をあらかじめソートしておくことにする。そうするとすごく考えやすい。
例えば をあらかじめソートして
5, 6, 11, 13, 22
としておいて、最初の 4 個について 24 通りの順列についての総和がわかっているとき、22 を挿入する方法は実質 2 通りしかなくて
- 22 を先頭に挿入するとき
- 22 を先頭以外に挿入するとき
前者は特別扱いして考える必要があるものの、後者は挿入された 22 は結局無視されることになる。以上の考察を基にして
- dp[ i ][ x ] := スタート時点での整数が であるときに、最初の 個についての順列すべてを考えたときに得られる値の総和
とすると
- dp[ 0 ][ x ] = x
- dp[ i + 1 ][ x ] = dp[ i ][ x % s[ i ]] + i × dp[ i ][ x ]
という風になる。
#include <iostream> #include <vector> #include <algorithm> using namespace std; const int MOD = 1000000007; int main() { int N, X; cin >> N >> X; vector<int> S(N); for (int i = 0; i < N; ++i) cin >> S[i]; sort(S.begin(), S.end()); vector<vector<long long> > dp(N+1, vector<long long>(X+1, 0)); for (int x = 0; x <= X; ++x) dp[0][x] = x; for (int i = 0; i < N; ++i) { for (int x = 0; x <= X; ++x) { dp[i+1][x] += dp[i][x % S[i]]; dp[i+1][x] %= MOD; dp[i+1][x] += dp[i][x] * i % MOD; dp[i+1][x] %= MOD; } } cout << dp[N][X] << endl; }