Advent Calendar Contest 2020 の 18 日目として、ecasdqina さん作の問題の tester を担当しました。tester 解は、writer 解と少しアプローチが異なっていたので紹介したいと思います。また、コンテスタントの提出を見て発見した、形式的べき級数を一切使用しないアプローチについても書いています。参加してくれた皆さんのコードが大変参考になりました、ありがとうございました。
問題概要
日本語なので原文参照。ProblemId = 5633 - yukicoder
解法 (1)
まず、問題を言い換えます。以下の問題の答えは元の問題の答えと等しいです。
- を満たす について、以下の操作を行ってできるボールの列としてありうるのは何通りかを計算し、その合計を報告せよ。列は、ある位置のボールについて色が異なったり、書いてある値が異なるときに区別される。
- の中から相異なる 個の整数を選ぶ。
- 区別できない 個の 赤い ボールを用意する。それぞれのボールに対し、上で選んだ 種類の整数のうちいずれか 1 つを書き込む。このとき、 種類全ての整数について、その整数が書かれたボールが少なくとも つ存在しなければならない。
- 区別できない 個の 青い ボールを用意する。それぞれのボールに対し、 以上 以下の整数のうちいずれか 1 つを書き込む。
- 合計 個のボールを 1 列に並べる。
問題文で問われている操作は「長さ の数列を作る」→「数列の中から、ちょうど 種類になるようにいくつか要素を選ぶ」という手順ですが、この言い換えでは「値が 種類存在する数列を作る」→「作った数列に対していくつか要素を挿入して長さ にする」という手順をイメージしています。問題文の操作を逆にしたようなものです。
「 長さが であって、 種類中 種類の値が含まれているような数列の総数」とおき、これを用いると、解は となります。上の問題で言うところの 2 番目の手順の通り数を、まるまる でおいてしまっている、ということです。
さて、 が実際に求められないと解けたことにはなりませんが、これはどのように求められるでしょうか?とりあえず二乗を許容することにすると、以下のような DP が立ちます。
これを高速化することを考えます。この見た目のまま高速化するのは難しいので、多項式として考えます。以下のように、 に関する多項式を定義します。
これを用いて先述の DP を表現すると以下のように変形できます。
と との間の漸化式が得られました。さらに変形してみます。
よって、答えは となります。分母にある多項式は分割統治のように FFT (NTT) を適用して多項式を作ってから inv を適用すればよく、これらは十分高速に対処可能です。
ソースコード (1)
ライブラリはもっていなかったので beet さんのものを拝借しました (ありがとうございます)
ソースコード本体 (折りたたんでいます)
int main() { int N, M, K; cin >> N >> M >> K; NTT<2> ntt; using mint = NTT<2>::M; auto conv = [&](auto as, auto bs) { return ntt.multiply(as, bs); }; FormalPowerSeries<mint> FPS(conv); Enumeration<mint> comb; comb.init(200010); auto go = [&](auto &&self, int l, int r) -> vector<mint> { if(r - l == 1) { return {mint(1), mint(-r)}; } int m = (l + r) / 2; return ntt.multiply(self(self, l, m), self(self, m, r)); }; vector<mint> A(K+1); A[K] = mint(1); for(int i=1; i<=K; i++) A[K] *= mint(i); vector<mint> B = FPS.inv(go(go, 0, K), N+1); vector<mint> X = ntt.multiply(A, B); mint ans(0); for(int i=K; i<=N; i++) { mint ways = X[i]; ways *= comb.C(M, K); ways *= comb.C(N, i) * mint(M).pow(N-i); ans += ways; } cout << ans << endl; return 0; }
解法 (2)
まず、解法 (1) と同様の言い換えをします (赤い ボールと 青い ボールのお話です)。
さて、赤い ボールについては、ちょうど 種類の整数から構成されなければいけませんでした。これについて少し条件を緩く (?) しつつ、包除原理の方針で考えていくことにします。
本来選ぶべき 種類の整数のうち、 種類を 絶対に選ばない ようにし、あとの 種類についてはどうでもよい (選んでも選ばなくてもよい) ときの場合の数を考えます。これを数えるには「赤い ボール 種類・青い ボール 種類の中から好きに選ぶことを 回行うときの通り数」を求め、具体的に 種類の中からどの 種類を選択したかを考慮すればよいため、 通りである、と立式できます。
あとは の偶奇に応じて足したり引いたりすればよいです。赤い ボールに対応させる整数をどう選択するかは 通りあるので、それを掛けるのを忘れないようにしてください。
ソースコード (2)
見ての通り、この方針であれば形式的べき級数を一切使わずに通すことができます。
using mint = ModInt<MOD>; int main() { Combination<mint> comb(200010); int N, M, K; scanf("%d%d%d", &N, &M, &K); mint ans(0); for(int i=0; i<=K; i++) { mint a = comb.C(K, i) * mint(M+K-i).pow(N); if(i % 2) ans -= a; else ans += a; } ans *= comb.C(M, K); printf("%lld\n", ans.v); return 0; }
tester でしたが、形式的べき級数に頼らない解法は全く思いついていませんでした・・・。とても勉強になりました。