DP 習題(一)

xingyu_xuan發表於2024-06-10

樸素 DP

[ABC301F] Anti-DDoS

題意

link

定義形如 DDoS 的序列為類 DDoS 序列,其中 DD 表示兩個相同的任意大寫字母,o 表示任意小寫字母,S 表示任意大寫字母。

給定一個由大小寫字母和 ? 組成的序列 \(S\),問有多少種將 ? 替換為大小寫字母的方案可以使 \(S\) 不含有任何一個類 DDoS 子序列,答案對 \(998244353\) 取模。

\(4 \le \left|S\right| \le 3 \times 10^5\)

解法

這是上一道例題的變式。

這一道題因為是對不含類 DDoS 子序列的方案計數,所以為了方便,我們設 \(f_{i,j}\) 是前 \(i\) 位中沒有類 DDoS 子序列中的前 \(j+2\) 位的方案數。顯然答案就是 \(f_{n,2}\)

首先我們考慮如何計算 \(f_{i,0}\),即使前 \(i\) 位中不含兩個相同大寫字母的方案數。考慮假設前 \(i\) 位有 \(m\)?,有 \(k\) 種大寫字母。注意到如果這 \(k\) 種大寫字母的總個數不為 \(k\),那麼此時方案數一定為 \(0\)。否則我們可以在 \(m\)? 選取 \(0\sim k\) 個選擇大寫字母,其餘選擇小寫字母,這樣我們可以列出式子:

\[f_{i,0}=\sum_{i=0}^{\min(m,k)}\binom m i \operatorname A_{k}^i\times 26^{m-i} \]

然後考慮如何計算 \(f_{i,1}\)\(f_{i,2}\)。對於不存在 DDo 的方案數,我們發現如果一個位置是大寫字母,那麼這裡我們就只需要保證之前不存在 DDo 就行了;而如果一個位置是小寫字母,我們這裡則要保證之前不存在 DD;如果是 ? 的話,等於說這裡任意小寫或大寫字母都可以填,於是有轉移式:

