1. 程式人生 > 實用技巧 >AtCoder agc024_e - Sequence Growing Hard

AtCoder agc024_e - Sequence Growing Hard

tzc:“AtC風格的計數DP。”我就想問了,AtC的題怎麼可能不是AtC風格的呢?

洛谷題目頁面傳送門 & AtC題目頁面傳送門

給定\(n,m,p\),求有多少個值域為\([1,m]\cap\mathbb Z\)的序列\(n+1\)元組\((a_0,a_1,\cdots,a_n)\),滿足:

  1. \(\forall i\in[0,n],|a_i|=i\)
  2. \(\forall i\in[0,n)\)\(a_i\)\(a_{i+1}\)的子序列;
  3. \(\forall i\in[0,n),a_i<a_{i+1}\)

答案對\(p\)取模。

\(n,m\in[1,300],p\in\left[2,10^9\right]\)

顯然,原問題等價於,一個序列\(a\)一開始為空,\(n\)次每次往裡任意位置塞一個\([1,m]\)內的數,使得字典序大於原來的序列,求\(n\)次組成的序列組的個數(注意這裡不能說操作的數量,因為在不同位置插入相同數可能得到的序列一樣)。

考慮在位置\(x\)插入\(y\)合法的充要條件。若\(x=|a|+1\)顯然合法。剩下的顯然要滿足\([y]+a_{x,|a|}>a_{x,|a|}\)。若\(y\neq a_x\),那麼字典序相對大小決定在它們身上,所以\(y>a_x\)。否則的話,,,注意到若把\(y\)插入到往右數第一個滿足\(y\neq a_{x'}\)的位置\(x'\)

,那麼得到的序列是一樣的,為了去重,我們可以直接不考慮這種情況,反而更方便了。於是充要條件就是\(x=|a+1|\)\(y>a_x\)

分析完之後還是不會做。這是一個動態的插入問題,每次的\(a\)都會變。不妨將時間定格在最後一刻,所有數都各就各位了,再來考慮每個位置\(i\)上的值\(v_i\)與被插入的時間(時間戳)\(dfn_i\)

稍微轉化一下,此時一個\((v,dfn)\)序列合法當且僅當,\(\forall i\in[1,n],v_i>v_{\min\limits_{j>i,dfn_j<dfn_i}\{j\}}\)(為了方便,設\(v_{n+1}=dfn_{n+1}=0\)

,合理性顯然)。考慮\(v_i\)\(>\)的那個東西固定的一個,就是使得\(dfn_x=1\)\(x\)。它的條件是\(v_x>v_{n+1}=0\)。然後不難發現,\(\forall i\in[1,x),\min\limits_{j>i,dfn_j<dfn_i}\{j\}\in[1,x]\),對應的,\(\forall i\in(x,n],\min\limits_{j>i,dfn_j<dfn_i}\{j\}\in[x+1,n+1]\)。這樣就可以把問題分解成\(x\)左右兩半相似的問題,其中左邊的值域變成\((v_x,m]\),右邊值域不變。問題就迎刃而解了。

考慮DP。設\(dp_{i,j}\)表示\((v,dfn)\)序列的大小為\(i\),值域為\([j,m]\)的方案數。邊界\(dp_{0,j}=1\),目標\(dp_{n,1}\)。列舉\(dfn_x=1\)\(x\)\(v_x\)兩個東西,就可以轉移了,注意到若左右各找一個方案,那麼左右兩半內部的\(dfn\)相對大小固定,而跨越左右兩半的\(dfn\)相對大小無所謂,將它們的\(dfn\)值域共同擴充套件,還要乘上一個組合數。

狀態轉移方程:

\[dp_{i,j}=\sum_{k=1}^i\sum_{o=j}^mdp_{k-1,o+1}dp_{i-k,j}\binom{i-1}{k-1} \]

不過這樣時間複雜度是\(\mathrm\!\left(n^4\right)\)的(視\(n,m\)同階),然鵝很好優化,移一下\(\sum\)得到

\[dp_{i,j}=\sum_{k=1}^idp_{i-k,j}\binom{i-1}{k-1}\sum_{o=j}^mdp_{k-1,o+1} \]

則實時維護\(\forall i\in[0,n],dp_i\)的字首和,後面那個\(\sum\)呼叫一下字首和複雜度就少一個\(n\)了。

程式碼:

#include<bits/stdc++.h>
using namespace std;
const int N=300,M=300;
int n,m,mod;
int comb[N+1][N+1];
int dp[N+1][M+2];
int Sum[N+1][M+2];
int main(){
	cin>>n>>m>>mod;
	comb[0][0]=1;
	for(int i=1;i<=n;i++)for(int j=0;j<=i;j++)comb[i][j]=((j?comb[i-1][j-1]:0)+(j<i?comb[i-1][j]:0))%mod;//預處理組合數 
	for(int i=1;i<=m+1;i++)Sum[0][i]=(Sum[0][i-1]+(dp[0][i]=1))%mod;//邊界 
	for(int i=1;i<=n;i++){//轉移 
		for(int j=1;j<=m;j++){
			for(int k=1;k<=i;k++)(dp[i][j]+=1ll*dp[i-k][j]*comb[i-1][k-1]%mod*(Sum[k-1][m+1]-Sum[k-1][j])%mod)%=mod;//轉移方程 
			Sum[i][j]=(Sum[i][j-1]+dp[i][j])%mod;//維護字首和 
//			printf("dp[%d][%d]=%d\n",i,j,dp[i][j]);
		}
		Sum[i][m+1]=Sum[i][m];//維護字首和 
	}
	cout<<(dp[n][1]+mod)%mod;//目標 
	return 0;
}