預設型 DP

HANGRY_Sol&Cekas發表於2024-08-24

預設型 DP

《美好的一天》--青春學概論
한 잔 술에 취해 잠긴 목엔
沉醉於一杯酒
갈라지는 목소린 다시
帶著沙啞的嗓音
두 잔 자기 전엔 기분 좋음
入睡前飲下第二杯讓心情愉悅
알 수 없는 세상에 빠져
陷入不可預知的世界
세 잔 또 네 잔 술에 빠진
又沉醉於第三杯第四杯
세상과 취해가는 사람들
這個世界還有酒醉的人們
다시 또다시 기분 좋음
再一次變得心情愉悅
다 노래를 부른다
大家放聲高歌
라라랄라랄라라
啦~
라라랄라랄라라
啦~
라라랄라랄라라
啦~
목소리는 다시
再次高歌
라라랄라랄라라
啦~
라라랄라랄라라
啦~
라라랄라랄라라
啦~
솔직히 우리는
說實話
친구 그 이상도
我們的關係
이하도 아닌데
似友情卻非友情
가끔 네가
偶爾會覺得
너무 예뻐 보여
你很美麗
이게 말이 돼
這像話嗎
특히 오늘 같은
特別是在今天
날이면 더더욱
這樣的日子
지금 네가 쳐다보고
你現在正在
있는 손거울
看的鏡子
그게 내 얼굴이었으면 하는
如果是我的臉就好了
말도 안 되는 생각
有著這樣不像話的想法
널 자기라고 부를 날이 올까
會有一天我能叫你親愛的嗎
언젠가 미안 취한 것 같아 나
會是什麼時候呢 對不起我好像喝醉了
그래도 좋은 날이니까
即使這樣也是好日子
뭐 이 정도는 괜찮잖아
應該沒有關係吧
한 잔 술이 들어가니까
飲下一杯酒
두 잔 새벽 세 시잖아
第二杯已經到了凌晨三點
괜찮아 첫차 뜰 때까지
沒關係 直到清晨來臨
같이 있어 줄게
我會一直
네 곁에
在你左右
하루 밤 새워 마신 술이
熬夜飲下的酒
이틀 휘청거리게 해도
即使會讓第二天眩暈
괜찮아 기대 내 어깨에
沒關係 靠在我的肩膀上
난 그림자처럼
我會像影子般
말없이 항상 네 옆에
默默無語 一直陪伴你身邊
세 잔 또 네 잔 술에 빠진
又沉醉於第三杯第四杯
세상과 취해가는 사람들
這個世界還有酒醉的人
다시 또다시 기분 좋음
再一次變得心情愉悅
다 노래를 부른다
大家放聲高歌
라라랄라랄라라
啦~
라라랄라랄라라
啦~
라라랄라랄라라
啦~
목소리는 다시
再次高歌
라라랄라랄라라
啦~
라라랄라랄라라
啦~
라라랄라랄라라
啦~
오늘의 주인공은 너
你是今天的主角
생일 축하해
生日快樂
동시에 코 앞까지 온
也祝賀你
입대를 축하해
近在眼前的入伍日子
놀리는 건 아냐
不是開玩笑
나도 곧 갈텐데 뭐
我也馬上就要去了
신경 쓰지 말고 임마
不要想太多 小子
빈 잔이나 채워
將空杯斟滿酒
넌 항상 되고 싶어 했잖아
你不是一直想要成為
진짜 사나이
真正的男人嗎
이젠 그 시간이 온 거지
現在這一天來了
나의 둘도 없는 친구 놈인
我獨一無二的朋友
너에게도 말이야
你也是一樣
오늘은 우리들끼리
今天我們要
끝까지 가는 날이야
一直喝到最後
한 잔 술이 들어가니까
飲下一杯酒
두 잔 새벽 세 시잖아
第二杯已經到了凌晨三點
우리는 아직 젊고
我們正年輕
이 새벽은 길어
這個深夜太長
하나 걱정이 있다면
僅有的擔心是
네가 조금 취했다는 정도
你已經微微醉了
하루 밤 새워 마신 술이
熬夜飲下的酒
이틀 휘청거리게 해도
即使會讓第二天眩暈
우리는 아직 젊고
我們正年輕
그건 축복이지
那就是祝福
이 노래는 우리를
這是為我們
위한 곡이지
唱的歌
세상은 취해 돌고
在我的世界沉醉旋轉
내 두 눈은 감기고
閉上我的雙眼
술에 취해 조금 어지럽지만
雖然醉酒讓我有些眩暈
이 기분이 난 좋아
但我喜歡這樣的心情
네가 생각나는 밤
想起你的夜裡
나는 다시 노래를 부른다
我再一次歌唱
라라랄라랄라라
啦~
라라랄라랄라라
啦~
라라랄라랄라라
啦~
목소리는 다시
再次高歌
라라랄라랄라라
啦~
라라랄라랄라라
啦~
라라랄라랄라라
啦~
목소리는 다시 꼬이긴 해도
再次高歌 就算跑調
하나도 안 이상해 괜찮아
沒關係 一點也不奇怪
오늘만큼은
今天這樣的日子
네가 저녁때 뭘 먹었는지
即使讓我看到你晚上吃的什麼
보여줘도 괜찮아
也沒關係
오늘만큼은
今夜
집에 가는 길에
回家的路
자꾸만 춤을 춰도 괜찮아
就算跳舞也沒關係
오늘만큼은
像今天
함께 있으면
在一起的日子
좋은 사람들과의 기분 좋은 날
和美好的人一起度過的讓人心情愉悅的日子
오늘만큼은
今天這樣
한 잔 술에 취해 잠긴 목엔
沉醉於一杯酒
갈라지는 목소린 다시
帶著沙啞的嗓音
두 잔 자기 전엔 기분 좋음
入睡前飲下第二杯讓心情愉悅
다 노래를 부른다
大家放聲高歌

