いろんな解法がありそうなんな
問題概要 (ARC 102 E)
整数 が与えられる。各
に対して、
- どの
に対しても
を満たす整数組 の個数を
で割った余りを求めよ (
面サイコロを
個振るという設定)。
制約
解法 1: 漸化式を立ててそれを展開 (本番で通した解法)
例えば の場合は、
- 1 と 4 は同時には出現しない
- 2 と 3 は同時には出現しない
という条件とみなすことができる。 がどの場合でも、
- 1 と
は同時には出現しない
- 2 と
は同時には出現しない
- ...
と
は同時には出現しない
という形の条件になる。ここで、 となるのは構わないが、
に対して
となってはいけない。
の値は
に応じて決まる。さて、
:=
面サイコロを
個振るときに、「
が 1 回出たらその後
は出ない」というタイプの条件が
個付されている状況下での場合の数
として、 を再帰的に表すことを考える (注意: 多分ここから先は解法 2 の包除原理の方がわかりやすそう)。
のとき、それ以後
以外の
個しか使えないので
通り
のとき、それ以後
個しか使えないので
通り
- ...
のとき、
通り
- それ以外のとき、
個から
個をとる重複組合せなので
通り
これを足す。再帰計算の末端は
通り
という感じ。このままこのメモ化再帰を計算すると、 の計算量になってしまうので高速化する必要がある。この漸化式について、
のところが全部 0 になるまで展開を繰り返すと、各
について
×
を合算したものになっていることがわかる。この計算なら、各 について
個の和として計算できるので全体の計算量も
になる。
参考:
C : 各剰余が0か各剰余がk/2ならOK
— リッキー (@rickytheta) 2018年9月1日
D : 2^19より大きい場合は2^18 2^17 2^16 で構成して、そうでない場合は 2^17 2^16 2^15 で構成して後は1から辺を張る
E : 「ある値が2個未満」「ある値とある値が同時存在不可」は同じ条件とみなせて、数え上げ遷移がans(n,k) - ans(n-2,k) なので経路数数えてsum
「aが0個か1個」という条件を、(全通り数) - (aを2個以上使う通り数) と解釈して、1つの条件を解くのにans'(n,k)-ans'(n-2,k)という遷移を作ると、最終的に全条件を解いた終端からもとに戻るまでの経路数(パスカルの三角形)をかけてあげればDPしなくてもいけるかなと思って出したら通った
— リッキー (@rickytheta) 2018年9月1日
#include <iostream> using namespace std; const int MAX = 210000; const int MOD = 998244353; long long fac[MAX], finv[MAX], inv[MAX]; void COMinit(){ fac[0] = fac[1] = 1; finv[0] = finv[1] = 1; inv[1] = 1; for(int i = 2; i < MAX; i++){ fac[i] = fac[i-1] * i % MOD; inv[i] = MOD - inv[MOD%i] * (MOD/i) % MOD; finv[i] = finv[i-1] * inv[i] % MOD; } } long long COM(int n, int k){ if(n < k) return 0; if (n < 0 || k < 0) return 0; return fac[n] * (finv[k] * finv[n-k] % MOD) % MOD; } int main() { COMinit(); int K, N; cin >> K >> N; for (int query = 2; query <= K*2; ++query) { long long res = 0; int Q = query; if (query > K + 1) Q = (K+1)*2 - query; int a = Q/2; for (int i = 0; i <= a; ++i) { long long tmp = COM(a, i) * COM(K - a + N - i - 1, N - i) % MOD; res = (res + tmp) % MOD; } cout << res << endl; } }
解法 2: 包除原理
僕は上記の思考過程で は
についての
×
の総和であることを導いたが、これに近い式は包除原理で考えると自然に導出できる。 個の制約のうち、
個 (以上) を破ってしまうものは
×
通り
となる。これを の偶奇によって足し引きすればよい。微妙に異なる式だけど、多分、ちゃんと式変形すると同じになるんだと思う。
#include <iostream> using namespace std; const int MAX = 210000; const int MOD = 998244353; long long fac[MAX], finv[MAX], inv[MAX]; void COMinit(){ fac[0] = fac[1] = 1; finv[0] = finv[1] = 1; inv[1] = 1; for(int i = 2; i < MAX; i++){ fac[i] = fac[i-1] * i % MOD; inv[i] = MOD - inv[MOD%i] * (MOD/i) % MOD; finv[i] = finv[i-1] * inv[i] % MOD; } } long long COM(int n, int k){ if(n < k) return 0; if (n < 0 || k < 0) return 0; return fac[n] * (finv[k] * finv[n-k] % MOD) % MOD; } int main() { COMinit(); int K, N; cin >> K >> N; for (int query = 2; query <= K*2; ++query) { long long res = 0; int Q = query; if (query > K + 1) Q = (K+1)*2 - query; int q = Q/2; for (int i = 0; i <= q; ++i) { long long tmp = COM(q, i) * COM(K+N-i*2-1, N-i*2) % MOD; if (i & 1) res = (res - tmp + MOD) % MOD; else res = (res + tmp) % MOD; } cout << res << endl; } }
解法 3: 戻す DP
まだちゃんと理解していない...
参考:
Eはクエリごとに普通のdpを書いたら3乗ですが、実は隣接クエリのdpはとっても似たdpなので、戻すdpをしてごっそりサボれます
— (nは自然数) (@n_vip) 2018年9月1日