AGC060 C: Large Heap
解法
[1] 条件の言い換え
対称性により、不等号の向きをすべて反転させても求める確率は変わりません。後の計算がやりやすくなるので、不等号の向きをすべて反転させて考えます。
長さ の実数列 が以下の条件を満たすとき、 は 上の列であると呼ぶことにします。
上の列 がさらに以下の条件をすべて満たすとき、 は 上のヒープ的な列であると呼ぶことにします。
このとき、求める確率は 上のヒープ的な列 を一様ランダムに つ選んだときに となる確率に一致します。これは以下のようにしてわかります。
- 上の列 がヒープ的であるか、また であるかは、 の間の大小関係のみに依存する。
- 上の列を一様ランダムに つ選んだとき、等しい要素が存在する確率は であるから無視できる。また、各要素が相異なるとき、要素間の大小関係としてありうるものは 通りあるが、それらは等確率で現れる。
さらに、以下のような根付き木 を考えます。
- 個の頂点を持つ。
- 根は頂点 である。
- について、頂点 は頂点 の つの子を持つ。
このとき、 上の列 がヒープ的であることは、木 の各頂点 に を書いたとき、頂点 が頂点 の親であるような木 の頂点の組 すべてについて頂点 に書かれた値が頂点 に書かれた値より大きいことと同値になります。
以上により、順列に関する問題を、木の各頂点に区間 に含まれる実数を書く問題に言い換えることができました。
上の列 を一様ランダムに選んだとき、 がヒープ的である確率を , がヒープ的でかつ である確率を とおくと、答えは となります。よって、 をそれぞれ求めればよいです。
[2] の計算
を求めるときは、実数で考えるのではなく、順列のままで考えると求めやすいです。
以下の事実が典型として知られています。
頂点の根付き木がある。長さ の順列 を一様ランダムに つ選んだとき、根以外の全ての頂点 に対し、その親を とすると となる確率は、頂点 の部分木の頂点数を とおくと である。
木 は深さ の完全二分木です。 に対し、深さ の頂点は 個存在し、それらの頂点の部分木の頂点数はすべて です。したがって、 となります。
もしくは、最終的に用いる値が であることから、漸化式 を用いると簡単に計算することができます。
[3] の計算
頂点 に書く値については特別な制約があるため、最初に固定しておきます。具体的には、頂点 には必ず を、頂点 には必ず を書くことにします。ただし、 とします。
以下のような木 DP を考えます。
- : 木 の頂点 の部分木の各頂点に区間 に含まれる実数を独立に一様ランダムに書くが、頂点 に数を書く場合には必ず を書き、頂点 に数を書く場合には必ず を書くとき、頂点 に書いた数が 以下であり、かつ頂点 の部分木で大小関係の条件が満たされている確率
ただし、頂点 の部分木で大小関係の条件が満たされているとは、頂点 の部分木に含まれる各頂点 に対し頂点 に書いた数が頂点 の子に書いた数より大きくなることと定義します。また、 は の範囲のみ考えるとします。
DP の遷移を考えます。
頂点 が葉で、 または である場合
頂点 が葉なので、大小関係に関する制約はありません。
のとき、頂点 に書かれる数は必ず なので、頂点 に書かれる数が 以下になる確率は のとき , のとき となります。よって、 となります。
同様に、 のときは となります。
頂点 が葉で、 でも でもない場合
上と同じく、大小関係に関する制約はないので、頂点 に書かれる数は区間 から一様ランダムに選ばれます。よって、頂点 に書かれる数が 以下になる確率は となるので、 となります。
頂点 が葉でなく、 または である場合
のとき、頂点 に書かれる数は必ず です。よって、 のとき明らかに となります。
とします。頂点 の部分木で大小関係の条件が満たされることは、以下の条件がともに成り立つことと同値になります。
- 頂点 に書かれた数が 以下であり、かつ頂点 の部分木で大小関係の条件が満たされている。
- 頂点 に書かれた数が 以下であり、かつ頂点 の部分木で大小関係の条件が満たされている。
よって、頂点 の部分木で大小関係の条件が満たされる確率は です。
以上より、遷移は となります。
同様に、 の場合の遷移は となります。
頂点 が葉でなく、 でも でもない場合
頂点 に書く数を とします。頂点 の部分木で大小関係の条件が満たされることは、以下の条件がともに成り立つことと同値になります。
- 頂点 に書かれた数が 以下であり、かつ頂点 の部分木で大小関係の条件が満たされている。
- 頂点 に書かれた数が 以下であり、かつ頂点 の部分木で大小関係の条件が満たされている。
これが成り立つ確率は です。
は区間 から一様ランダムに選ばれますが、 である必要があるので、遷移は となります。
は、木 の頂点 に を、頂点 に頂点 を、他の各頂点に区間 に含まれる実数を独立に一様ランダムに選んで書くとき、各辺の大小関係の条件が満たされる確率となります。よって、 となります。
[4] 計算例
例として、サンプル の結果を計算する過程を示します。
- である。
- 頂点 は葉で頂点 であるので、 である。以後、このような場合に と省略する。
- 頂点 は葉で頂点 でも頂点 でもないので、 である。頂点 も同様である。
- である。
- である。頂点 も同様である。
- である。
- である。
- である。
- である。
- である。これを計算すると となる。
- 答えは である。
[5] 計算量の削減
頂点は 個あるため、すべての頂点について を計算することはできません。しかし、木 には構造が同じ部分が多数存在することを利用すると、 を計算する必要のある頂点を 個に減らすことができます。各頂点の は の多項式となりますが、それらの特徴と計算手順は以下のようになります。ただし、 とします。
- を の先祖でも の先祖でもない頂点とすると、 は頂点 の高さのみに依存する のみの単項式である。 は 時間の前計算のもと、 時間で得ることができる。
- を の先祖で の先祖でない頂点とする。また、頂点 の距離を とおくと、 は の 項の多項式であり、次数はすべて等しい。 のとき、 の子で部分木に を含むほうを とおくと、 は から 時間で計算できる。
- を の先祖で の先祖でない頂点とする。また、頂点 の距離を とおくと、 は の 項の多項式であり、次数はすべて等しい。 のとき、 の子で部分木に を含むほうを とおくと、 は から 時間で計算できる。
これらを用いると、 を 時間で計算することができます。また、 は の 項の多項式、 は の 項の多項式なので、 は 項の多項式となります。
なので、積分を項ごとに分解すると、 を計算することに帰着されます。
なので、積分の順序を交換して
となります。これで を 時間で求めることができたので、答えも 時間で求めることができます。
実装
#include <bits/stdc++.h> using namespace std; const long long MOD = 998244353; long long modpow(long long a, long long b){ long long ans = 1; while (b > 0){ if (b % 2 == 1){ ans *= a; ans %= MOD; } a *= a; a %= MOD; b /= 2; } return ans; } long long modinv(long long a){ a %= MOD; return modpow(a, MOD - 2); } vector<long long> POW2; struct poly{ vector<tuple<long long, long long, long long>> P; poly(): P(){ } int size(){ return P.size(); } void multiply_monomial(poly f){ int N = P.size(); for (int i = 0; i < N; i++){ get<0>(P[i]) *= get<0>(f.P[0]); get<0>(P[i]) %= MOD; get<1>(P[i]) += get<1>(f.P[0]); get<1>(P[i]) %= MOD; get<2>(P[i]) += get<2>(f.P[0]); get<2>(P[i]) %= MOD; } } void integrate(){ int N = P.size(); for (int i = 0; i < N; i++){ get<1>(P[i])++; get<1>(P[i]) %= MOD; get<0>(P[i]) *= modinv(get<1>(P[i])); get<0>(P[i]) %= MOD; } } void integrate_a(){ int N = P.size(); long long sum = 0; long long deg = (get<1>(P[0]) + get<2>(P[0]) + 1) % MOD; for (int i = 0; i < N; i++){ get<1>(P[i])++; get<1>(P[i]) %= MOD; get<0>(P[i]) *= modinv(get<1>(P[i])); get<0>(P[i]) %= MOD; sum += MOD - get<0>(P[i]); sum %= MOD; } P.push_back(make_tuple(sum, 0, deg)); } }; int main(){ int N, A, B; cin >> N >> A >> B; POW2.resize(N + 1); POW2[0] = 1; for (int i = 0; i < N; i++){ POW2[i + 1] = POW2[i] * 2 % MOD; } vector<poly> f(N); f[N - 1].P.push_back(make_tuple(1, 1, 0)); for (int i = N - 2; i >= 0; i--){ f[i] = f[i + 1]; f[i].multiply_monomial(f[i]); f[i].integrate(); } poly ga; if (A == N - 1){ ga.P.push_back(make_tuple(1, 0, 0)); } else { ga = f[A + 1]; ga.multiply_monomial(ga); swap(get<1>(ga.P[0]), get<2>(ga.P[0])); } for (int i = A; i >= 2; i--){ ga.multiply_monomial(f[i]); ga.integrate_a(); } poly gb; if (B == N - 1){ gb.P.push_back(make_tuple(1, 0, 0)); } else { gb = f[B + 1]; gb.multiply_monomial(gb); swap(get<1>(gb.P[0]), get<2>(gb.P[0])); } for (int i = B; i >= 2; i--){ gb.multiply_monomial(f[i]); gb.integrate_a(); } int N1 = ga.size(); int N2 = gb.size(); long long p2 = 0; vector<long long> T(N2); for (int i = 0; i < N2; i++){ T[i] = modinv(get<2>(gb.P[i]) + 1); } for (int i = 0; i < N1; i++){ for (int j = 0; j < N2; j++){ long long c = get<0>(ga.P[i]) * get<0>(gb.P[j]) % MOD; long long x = (get<1>(ga.P[i]) + get<1>(gb.P[j])) % MOD; long long a = get<2>(ga.P[i]); long long b = get<2>(gb.P[j]); p2 += T[j] * modinv(a + b + 2) % MOD * modinv(x + a + b + 3) % MOD * c % MOD; p2 %= MOD; } } long long p1inv = 1; for (int i = 2; i <= N; i++){ p1inv *= p1inv; p1inv %= MOD; p1inv *= POW2[i] - 1; p1inv %= MOD; } cout << p2 * p1inv % MOD << endl; }