前言

一種非常神奇的 \(DP\) 方式,可以處理排列計數問題。

例題

DP 搬運工1

題意:給你 \(n,K\) ,求有多少個 \(1\)\(n\) 的排列,滿足相鄰兩個數的 \(\max\) 的和不超過 \(K\) 。答案對 \(998244353\) 取模。

\(n \le 50 , K \le n^2\)

\(dp_{i , j , k}\) 表示 前 \(i\) 個數,分成 \(j\) 個連續段,和為 \(k\) 的個數。

我們從大往小里加,保證了每次增加的答案為 \(i\) .

無非分成三種情況:

  1. \(i\) 自成一個連續段 : 現在有 \(j - 1\) 個連續段,新加的有 \(j\) 種可能性,因此:

\[dp_{i , j , k} += dp_{i - 1 , j - 1 , k - i} \times j \]

  1. \(i\) 並在一個連續段裡,即變為一個連續段的兩端。 \(j\) 個連續段,顯然有 \(2j\) 種選法,因此:

\[dp_{i , j , k} += dp_{i - 1 , j , k - i} \times 2j \]

  1. \(i\) 連起來兩個連續段,最後 \(j\) 段,之前就是 \(j + 1\) 段,那麼就是 \(j\) 個空,因此:

\[dp_{i , j , k} += dp_{i - 1 , j + 1 , k - 2i} \times j \]

於是我們就得到了 \(dp_{i , j , k}\) .

TexCode

CODE
#include <bits/stdc++.h>
#ifdef Using_Fread
char buf[1 << 20] , *p1 , *p2 ; 
#define getchar() ((p1 == p2 && (p2 = (p1 = buf) + fread(buf , 1 , 1 << 20 , stdin) , p1 == p2)) ? EOF : *p1 ++)
#endif
#ifdef linux
#define getchar getchar_unlocked
#define putchar putchar_unlocked
#endif
using namespace std ; 
typedef long long ll ; 
const int N = 52 ; 
const int mod = 998244353 ; 
namespace Fast_IO {
	inline ll read() {
		ll x = 0 , f = 1 ; 
		char c = getchar() ; 

		while (c < '0' || c > '9') {
			c = getchar() ; 
		}

		while (c >= '0' && c <= '9') {
			x = x * 10 + c - '0' ; 
			c = getchar() ; 
		}

		return x * f ; 
	}
	void write(ll x) {
		if (x / 10) write(x / 10) ; 
		putchar(x % 10 + '0') ; 
	}
} using namespace Fast_IO ; 

int n , K ; 
int dp[N][N][N * N] ; 