\[f_{i,1}=\left\{\begin{matrix} f_{i-1,0}&(s_{i}\in [\text{a}, \text{z}])\\ f_{i-1,1}&(s_i\in[\text{A},\text{Z}])\\ 26f_{i-1,0}+26f_{i-1,1}&\text{otherwise} \end{matrix}\right. \]

\(f_{i,2}\) 的轉移類似。

這樣我們就在 \(O(n|\Sigma|)\) 的時間複雜度內解決了此題。

程式碼
#include<bits/stdc++.h>
using namespace std;
string s;
#define int long long
int f[300005][5];
int frac[300005],ifrac[300005],_26[300005];
const int mod=998244353;
int ksm(int a,int b){
	if(!b)return 1;
	return (b&1?a:1)*ksm(a*a%mod,b/2)%mod;
}
int vis[27],lftc=26;
int A(int a,int b){
	if(a<b)return 0;
	return frac[a]*ifrac[a-b]%mod;
}
int C(int a,int b){
	if(a<b)return 0;
	return frac[a]*ifrac[a-b]%mod*ifrac[b]%mod;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>s;
	int siz=s.size();
	frac[0]=_26[0]=1;
	int lim=max(26ll,siz);
	for(int i=1;i<=lim;i++)frac[i]=frac[i-1]*i%mod,_26[i]=_26[i-1]*26%mod;
	ifrac[lim]=ksm(frac[lim],mod-2);
	for(int i=lim-1;i>=0;i--)ifrac[i]=ifrac[i+1]*(i+1)%mod;
	int cntq=0;
	f[0][0]=f[0][1]=f[0][2]=1;
	for(int i=1;i<=siz;i++){//DD
		if(s[i-1]=='?')cntq++;
		else if(s[i-1]>='A'&&s[i-1]<='Z'){
			if(vis[s[i-1]-'A'+1])break;
			else vis[s[i-1]-'A'+1]=1,lftc--;
		}
		int &p=f[i][0];
		for(int j=min(lftc,cntq);j>=0;j--){
			p=(p+C(cntq,j)*A(lftc,j)%mod*_26[cntq-j]%mod)%mod;
		// cout<<j<<" "<<cntq<<" "<<lftc<<" "<<C(cntq,j)<<" "<<A(lftc,j)<<" "<<frac[26]<<" "<<p<<"\n";
		}
	}
	// cout<<"\n";
	for(int i=1;i<=siz;i++){//DDo
		if(s[i-1]=='?')f[i][1]=(26ll*f[i-1][0]%mod+26ll*f[i-1][1]%mod)%mod;
		else if(s[i-1]>='a'&&s[i-1]<='z')f[i][1]=f[i-1][0];
		else f[i][1]=f[i-1][1];
	}
	for(int i=1;i<=siz;i++){
		if(s[i-1]=='?')f[i][2]=(26ll*f[i-1][1]%mod+26ll*f[i-1][2]%mod)%mod;
		else if(s[i-1]>='a'&&s[i-1]<='z')f[i][2]=f[i-1][2];
		else f[i][2]=f[i-1][1];
	}
	cout<<f[siz][2];
	return 0;
}

P2224 [HNOI2001] 產品加工

題意

link

某加工廠有 A、B 兩臺機器,來加工的產品可以由其中任何一臺機器完成,或者兩臺機器共同完成。由於受到機器效能和產品特性的限制,不同的機器加工同一產品所需的時間會不同,若同時由兩臺機器共同進行加工,所完成任務又會不同。

某一天,加工廠接到 \(n\) 個產品加工的任務,每個任務的工作量不盡一樣。

你的任務就是:已知每個任務在 A 機器上加工所需的時間 \(t_1\),B 機器上加工所需的時間 \(t_2\) 及由兩臺機器共同加工所需的時間 \(t_3\),請你合理安排任務的排程順序,使完成所有 \(n\) 個任務的總時間最少。

\(1\leq n\leq 6\times 10^3\)\(0\leq t_1,t_2,t_3\leq 5\)

解法

和上一道題有一些相似之處。

注意到題面沒有說非要按順序完成這些任務,直接按順序加入元素顯然可能會導致等待,這樣不同順序會有不同的 DP 結果,所以我們需要一種不導致等待或者可以按某個順序 DP 的 DP 方式。

考慮假設這個時候我們已經透過 DP 算出了一個前 \(i\) 個元素的排程順序。

這個時候我們對 A 或 B 設定一個單獨的任務,並不會產生額外的工作時間變化。

但是我們對 A,B 設定一個一起做的任務,我們發現此時 B 就需要被迫等待 A 做完剩下的才能和 A 一起做。這個時候我們把這個任務插到開頭,發現就沒有這個等待的時間了,這時因為沒有多餘時間,所以一定最優。

因為最優情況下一定沒有等待時間,所以原問題就變成了有一些任務,選一些給 A 做,選一些給 B 做,再選一些讓它們一起做,求兩個機器運作的時間的最大值的最小值,這樣每個元素都是獨立的,加入時不受前面或後面元素的影響,這樣就能 DP 了。

所以我們設 \(f_{i,j}\) 為前 \(i\) 個元素中,A 機器執行了 \(j\) 時間,B 機器執行的最小時間。轉移是簡單的,就討論這個任務是由 A 做還是由 B 做還是一起做。有:

\[f_{i,j}\gets f_{i-1,j-t_1}\\ f_{i,j}\gets f_{i-1,j}+t_2\\ f_{i,j}\gets f_{i-1,j-t_3}+t_3 \]

這樣我們能在 \(O(nV)\) 時間複雜度內解決這個問題,其中 \(V=5n\)

程式碼
#include<bits/stdc++.h>
using namespace std;
int n;
const int M=3e4;
int dp[2][M+5];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	int now=1,ed=0,t1,t2,t3;
	memset(dp[0],0x3f,sizeof dp[0]);
	dp[0][0]=0;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>t1>>t2>>t3;
		memset(dp[now],0x3f,sizeof dp[now]);
		for(int j=0;j<=M;j++){
			if(t1&&j>=t1)dp[now][j]=min(dp[now][j],dp[ed][j-t1]);
			if(t2)dp[now][j]=min(dp[now][j],dp[ed][j]+t2);
			if(t3&&j>=t3)dp[now][j]=min(dp[now][j],dp[ed][j-t3]+t3);
		}
		swap(now,ed);
	}
	int ans=1e9;
	for(int i=0;i<=M;i++)ans=min(ans,max(i,dp[ed][i]));
	cout<<ans;
	return 0;
}

