第4回では, 先頭固定テクニックとぼくが言う, 主に定数個入力数え上げで平等性が高い状況のときに使えるDPについて書きます.
問題1
例題4-1 個の区別する玉を個の区別しない箱に入れる方法の数はいくつですか. ただし, 各箱には必ず個以上の玉を入れるものとします.
つまり, 第2種Stirling数を求めてください.
ただし, 答えはで割った余りで出力してください.時間制限:2秒、メモリ制限:1024 MB
制約
入力
N K
考察
を個の区別する玉を個の区別しない箱に各箱1個以上入れる方法とします.
次ののDPが知られています.
mod = 998244353 n,k = map(int,input().split()) dp = [[0] * (k+1) for j in range(n+1)] dp[0][0] = 1 for i in range(n): for j in range(k): dp[i+1][j+1] = (j+1) * dp[i][j+1] + dp[i][j] dp[i+1][j+1] %= mod print(dp[n][k])
今回は, 先頭固定テクニックで解いてみます. について考えます. ある1つの箱について考えます. その箱は個の玉を含んでいるものとします. このとき, ある玉の組み合わせを固定すると, その組み合わせを含むものは, 通りとなります.
それはそうなのですが, 実際にすべての組み合わせについてこれを足し合わせると(つまりを掛けると), 次の画像のように同じ結果になるものがダブルカウントしてしまうことがあります.
ダブルカウントしない方法として, 「今見ている配列のうち, 先頭を固定する」ということが考えられます. 先頭を固定すれば, 後続は通りですが, 先頭が固定されていることにより遷移先で先頭が証人になっているので, 後々になってダブルカウントすることはありません. これが先頭固定テクニックです.
は, のすべてについての総和となります. をで前計算することにより, このDPは となります.
ACコード
mod = 998244353 N = 400 fact = [1]*(N+1) factinv = [1]*(N+1) for i in range(2, N+1): fact[i] = fact[i-1] * i % mod factinv[-1] = pow(fact[-1], mod-2, mod) for i in range(N-1, 1, -1): factinv[i] = factinv[i+1] * (i+1) % mod def cmb(a, b): if (a < b) or (b < 0): return 0 return fact[a] * factinv[b] % mod * factinv[a-b] % mod n,k = map(int,input().split()) dp = [[0] * (k+1) for j in range(n+1)] dp[0][0] = 1 for i in range(1,n+1): for j in range(1,k+1): for m in range(1,i+1): dp[i][j] += dp[i-m][j-1] * cmb(i-1, m-1) % mod dp[i][j] %= mod print(dp[n][k])
時間計算量はあまりよくないけれど, 有名な遷移とはまた異なる遷移ができました.
余談ですが, と変形できます. に注目すると, 次の式が得られます.
(ただしは任意の非負整数)
問題2
例題4-2 ACM-ICPC Japan Alumni Group Contest 2019 2日目 H
問題文は長いので上のリンクを見てください.時間制限:2秒、メモリ制限:256 MB
制約
考察
長さの順列のスコアの総和を, スコアの2乗の総和をとします.
このとき, スコアの平均値は, 分散は です.
よって, を求めればokです.
順列を対称群の元だと見ると, 「サイクル森の1つの連結成分の数」は, 「巡回置換の長さ」(つまりは閉路からのみなる)と確認することができます.
その閉路に注目します. を求めてみましょう. を, 「先頭に含まれるグループの連結成分の数」とします. このとき, そのようなグループの決め方は個, さらにグループの中の閉路の作り方が個あります. (次対称群の長さの巡回置換は個あるので)
から乗算されるので, 閉路の大きさも掛けて, がに加算されます.
を考慮して, となります.
このように, 先頭固定テクニックは対称群(置換)と相性が良いです.
同様に です.
実はこれでコードができました. しかし間に合わないので工夫をします.
式変形をすると,
で, 順序を変えると
同様に
です.
を持つ変数を管理すれば, この5つは, を1つ増やすごとにで更新できます. よって累積和的に各をで更新でき, 全体でで解けます.
ACコード
mod = 10**9 + 7 N = 100256 fact = [1]*(N+1) factinv = [1]*(N+1) for i in range(2, N+1): fact[i] = fact[i-1] * i % mod factinv[-1] = pow(fact[-1], mod-2, mod) for i in range(N-1, 1, -1): factinv[i] = factinv[i+1] * (i+1) % mod def cmb(a, b): if (a < b) or (b < 0): return 0 return fact[a] * factinv[b] % mod * factinv[a-b] % mod n = int(input()) a0 = 1 a1 = 0 b0 = 1 b1 = 0 b2 = 0 t = 0 s = 0 for i in range(1, n+1): t = fact[i-1] * (i*a0 - a1) % mod s = fact[i-1] * (i*i*b0 - 2*i*b1 + b2) % mod a0 = (a0 + t*factinv[i]) % mod a1 = (a1 + t*i*factinv[i]) % mod b0 = (b0 + s*factinv[i]) % mod b1 = (b1 + s*i*factinv[i]) % mod b2 = (b2 + s*i*i*factinv[i]) % mod a = t*factinv[n]%mod print((s - 2*t*a + a*a*fact[n])*factinv[n]%mod)