signed main() {
	#ifdef LOCAL
	freopen("1.in" , "r" , stdin) ; 
	freopen("1.out" , "w" , stdout) ; 
	#endif
	n = read() , K = read() ; 
	if (n == 1) {
		cout << 1 ; 
		return 0 ; 
	}
	dp[1][1][0] = 1 ; ll val = 1 ; 

	for (int i = 2 ; i <= n ; ++ i) {
		dp[i][i][0] = (val * i) % mod ; val = (val * i) % mod ; 

		for (int j = 1 ; j < i ; ++ j) {
			for (int k = 1 ; k <= i * i ; ++ k) {
				dp[i][j][k] = ((ll)dp[i - 1][j - 1][k] * (ll)j) % mod ; 
				dp[i][j][k] = (dp[i][j][k] + (dp[i - 1][j][k - i] * ((2ll * (ll)j) % mod)) % mod) % mod ; 

				if (k >= 2 * i) dp[i][j][k] = (dp[i][j][k] + (ll)((ll)dp[i - 1][j + 1][k - 2 * i] * (ll)j) % mod) % mod ; 
			}
		}
	}

	ll ans = 0 ; 
	for (int i = 1 ; i <= K ; ++ i) {
		ans = (dp[n][1][i] + ans) % mod ; 
	}

	cout << ans << '\n' ; 
}

[JOI Open 2016] 摩天大樓

譯自 JOI Open 2016 T3 「高層ビル街 / Skyscraper」

題目描述

將互不相同的 \(N\) 個整數 \(A_1, A_2, \cdots, A_N\) 按照一定順序排列。

假設排列為 \(f_1, f_2, \cdots, f_N\),要求:\(| f_1 - f_2| + | f_2 - f_3| + \cdots + | f_{N-1} - f_N| \leq L\)

求滿足題意的排列的方案數對 \(10^9+7\) 取模後的結果。

\(N \le 100 , L \le 1000 , A_i \le 1000 (i \in [1 , n])\)

這是好題

先考慮一下 \(O(n^2\sum\limits a_i)\) 的做法:

我們為了處理掉絕對值號,直接將 \(A_i\) 從大到小排序。

如果我們不進行連續段合併,就是說只考慮往兩端加的情況,顯然是一個鉤子狀的。

圖:

(反正類似吧)

然後我們對於這樣的段來說,其答案顯然為 \(left + right - 2mid\)

( \(left\) 指最左端值, \(right\) 指最右段值, \(mid\) 指中間段最小的那個數 )

然後如果兩個鉤子合併的話,圖:

我們就發現中間那個最高的變成左右兩個的左端或右端。

那總答案 \(\Delta\)\(2new - left_{right} - right_{left}\)

我們發現只要不是最左端或最右端,那麼都會經歷一個這樣的過程,因此在我們 \(DP\) 的時候,只將區間最左和區間最右的特殊考慮。

具體的,設 \(dp_{i , j , k , d}\) 表示前 \(i\) 個, \(j\) 段 , 答案為 \(k\) , 擁有 \(d\) 個端點( \(1\)\(n\) )。

那如果是合併段:

\[dp_{i , j , k , d} = dp_{i - 1 , j + 1 , k - 2a_i , d} \times j \]

至於為什麼從 \(k-2a_i\) 轉移來,就是為了上圖中可能的減法(我們不知道那倆數是啥)準備的,非極端點不加入轉移。

若是新加段且不為端點:

\[dp_{i , j , k , d} = dp_{i - 1 , j - 1 , k + 2a_i , d} \times j \]

\(a_i\) 將是 \(a_i\) 所在鉤子的最小值, (上面不是算出 \(-2mid\) 嗎)。

若是新加段並是端點:

\[dp_{i , j , k , d} = dp_{i - 1 , j - 1 , k + a_i , d - 1} \times(2 - d + 1) \]

端點只有左右兩種情況了吧。為什麼從 \(k + a_i\) 轉移來是因為其與某個 \(left\)\(right\) 重合,需特殊處理。

若是並在某段兩邊,且不為端點:

\[dp_{i , j , k , d} = dp_{i - 1 , j , k , d} \times (2j - d) \]

若是並在某段兩邊,且為端點:

\[dp_{i , j , k , d} = dp_{i - 1 , j , k + a_i , d - 1} \times (2 - d + 1) \]

然後我們發現這個 \(DP\) 第三維是要處理到 \(\sum a_i\) 的,因為既有加又有減。

所以時間複雜度是 \(O(n^2 \sum a_i)\) 的,寄了。

然後考慮一下最佳化哈。有這麼個東西叫提前處理答案。

對於一個差分 \(a_{i + 1} - a_i\) 他的貢獻。