區間 DP

P2470 [SCOI2007] 壓縮

link

題意

給一個由小寫字母組成的字串,我們可以用一種簡單的方法來壓縮其中的重複資訊。壓縮後的字串除了小寫字母外還可以(但不必)包含大寫字母R與M,其中M標記重複串的開始,R重複從上一個M(如果當前位置左邊沒有M,則從串的開始算起)開始的解壓結果(稱為緩衝串)。

bcdcdcdcd 可以壓縮為 bMcdRR,下面是解壓縮的過程:

已經解壓的部分 解壓結果 緩衝串
b b b
bM b .
bMc bc c
bMcd bcd cd
bMcdR bcdcd cdcd
bMcdRR bcdcdcdcd cdcdcdcd

\(n\leq 50\)

解法

和上道題一樣的壓縮字串類的題。

其實這題可以加一個輸出方案,這樣的話這題就是一個作者認為非常好的例題。

因為這題要處理 M,所以我們可以設 \(f_{i,j}\) 為區間 \([i,j]\) 可以用 R 字元壓縮的最短長度。

這裡就有兩個轉移,第一個是 \(f_{i,j}\gets f_{i,i+\frac{j-i+1}{2}-1}+1\),壓縮一半。

第二個是合併兩個區間,我們有 \(f_{i,j}\gets f_{i,k}+(j-k+1)\),因為第二個區間不能有 R

然後我們考慮把所有區間的 \(f\) 用另外一個 DP 合併起來,設 \(g_{i}\) 為前 \(i\) 個元素的壓縮後最短長度,顯然有 \(g_{i}\gets g_j+f_{j+1,i}+1\)\(1\) 是給前面加的 M 加的。

這兩個方程式都很像能 DP 最佳化的樣子,如果胡出來這個最佳化的可以私信作者。

UPD:作者胡了一個 \(O(n^2)\) 的掃描一遍 + 單調棧的做法,但是這題 \(n\leq 50\) 隨便過。

最後答案就是 \(g_{1,n}-1\)

程式碼
#include<bits/stdc++.h>
using namespace std;
char s[55],*ss=s+1;
int dp[55][55];
int f[55];
#define ull unsigned long long
ull hsh[55];
const ull base=179;
ull _b[55];
ull gethsh(int l,int r){
	return hsh[r]-hsh[l-1]*_b[r-l+1];
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>ss;
	int n=strlen(ss);
	_b[0]=1;
	for(int i=1;i<=n;i++)hsh[i]=hsh[i-1]*base+s[i]-'a'+1,_b[i]=_b[i-1]*base;
	for(int i=1;i<=n;i++)f[i]=1e9;
	memset(dp,0x3f,sizeof dp);
	for(int i=1;i<=n;i++)dp[i][i]=1;
	for(int i=2;i<=n;i++){
		for(int j=1;j<=n-i+1;j++){
			for(int k=j;k<j+i-1;k++){
				dp[j][j+i-1]=min(dp[j][j+i-1],dp[j][k]+(j+i-1)-k);
			}
			if(i%2==0){
				if(gethsh(j,j+i/2-1)==gethsh(j+i/2,j+i-1))dp[j][j+i-1]=min(dp[j][j+i-1],dp[j][j+i/2-1]+1);//...R
			}
		}
	}
	// cerr<<dp[1][4]<<" "<<gethsh(1,2)<<" "<<gethsh(3,4)<<"\n";
	for(int i=1;i<=n;i++){
		for(int j=0;j<i;j++){
			f[i]=min(f[i],f[j]+dp[j+1][i]+1);
		}
	}
	cout<<f[n]-1;
	return 0;
}

P3592 [POI2015] MYJ

link

