1. 程式人生 > 實用技巧 >關於DP題的狀態定義轉換和各種優化這檔事

關於DP題的狀態定義轉換和各種優化這檔事

前言

由於\(DarthVictor \ DP\)太菜,最近在刷各種\(DP\)題提升水平,於是見到了各種奇妙的狀態定義轉換和各種奇妙的\(DP\)優化,遂處處志之。可能隨遇到新的\(DP\)題隨時更新。

狀態定義轉化

前言

由於\(DP\)時某一維資料太大,按照正常思路可能會出現陣列開不下的情況,這時候可以試著改變一下狀態定義,以求出路。主要方法有交換兩維,尋找新維等(起名無力……)。

交換兩維

跑揹包時經常用到,可能會揹包容量/物品體積過大而價值較小,這時候可以把體積和價值交換一下,\(f[i]\)定義為達到價值\(i\)至少需要多少體積,最後用揹包容量二分或用其他方法查詢即可即可。

例題:經典版



解說

非常典型的一道例題,物品體積(本題中為要花的錢)和揹包容量(預算)非常之大而價值非常小,符合上面的特點,所以交換正常情況下價值和體積這兩維最後二分即可。

注意! 剛跑出來的\(f[i]\)陣列是不單調的,在進行二分之前要先進行一定的處理:

for(re int k=sum;k;k--) f[k]=min(f[k],f[k+1]);

舉例來說,我們現在買價值為\(k\)的東西需要一百萬,而買價值為\(k+1\)的東西要花一塊錢,那麼顯然\(f[k]=1,000,000\)就莫得用處了,直接抹掉就好。

順帶一提,本題中還涉及了時間這碼事,由於本題中時間是單調的,排個序依次搞即可。

完整程式碼

#include<bits/stdc++.h>
#define re register
using namespace std;
const int maxn=300+3,maxm=1e5+3;
int n,m,ans[maxm],f[maxn*maxn],sum;//f[i]為價值是i時的最小空間
struct mall{
	int c,v,t;
	bool operator < (const mall &A) const{
		return t<A.t;
	}
}a[maxn];
struct buy{
	int t,m,id;
	bool operator < (const buy &A) const{
		return t<A.t;
	}
}b[maxm];
int main(){
	freopen("market.in","r",stdin);
	freopen("market.out","w",stdout);
	scanf("%d%d",&n,&m);
	for(re int i=1;i<=n;i++) scanf("%d%d%d",&a[i].c,&a[i].v,&a[i].t),sum+=a[i].v;
	for(re int i=1;i<=m;i++) scanf("%d%d",&b[i].t,&b[i].m),b[i].id=i;
	sort(a+1,a+1+n);
	sort(b+1,b+1+m);
	memset(f,0x3f,sizeof(f));
	f[0]=0;
	int j=1;
	for(re int i=1;i<=m;i++){
		while(j<=n&&a[j].t<=b[i].t){
			for(re int k=sum;k>=a[j].v;k--) f[k]=min(f[k],f[k-a[j].v]+a[j].c);
			for(re int k=sum;k;k--) f[k]=min(f[k],f[k+1]);//保持單調性
			j++;
		}
		ans[b[i].id]=upper_bound(f+1,f+1+sum,b[i].m)-f-1;
	}
	for(re int i=1;i<=m;i++) printf("%d\n",ans[i]);
	return 0;
}

例題:樹上版



解說

特點還是體積大,只不過這次被扔到樹上了(不會有人看不出來這是個樹上的題吧?不會吧不會吧……)

因為本題問最多可以買幾個商品,因此本題中隱含的價值就是每個商品價值均為\(1\),非常小,符合上面的特點,故定義\(f[i][j][1/0]\)\(i\)的子樹中購買\(j\)件商品用/不用優惠券至少需要多少錢,跑樹型\(DP\)即可。最後由於這個\(f[i]\)陣列是三維的不太好二分就直接全部和總錢數比一下大小。

