問題リンク
問題概要
以上 以下の整数からなる長さ の数列 に対して、 を、すべての要素が である長さ の数列 に以下の操作を繰り返して を得るのに必要な最小の操作回数とする。
- 整数 と、正整数 を1つ選び、各 を で置き換える。
ありうる 通りの すべてに関する の和を で求めよ。
思考過程
こういう「ありうるすべての○○についての総和」みたいな問題は、まとめて数え上げられることの方が多い気がする。 の位置に を追加したとき、操作回数が増えるのは、
- これまでに が使われていない場合。
- これまでに が使われたが、その後 より小さい数字が使われている場合
の2パターンに分けられる。 を固定し、それぞれを丁寧に数える。
パターン1
かつ、すべての について であるような の数は、
に等しい。
パターン2
ある が存在して であり、かつ、ある が存在して となるような の数を求める。 である最大の の値(これを とおく)で場合分けをする。 を固定したとき、
- については は自由に決められる。
- は仮定より に等しい。
- については、「『すべての について 』でなく、かつすべての について である」ものを数えればいい。
また、これらは互いに独立に決められるので、 を固定したときのパターン数は、
に等しい。
解法
あとは上の2式の に関する総和を取ればよい。このままだと かかってしまうが、 に関する和は等比数列の和の公式を使って以下のようにまとめられるので、 で解ける。
実装
TLがやや厳しめで、毎回累乗や逆元を計算していると間に合わないので、前計算が必要だった(1146ms)。
#include <bits/stdc++.h> #include <atcoder/modint> using namespace std; using namespace atcoder; using mint = modint998244353; int main(){ int n, m; cin >> n >> m; if(m == 1){ cout << 1 << '\n'; return 0; } //逆元の前計算 vector<mint> inv(5010, 0); for(int i = 1; i < 5010; i++){ inv[i] = mint(i).inv(); } //累乗の前計算 vector<vector<mint>> pow(5010, vector<mint>(5010, 0)); for(int i = 0; i < 5010; i++){ mint p = 1; for(int j = 0; j < 5010; j++){ pow[i][j] = p; p *= i; } } //1 - (m/(m-i))^jの前計算 vector<vector<mint>> r(5010, vector<mint>(5010)); for(int i = 1; i < m; i++){ mint t = 1, u = inv[m-i] * m; for(int j = 0; j <= n; j++){ r[i][j] = 1 - t; t *= u; } } for(int j = 0; j <= n; j++) r[m][j] = 1; //r[i][j]の逆元の前計算 vector<mint> rInv(5010); for(int i = 0; i < 5010; i++){ if(r[i][1] == 0) continue; rInv[i] = r[i][1].inv(); } mint ans = 0; for(int i = 1; i <= n; i++){ for(int x = 1; x <= m; x++){ ans += pow[m-1][i-1] * pow[m][n-i]; if(i <= 2) continue; ans += pow[m-1][i-2] * r[1][i-2] * rInv[1] * pow[m][n-i]; ans -= pow[m-x][i-2] * r[x][i-2] * rInv[x] * pow[m][n-i]; } } cout << ans.val() << '\n'; }
感想
考察自体はすんなり行けたが、添字合わせや定数倍高速化でものすごく時間を取られた。