題意

\(n\) 家洗車店從左往右排成一排,每家店都有一個正整數價格 \(p_i\)。有 \(m\) 個人要來消費,第 \(i\) 個人會駛過第 \(a_i\) 個開始一直到第 \(b_i\) 個洗車店,且會選擇這些店中最便宜的一個進行一次消費。但是如果這個最便宜的價格大於 \(c_i\),那麼這個人就不洗車了。請給每家店指定一個價格,使得所有人花的錢的總和最大。

\(n\leq 50,m\leq 4000\)

解法

和上一道題基本一樣,而且要輸出方案,所以是選做。

離散化 \(c_i\),設 \(f_{l,r,p}\) 為區間 \([l,r]\) 的定價都不小於 \(p\) 的時候,對於行駛區間完全包含於 \([l,r]\) 的人的花費最大值。

考慮隨意指定這個區間的一個位置,欽定其定價為 \(p\),因為其餘定價都不小於 \(p\),所以行駛跨過該位置的人如果要花費就可以在這裡花費。所以我們有:

\[f_{l,r,p}=\max_{pos\in[l,r]}(p\times cnt_{pos,p}+f_{l,pos-1,p}+f_{pos+1,r,p}) \]

其中 \(cnt_{pos,p}\) 表示行駛區間在 \([l,r]\) 內且跨越 \(pos\) 位置又能接受 \(p\) 價格的人的數量。

這個方程式顯然不完整,因為它只包含了這個區間有定價為 \(p\) 的點的情況。如果整個區間都是 \(>p\) 的定價,那麼我們完全可以從 \(f_{l,r,p+1}\) 轉移過來。這樣的轉移就完整了。

最後一個問題就是如何計算 \(cnt_{pos,p}\)。我們在列舉 \(l,r,pos\) 的時候我們可以列舉每一個人計算,不難發現每個人都是接受一個價格字首的,所以我們可以差分。這樣我們可以在 \(O(m)\) 解決這個問題。

輸出方案的話同樣記錄 \(f_{l,r,p}\) 是從哪個位置還是從 \(f_{l,r,p+1}\) 轉移過來,最後 DFS 一遍即可。

程式碼
#include<bits/stdc++.h>
using namespace std;
int n,m;
int cval[4005],ccnt;
int l[4005],r[4005],c[4005];
int f[55][55][4005],ans[55],o[55][55][4005];
int tmp[4005];
const int SIG=1e8;
void getans(int l,int r,int val){
	if(l>r)return;
	if(o[l][r][val]==0){
		for(int i=l;i<=r;i++)ans[i]=cval[val];
		return;
	}
	else if(o[l][r][val]==SIG)getans(l,r,val+1);
	else{
		ans[o[l][r][val]]=cval[val];
		getans(l,o[l][r][val]-1,val);
		getans(o[l][r][val]+1,r,val);
	}
	return;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=m;i++)cin>>l[i]>>r[i]>>c[i],cval[++ccnt]=c[i];
	sort(cval+1,cval+ccnt+1);
	ccnt=unique(cval+1,cval+ccnt+1)-cval-1;
	for(int i=1;i<=m;i++){
		c[i]=lower_bound(cval+1,cval+ccnt+1,c[i])-cval;
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n-i+1;j++){
			for(int k=j;k<=j+i-1;k++){
				memset(tmp,0,sizeof tmp);
				for(int p=1;p<=m;p++)if(l[p]>=j&&l[p]<=k&&r[p]>=k&&r[p]<=j+i-1)tmp[c[p]]++;
				for(int p=ccnt;p>=1;p--)tmp[p]+=tmp[p+1];
				for(int p=1;p<=ccnt;p++){
					if(f[j][j+i-1][p]<tmp[p]*cval[p]+f[j][k-1][p]+f[k+1][j+i-1][p]){
						f[j][j+i-1][p]=tmp[p]*cval[p]+f[j][k-1][p]+f[k+1][j+i-1][p];
						o[j][j+i-1][p]=k;
					}
				}
			}
			for(int p=ccnt;p>=1;p--){
				if(f[j][j+i-1][p]<f[j][j+i-1][p+1]){
					f[j][j+i-1][p]=f[j][j+i-1][p+1];
					o[j][j+i-1][p]=SIG;
				}
			}
		}
	}
	cout<<f[1][n][1]<<"\n";
	getans(1,n,1);
	for(int i=1;i<=n;i++)cout<<ans[i]<<" ";
	return 0;
}

