[dp 記錄]agc024E Sequence Growing Hard
題意:
給定一個序列,每次往其中插入 \([1,k]\) 間的一數,要求字典序遞增,求方案數。設插入 \(i\) 數的數列為 \(A_i\),存在一個 \(A_i\) 不同即兩插入方案不同,否則即一致。
\(n,k \leq 300\)
新增一個數,該數後面第一個與它數字的數字小於它本身。注意到連續相同數可以插入到任意位置,不妨欽定插到最後面,則每次轉移時 \(x_i > x_{i+1}\)。
這樣仍然不好做。無論列舉填進去的數還是列舉填的位置都需要知道 \(n\) 個數的詳細資訊。
注意到操作序列能唯一確認方案,我們渴望找到什麼能與操作序列雙射。
看到操作序列,尤其轉移與大小相關,可以考慮上樹。
考慮怎樣計數操作,不會做。試想更簡單的問題:對於一個確定的序列,怎樣刻畫“插入”操作並對其計數?根據性質,一個數會插入在另一個數前。因此,可以考慮向位置靠後的數連邊。這樣,每個位置都有一個出邊,只有虛點沒有出邊。所以這是一棵樹,樹上每個節點的父親的權值都嚴格小於它。
—— https://www.luogu.com.cn/blog/Yansuan-HCl/solution-at-agc024-e
這樣還是不易於統計,再做一步轉化,把每個字元的位置用它被插入的時間表示。那麼我們需要做的即是對每次操作,選擇之前的一次操作表示這次插在之前插的位置前面。特別地,選擇 \(0\) 操作表示這次插在最後面。
——
https://www.luogu.com.cn/blog/0123456-3456789/solution-at3962
一次插入操作可以用它插入時在它後面的數刻畫。所以連邊,自然構成一棵樹。
定義一個節點的權值為二元組 \((val, id)\),每次操作新建點掛到它後面的點下面,\(id\) 為插入時間,\(val\) 為權值。則 \(\forall v\) 為 \(u\) 兒子,\(val_v > val_u,id_v > id_u\)。容易驗證這樣的樹與操作序列雙射。這是漂亮的有傳遞性的偏序關係,無後效性的子結構,可以進行 dp 了。
記錄三維(大小也是應該關心的)還是多了些。發現每次轉移相當於加一棵子樹進來,欽定那是 \(id\)
\(id\) 是排列,關於排列的平移是唯一的,所以欽定 \(id\) 最小是好做的。
設 \(dp_{v,x}\) 表示根節點 \(val=v,siz=x\) 的樹的方案數。轉移:
\[dp_{v,x} = \sum_{i=v+1}^{k} \sum_{y=1}^{x-1} dp_{i,y}dp_{v,x-y}\binom{x-2}{y-1} \]後面的組合數是因為兩邊的 \(val\) 的相對順序沒有限制,除了兩個根以外的相對順序都可以隨意選擇。
把 \(i\) 這一維字尾和滾掉即可做到 \(O(n^3)\)。
#include <cstdio>
using namespace std;
const int M = 305;
int mod;
int add(int a, int b) {a += b; return a > mod ? a-mod : a;}
int mins(int a, int b) {a -= b; return a < 0 ? a+mod : a;}
void addn(int &x, int y) {x += y; if(x > mod) x -= mod;}
int n, dp[M][M], k, C[M][M], s[M][M];
int main(){
scanf("%d %d %d", &n, &k, &mod);
C[0][0] = 1;
for(int i = 1; i <= n; i++) {
C[i][0] = 1;
for(int j = 1; j <= i; j++)
C[i][j] = add(C[i-1][j-1], C[i-1][j]);
printf("\n");
}
for(int i = 0; i <= k; i++) dp[i][1] = 1, s[i][1] = k-i+1;
for(int x = 2; x <= n+1; x++) {
for(int v = k; v >= 0; v--) {
for(int y = 1; y < x; y++)
addn(dp[v][x], 1ll * C[x-2][y-1] * s[v+1][y] % mod * dp[v][x-y] % mod);
s[v][x] = add(s[v+1][x], dp[v][x]);
}
}
printf("%d\n", dp[0][n+1]);
}