完整程式碼

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int lzw=5e3+3;
int head[lzw],c[lzw],d[lzw],n,b,tot,f[lzw][lzw][2],size[lzw];
struct edge{
	int to,next;
}e[lzw];
void add(int a,int b){
	e[++tot].to=b;
	e[tot].next=head[a];
	head[a]=tot;
}
void dfs(int u){
	size[u]=1;
	f[u][0][0]=0;
	f[u][1][0]=c[u],f[u][1][1]=c[u]-d[u];
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to;
		dfs(v);
		for(int j=size[u];j>=0;j--){
			for(int k=0;k<=size[v];k++){
				f[u][j+k][0]=min(f[u][j+k][0],f[u][j][0]+f[v][k][0]);
				f[u][j+k][1]=min(f[u][j+k][1],f[u][j][1]+min(f[v][k][0],f[v][k][1]));
			}
		}
		size[u]+=size[v];
	}
}
signed main(){
	scanf("%lld%lld",&n,&b);
	for(int i=1;i<=n;i++){
		scanf("%lld%lld",&c[i],&d[i]);
		if(i>1){
			int tmp;
			scanf("%lld",&tmp);
			add(tmp,i);
		}
	}
	memset(f,0x3f,sizeof(f));
	dfs(1);
	int ans;
	for(int i=n;i>=0;i--){
		if(f[1][i][0]<=b||f[1][i][1]<=b){
			printf("%lld\n",i);
			return 0;
		}
	}
}

尋找新維

自己瞎起的名字……

主要出現在各種題目的加強版裡,一個變數擴大到陣列開不下之後就事實能不能找到一個新的加到狀態定義裡面替換原來的。

(感覺和交換二維差不多啊……)

例題

洛谷P3147 [USACO16OPEN]262144 P

是下面這個的加強版:

洛谷P3146 [USACO16OPEN]248 G

解說

原來的題就是一個簡簡單單的經典區間\(DP\),而資料擴大之後不僅陣列開不下而且也跑不動啊……

於是我們觀察一下這道題裡面有什麼量的資料範圍比較小可以用來利用一下……\(emm\),顯然數字的範圍非常小,我們應該試著用它來定義狀態。

於是就有了這個奇妙的東西:

定義\(f[i][j]\)代表以\(i\)為左端點能組合出數字\(j\)的最小右端點

那麼有

\[f[i][j]=f[i-1][f[i-1][j]] \]

這正常人誰能想到啊[托腮][托腮] 以後還是好好鍛鍊思維能力吧……

順帶一提\(j\)可不止只到\(40\)啊,本題中最大的資料範圍\(262144=2^{18}\),也就是說最大的合併出的資料只會達到\(58\),完全可以接受。

完整程式碼

#include<bits/stdc++.h>
#define re register
using namespace std;
const int lzw=248+3;
int n,f[50+3][lzw],ans;
char buf[1<<20], *p1, *p2;
char gc() { return p1 == p2 ? p2 = buf + fread(p1 = buf, 1, 1<<20, stdin), (p1 == p2) ? EOF : *p1++ : *p1++; }
inline int read(int f = 1, char c = gc(), int x = 0) {
	while(c < '0' || c > '9') f = (c == '-') ? -1 : 1, c = gc();
	while(c >= '0' && c <= '9') x = x * 10 + c - '0', c = gc();
	return f * x;
}
int main(){
	n=read();
	for(re int i=1;i<=n;i++){
		int tmp=read();
		f[tmp][i]=i+1;
	}
	for(re int i=2;i<=50;i++){
		for(re int j=1;j<=n;j++){
			if(f[i][j]||f[i-1][f[i-1][j]]) ans=i;
			if(!f[i][j]) f[i][j]=f[i-1][f[i-1][j]];
		}
	}
	printf("%d\n",ans);
	return 0;
}

各種奇妙優化

堆優化

總結一下特點似乎是決策的時候涉及在兩個量之間進行選擇,於是我們就開兩個堆分別維護兩種變數,並且人為規定一個主變數一個次變數,每次決策時從兩個堆頂選最佳的同時維護堆合法。如果主變數目前不夠優就把它扔進次變數的堆裡。

例題

為了說服珍娜女王停止改造計劃,\(Eddie\)\(Hobo\)踏上了去往王宮的征程。\(Sunshine Empire\)依次排列著\(N+1(N \le 5 \times10^5)\)座城市,\(0\) 號城市是出發地,\(N\) 號城市是王宮。