揹包 DP

P1941 [NOIP2014 提高組] 飛揚的小鳥

link

題意

Flappy Bird 是一款風靡一時的休閒手機遊戲。玩家需要不斷控制點選手機螢幕的頻率來調節小鳥的飛行高度,讓小鳥順利透過畫面右方的管道縫隙。如果小鳥一不小心撞到了水管或者掉在地上的話,便宣告失敗。

為了簡化問題,我們對遊戲規則進行了簡化和改編:

遊戲介面是一個長為 \(n\),高為 \(m\) 的二維平面,其中有 \(k\) 個管道(忽略管道的寬度)。

小鳥始終在遊戲介面內移動。小鳥從遊戲介面最左邊任意整數高度位置出發,到達遊戲介面最右邊時,遊戲完成。

小鳥每個單位時間沿橫座標方向右移的距離為 \(1\),豎直移動的距離由玩家控制。如果點選螢幕,小鳥就會上升一定高度 \(x\),每個單位時間可以點選多次,效果疊加;如果不點選螢幕,小鳥就會下降一定高度 \(y\)。小鳥位於橫座標方向不同位置時,上升的高度 \(x\) 和下降的高度 \(y\) 可能互不相同。

小鳥高度等於 \(0\) 或者小鳥碰到管道時,遊戲失敗。小鳥高度為 \(m\) 時,無法再上升。

現在,請你判斷是否可以完成遊戲。如果可以,輸出最少點選螢幕數;否則,輸出小鳥最多可以透過多少個管道縫隙。

\(n\leq 10000,m\leq 1000\)

解法

直接從左到右掃一遍,如果遇到管道那就設定強制不可達。

特判下降和最高點的轉移,中間的轉移列舉同餘系然後掃一遍中間的數就行了。揹包的轉移是簡單的。

不是依賴性揹包。

程式碼
#include<bits/stdc++.h>
using namespace std;
int dp[2][1005];
int n,m,k;
int upo[10005],downo[10005];
int pos[10005],L[10005],R[10005];
int id[10005];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m>>k;
	for(int i=1;i<=n;i++)cin>>upo[i]>>downo[i];
	for(int i=1;i<=k;i++)cin>>pos[i]>>L[i]>>R[i],id[pos[i]]=i;
	int now=1,ed=0;
	dp[ed][0]=1e9;
	int cnt=0;
	for(int i=1;i<=n;i++){
		memset(dp[now],0x3f,sizeof dp[now]);
		for(int j=1;j<m;j++)dp[now][m]=min(dp[now][m],dp[ed][j]+(m-j+(upo[i]-1))/upo[i]);
		dp[now][m]=min(dp[now][m],dp[ed][m]+1);
		for(int j=m-downo[i];j>=1;j--)dp[now][j]=min(dp[now][j],dp[ed][j+downo[i]]);
		for(int j=1;j<=upo[i];j++){
			int tmp=1e9;
			for(int k=j;k<m;k+=upo[i]){
				dp[now][k]=min(dp[now][k],tmp+1);
				tmp=min(tmp+1,dp[ed][k]);
			}
		}
		if(id[i]){
			for(int j=1;j<=L[id[i]];j++)dp[now][j]=1e9;
			for(int j=R[id[i]];j<=m;j++)dp[now][j]=1e9;
			cnt++;
		}
		bool flag=0;
		for(int j=1;j<=m;j++)if(dp[now][j]<1e8)flag=1;
		if(!flag)return cout<<0<<"\n"<<cnt-1,0;
		swap(now,ed);
	}
	int ans=1e9;
	for(int i=1;i<=m;i++){
		ans=min(dp[ed][i],ans);
	}
	cout<<1<<"\n"<<ans;
	return 0;
}

相關文章