我們發現對於 \(a_i - a_j (i > j)\) 來說,我們知道這個:

\[a_i - a_j = \sum_{k = j + 1}^{i}a_k - a_{k - 1} \]

那麼只有左右 \(i < k \le j\)\(a_k - a_{k - 1}\) 才被計算。

那麼就是說在當 \(a_{i + 1}\) 加入時:

現在共有 \(j\) 個段吧,那麼這 \(j\) 個段裡每個數都是小於 \(a_{i + 1}\) 的。

那麼加入 \(a_{i + 1}\) 以後,很多數並在了 \(j\) 個段的左右兩端。

假設段 \(k\) 在左端未處理 \(a_{i + 1}\) 前數為 \(x\) , 第一個在處理完 \(a_{i + 1}\) 後數為 \(y\)

顯然 \(y > a_{i + 1} > x\)

那麼 \(y - x\) 的值就有 \(a_{i + 1} - a_i\) 的一份貢獻。

再往後,新並在 \(y\) 左的值就不滿足條件了。同理, \(k\) 右端第一個新並上的值一樣,所有端包括 \(a_{i + 1}\) 所處的端也一樣。

那麼我們就發現共有 \(2j\) 個位置是符合條件的,是能被貢獻上的。當然還要考慮左右極端點,設已有個數為 \(d\) , 所以本差分貢獻為 \((2j - d) \times (a_{i + 1} - a_i)\) ( \(j\) 是處理前的段的數量 ) 。

然後就可以得到式子了。 \(DP\) 設計不變,只要將所有的 \(k\) 加減什麼數全部改成 \(k_2 =(2j - d) \times (a_{i + 1} - a_i)\) 就可以了。

如果目前的 \(k_2\) 超過 \(L\) 直接不算棄了就行,時間複雜度 \(O(n^2L)\)

#include <bits/stdc++.h>
using namespace std ; 
typedef long long ll ; 
const int N = 101 ; 
const int M = 1010 ; 
const int mod = 1e9 + 7 ; 

int n , m , a[N] ; 
int dp[N][N][M][3] ; 

signed main() {
	ios :: sync_with_stdio(0) , cin.tie(0) , cout.tie(0) ; 
	cin >> n >> m ; 

	if (n == 1) { // 特判 n = 1
		cout << 1 ; return 0 ; 
	}

	for (int i = 1 ; i <= n ; ++ i) cin >> a[i] ; 

	stable_sort(a + 1 , a + n + 1) ; 
	dp[0][0][0][0] = 1 ; 

	for (int i = 0 ; i < n ; ++ i) {
		for (int j = 0 ; j <= i ; ++ j) {
			for (int k = 0 ; k <= m ; ++ k) {
				for (int d = 0 ; d <= 2 ; ++ d) {
					ll k2 = 1ll * (2 * j - d) * (a[i + 1] - a[i]) + 1ll * k ; 
					if (k2 > m || k2 < 0) continue ; 
					if (!dp[i][j][k][d]) continue ; 

					if (j >= 2) dp[i + 1][j - 1][k2][d] = (1ll * dp[i + 1][j - 1][k2][d] + 1ll * dp[i][j][k][d] * (j - 1)) % mod ; // 合併兩個區間
					if (j >= 1) dp[i + 1][j][k2][d] = (1ll * dp[i + 1][j][k2][d] + 1ll * dp[i][j][k][d] * (2 * j - d)) % mod ; // 並在一個區間左右且不為端點
					dp[i + 1][j + 1][k2][d] = (1ll * dp[i + 1][j + 1][k2][d] + 1ll * dp[i][j][k][d] * (j + 1 - d)) % mod ; // 重開一個且不為端點
					
					if (d < 2 && j >= 1) dp[i + 1][j][k2][d + 1] = (1ll * dp[i + 1][j][k2][d + 1] + 1ll * dp[i][j][k][d] * (2 - d)) % mod ; //  並在一個區間左右且為端點
					if (d < 2) dp[i + 1][j + 1][k2][d + 1] = (1ll * dp[i + 1][j + 1][k2][d + 1] + 1ll * dp[i][j][k][d] * (2 - d)) % mod ; // 重開一個且為端點
				}
			}
		}
	}

	ll ans = 0 ; 

	for (int i = 0 ; i <= m ; ++ i) {
		ans = (ans + 1ll * dp[n][1][i][2]) % mod ; 
	}

	cout << ans ; 
}

相關文章