火車是 \(Sunshine Empire\) 的主要交通工具。\(Eddie\)\(Hobo\) 可以在當前的城市上車,並且在之後的某一座城市下車。從第\(i-1\)座城市乘坐到第\(i\)座城市需要花費\(A_i\)的費用。同時,在第\(i\) 座城市上車需要繳納\(B_i\)的稅款。其中,稅款屬於額外支出,不屬於乘坐這段火車的費用\((A_i,B_i<=10^6)\)

珍娜女王為了促進\(Sunshine Empire\)的繁榮發展,下令:如果連續地乘坐一段火車的費用大於這次上車前所需繳納的稅款,則這次上車前的稅款可以被免除,否則免去乘坐這段火車的費用。

然而,為了保證火車的正常執行,每一次乘坐都不能連續經過超過\(K(K \le 10^5)\)座城市(不包括上車時所在的城市),並且,\(Eddie\)\(Hobo\) 的移動範圍都不能超出\(Sunshine Empire\)\(Eddie\)想知道,到達王宮的最小總花費是多少?

解說

首先\(n^2\)線性\(DP\)非常好想:

\[f[i]=min(f[j]+max(b[j],sum[i]-sum[j])) \]

我們發現轉移的時候無非從\(f[j]+b[j]\)\(f[j]+sum[j]\)這兩種狀態轉移過來,符合上面所說的在兩個量之間選擇進行決策,那麼我們就開兩個大根堆,一個扔\(f[j]+b[j]\),一個扔\(f[j]+sum[j]\)。規定\(f[j]+b[j]\)為主變數。若\(q1.top()>=f[j]-sum[j]+sum[i]\)那就直接用它;若\(q1.top()<f[j]-sum[j]+sum[i]\) 那就把他的被動決策塞入另一個堆中維護,因為不確定以後它的次變數會不會被用到。

同時注意一下\(K\)的問題,這關係到堆的合法性,其實還是很好維護的,每次決策前後都把不合法的——和目前\(i\)的距離大於\(K\)的——清理掉就行了。那麼這樣兩個堆中的狀態都是合法的,然後直接取堆頂元素就是最優的。

隨手引用一段學長的話:

而可能你會想到那第一個堆中的元素\(pop\)掉了,會不會有後效性,其實不會,因為\(sum[i]-sum[j]\)\(sum[j]\)固定而\(sum[i]\)遞增,所以當他從\(q1 \ pop\)出去後,他在\(q2\)中就會一直呆著了。

完整程式碼

#include<bits/stdc++.h>
#define re register
#define ll long long 
using namespace std;
inline int read(){
	int f=1,x=0;char c=getchar();
	while(c<'0'||c>'9') f=(c=='-'?-1:1),c=getchar();
	while(c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
	return f*x;
}
struct node{
	int id;
	ll val;
	node(){}
	node(int x,ll y){id=x,val=y;}
	bool operator < (const node &A)const{
		return val>A.val;
	}
};
priority_queue<node> q1,q2;
const int lzw=5e5+3;
int n,k,b[lzw],tmp;
ll sum[lzw],f[lzw];
int main(){
	freopen("empire.in","r",stdin);
	freopen("empire.out","w",stdout);
	n=read(),k=read();
	for(re int i=1;i<=n;i++) tmp=read(),sum[i]=sum[i-1]+tmp;
	for(re int i=1;i<=n;i++) b[i]=read();
	memset(f,0x3f,sizeof(f));
	f[0]=0;
	for(re int i=1;i<=n;i++){
		q1.push(node(i-1,f[i-1]+b[i]));
		while(q1.size()){
			if(q1.top().id>=i-k) break;
			q1.pop();
		}
		while(q2.size()){
			if(q2.top().id>=i-k) break;
			q2.pop();
		}
		while(q1.size()){
			node now=q1.top();
			if(now.val>f[now.id]+sum[i]-sum[now.id]) break;
			q1.pop();
			q2.push(node(now.id,f[now.id]-sum[now.id]));
		}
		while(q1.size()){
			if(q1.top().id>=i-k) break;
			q1.pop();
		}
		while(q2.size()){
			if(q2.top().id>=i-k) break;
			q2.pop();
		}
		if(!q1.empty()) f[i]=min(f[i],q1.top().val);
		if(!q2.empty()) f[i]=min(f[i],sum[i]+q2.top().val);
	}
	printf("%lld\n",f[n]);
	return 0;
}

幸甚至哉,歌以詠志。