預設型DP

Q昀發表於2024-07-20

DP搬運工1

Description

給你 n,k,求有多少個 1 到 n 的排列,滿足相鄰兩個數的 max 的和不超過 k。

Input Format

一行兩個整數 n,k。

Output Format

一行一個整數 ansans 表示答案 \(\text{mod 998244353}\)

Sample

樣例輸入1
4 10
樣例輸出1
16
樣例輸入2
10 66
樣例輸出2
1983744

Hint

有 50 個 測試點,第 i 個測試點為 n=i , \(k\leq n^2\)

題解

對於此題我們很難設計狀態,其中最難是得保證每個數只選了一次,因此我們不能列舉每一位該填什麼,而是這個數填哪個位置
由於本題性質,可以從小到大列舉方便算其貢獻;

然而依舊不是那麼好做,需要設計一個恰當的狀態。其實預設型DP還有另外的一個名字連續段DP,我們可以設 $$f_{i,j,k}表示列舉到第 i 個數,所填位置中間有 j 個斷點時總貢獻為 k 的個數$$
其中 j 這一維略顯突兀,用圖形表示填法大致如下:

\[O()OOO()OO()O \]

連續的O表示連續填的數,()表示中間有一個斷點,這個時候的j就表示有幾個(),不考慮兩端
我們並不會考慮()中間有多少個空位,只需知道有這麼個空位就行了這就足夠給出轉移方程了:

分類討論:

  • 一.放兩邊擴充
    1. 緊貼兩邊新增貢獻為 i ,有兩邊所以計數乘 2 :
      \(f[i][j][k+i]+=f[i-1][j][k]*2\)
    2. 隔開填沒有新貢獻,但是會增加一個斷斷點,有兩邊所以計數乘 2 :
      \(f[i][j+1][k]+=f[i-1][j][k]*2\)
  • 二.在中間斷點部分進行操作
    1. 緊貼斷點兩側的連續段新增貢獻為 i ,有 j 個斷點 每個斷點兩邊分別計數 所以計數乘 j*2 :
      \(f[i][j][k+i]+=f[i-1][j][k]*j*2\)
    2. 在斷點中間填沒有新增貢獻,但會增加斷點數,有 j 個斷點所以計數乘 j :
      \(f[i][j+1][k]+=f[i-1][j][k]*j\)
    3. 減少一個斷點,即把斷點兩邊的連續段用我們新填的數連起來,此時貢獻為 i*2,斷點減少 1 ,有 j 個斷點所以計數乘 j :
      \(f[i][j-1][k+2*i]+=f[i-1][j][k]*j\)

初始狀態 \(f_{1,0,0}=1\)
答案 \(\displaystyle \sum^{k}_{i=1} {f_{n,0,i}}\)

這個DP設計並沒有考慮中間斷點的距離為多長,可以想做每個斷點的距離實際上有無限長。
操作二.3 是強行把一個斷點兩邊的連續段連在一起,考慮也只是相對位置,並不是有些題解中寫的每次狀態轉移會篩掉一部分不合法狀態的計數,或是在答案中無法體現,它都是合法的。
不要拘束於它只有 n 個可填位置,用相對位置去理解,到最後填完的時候自然就沒有斷點了,也正好填了那麼多位。

code
const int mod=998244353;
ll f[52][52][2610];

inline void m(ll &x,const ll &y)
{
	x=(x+y)%mod;
}

signed main()
{
	int n=read(),k=read();
	
	f[1][0][0]=1;
	for(int i=2;i<=n;++i)
		for(int j=0;j<=n;++j)
			for(int u=0;u<=k;++u)
			{
				//Á½±ß 
				m(f[i][j][u+i],f[i-1][j][u]*2);//½ôÌù 
				m(f[i][j+1][u],f[i-1][j][u]*2);//¿Õ 
				
				//Öмä 
				m(f[i][j][u+i],f[i-1][j][u]*j*2);//Ìù±ß 
				m(f[i][j+1][u],f[i-1][j][u]*j);//Öмä 
				if(j)m(f[i][j-1][u+2*i],f[i-1][j][u]*j);//Á¬½Ó 
			}
	
	ll ans=0;	
	for(int i=0;i<=k;++i)
		m(ans,f[n][0][i]);
		
	__print(ans);
	
	return 0;
}

相關文章