关于DP优化

DP优化考查点通常是状态设计优化,以及通过一些数据结构或者其他方式来维护候选决策集合。
本文主要涉及状压DP,单调队列优化DP,线段树优化DP。(视情况而定,作者目前就掌握这些)

(斜率优化以及四边形不等式优化以后有时间再写,真的好想写图论和数论,虽然比较菜)

状态压缩DP的特点是将状态压缩为一个整数进行处理,比方说对于一个决策,我需要n个状态的值来辅助,那么这时候我们就可以定义DP方程的某一维为一个整数,同时将该整数作为一个n位的非10进位制数,具体几进位制依据原来状态的值域来定。我们通过枚举该数来遍历所有的状态,可以想到,阶段这一因素类似被省略,然而这并无大碍,因为我们在这样遍历状态时,状态的扩展一定是具有无后效性的,所以不必担心这一点,反正本来枚举阶段就是为了状态的扩展。

OK,我们既已了解了状压DP的原理便可以考虑从题目入手深入挖掘其中的技巧

CF580D

Kefa and Dishes

题意:kefa进入了一家餐厅,这家餐厅中有n个菜(0<n<=18),kefa对第i个菜的满意度为ai(0<=ai<=109),并且对于这n个菜有k个规则,如果kefa在吃完第xi个菜之后吃了第yi个菜(保证xi、yi不相等),那么会额外获得ci(0<=ci<=109)的满意度。kefa要吃m道任意的菜(0<m<=n),但是他希望自己吃菜的顺序得到的满意度最大,请你帮帮kefa吧!

该题仔细分析的话会发现,若要进行状态转移,我们需要知道上一盘菜吃的什么,这一盘菜吃的什么,以及吃的菜的个数,还有其他菜吃的情况,其实我们真正难搞的是最后一个状态,虽然n只有18,但是给数组额外开18维,未免有点。但是考虑到对于每一道菜我们只需要知道其吃没吃就行,即其状态只有0和1两种,因此我们考虑使用一个18位二进制数来表示其他菜吃的情况,简单quickly pow一下就知道这个整数的范围不会太大,dp数组开的够。
DP方程按题意模拟就行,时间复杂度过关O(2(n)*n2)代码如下

#include<bits/stdc++.h>
using namespace std;
#define int long long
int const maxn  = 20;
int dp[1<<maxn][maxn],a[maxn],p[1<<maxn],w[maxn][maxn];
signed main(){
	ios::sync_with_stdio(false);
	int n,m,k;
	cin >> n >> m >> k;
	for(int i = 0;i < n;i ++){
		cin >> a[i];
		dp[1<<i][i] = a[i];
	}
	for(int i = 1;i <= k;i ++){
		int x,y,c;
		cin >> x >> y >> c;
		w[x-1][y-1] = max(w[x-1][y-1],c);
	
	}
	for(int i = 0;i < (1<<n);i ++){
		for(int j = 0;j < n;j++){
			if(i&(1<<j)){
				p[i]++;
			}
		}
	}
	for(int i = 0;i < (1<<n);i ++){
		for(int j = 0;j < n ;j ++){
			if(i&(1<<j)){
				for(int k = 0 ;k < n ;k++){
					if(k==j)continue;
					if(i&(1<<k))continue;
					dp[i|(1<<k)][k] = max(dp[i|(1<<k)][k],dp[i][j] + w[j][k] + a[k]);
				}
			}
			
		}
	}
	int ans  = 0;
	for(int i = 0;i < (1<<n);i ++){
		if(p[i]!=m)continue;
		for(int j = 0;j < n;j ++){
			ans = max(ans,dp[i][j]);
		}
	}
	cout << ans;
	return 0;
}

CF11D

A Simple Task

题意:给出一个有向图,请求该图里的环的个数
1<=n <=19
m>=0

确定环是否存在的做法有很多种,例如拓扑排序和tarjan缩点,但求环的数量的方法目前作者掌握的只有一个dfs。
但考虑dfs的时候本质也是依靠目前经过点的参数来进行计算,与dp时所需的状态本质上没啥区别。
但如果真要写这个函数,那所需传递的参数个数也就太多了。看到1<=n<=19的范围差不多就该意识到一点,先考虑每个点的状态也无非是经过和没经过,那就直接状压呗,考虑第一维是用二进制数枚举的点所经过的情况,第二维枚举的是起点,第三维枚举的是终点,考虑出现环的情况即为出现一条返祖边,又考虑到我们第一维所进行的遍历可以取其最低位数为起点,这样也不用考虑出现"1,2,3,4"与”4,1,2,3“这种环重复的情况,最后再将答案减去m排除二元环,因为是无向图所以除以2,得出最终结果

#include<bits/stdc++.h>
using namespace std;
int const maxn = 20;
#define int long long
int dp[maxn][1<<maxn];
int ans;
vector<int> G[maxn];

int lowbit(int x){
	return x&(-x);
}

signed main(){
	int n,m;
	cin >> n >> m ;
	for(int i = 1;i <= m;i ++){
		int u,v;
		cin >> u >>  v ;
		u --,v--;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	for(int i = 0;i < n;i ++){
		dp[i][1<<i] = 1;
	}
	for(int i = 0;i < (1<<n);i ++){
	    for(int j = 0;j < n;j ++){
	    	if(!dp[j][i])continue;
	    	
	    	for(auto k : G[j]){
	    		if(lowbit(i) > (1<<k))continue;
	    		if(i&(1<<k)){
	    			if(lowbit(i) == (1<<k)){
	    				ans += dp[j][i];
	    			}
	    		}else {
	    			dp[k][i|(1<<k)] += dp[j][i];
	    		}
	    	}
	    }
	}
	cout << (ans - m )/2;
	return 0;
	cout << ans;
}

考虑这样一个问题

dp[i] = min(dp[j] + val(j) + val(i)) (a+b * i<=j<=c+d * i)

我们可以发现枚举i的时候一定可以得到val(i),所以val(i)可以提出来

dp[i] = min(dp[j]+val(j)) + val(i) (a+b * i<=j<=c+d * i)

同时我们考虑到,随着i的增大,j的左界增大,j的右界减小,即此时的决策集合剔除一些过期决策且添加一些新的决策,而我们所需要的是在这个集合中使(dp[j]+val(j))最小的j,可以想到使用某种数据结构,维护这个最优决策,因为随着阶段i的增大,已经被踢出的决策不会再此加入集合,因此我们可以考虑使用单调队列来维护决策集合,将队头设为最优决策,每次i更新时,先将过期决策踢出,再考虑维护这个单调队列的单调性,最后加入这个决策。这样每次转移在均摊后的时间复杂度为O(1),非常好。
考虑该问题的变形

dp[i] = min(dp[j]) + val(i) (a[i]<=j<= b[i])

该问题与前面那个的不同在于对于不同的i,决策被踢出决策集合后并非不会再次成为最优决策,而这就要求我们需要一个数据结构维护之前所出现过的所有最优决策,考虑到无后效性原则与单点修改加区间求最值这些要求,线段树优化极为合适。对于每个枚举到的i,我们先查询在[a[i],b[i]]之间的最优决策,然后进行状态转移,同时将该决策上传到线段树上。

CF372C

Watching Fireworks is Fun

题意:一个城镇有n个区域,从左到右1编号为n,每个区域之间距离1个单位距离。节日中有m个烟火要放,给定放的地点a[i],时间t[i],如果你当时在区域x,那么你可以获得b[i]-|ai一x|的开心值。你每个单位时间可以移动不超过d个单位距离。你的初始位置是任意的(初始时刻为1),求你通过移动能获取到的最大的开心值。

1 <= n <= 150000
1 <= m <= 300
1<= a[i] <= n
1<= b[i],t[i]<=1e9

该题容易想到一个转移方式为dp[i][j] = max(dp[i][j],dp[i-1][k] + b[i] - |a[i] - j|),dp[i][j]表示到第i场表演时,位置处在j所能得到的最大满意度。可以发现对于第i场的第j个位置,我们在方程里需要通过遍历所获取的信息只有dp[i-1][k],但是考虑到j - d(t[i] - t[i-1]) <= k <= j + d(t[i] - t[i-1]),即过期的决策集合不会再次使用,这时候我们考虑使用单调队列优化时间复杂度到O(nm),维护一个单调队列,队头是使dp[i-1][k]最小的k,然后直接根据方程更新即可。
当然此题看起来也可使用线段树进行优化,但最终的时间复杂度会达到O(nmlogn),过不了该题。
空间开不了n*m,但观察方程可以发现转移所需阶段信息不超过2个,所以直接滚动数组,也可直接省略

#include<bits/stdc++.h>
using namespace std;
#define int long long

int const maxn = 2e6;
int a[maxn],b[maxn],t[maxn];
int dp[2][maxn];
int q[maxn];
signed main(){
	int n,m,d;
	cin >> n >> m >> d;
	for(int i = 1;i <= m;i ++){
		cin >> a[i]>>b[i]>> t[i];
	}
	memset(dp,0x3f,sizeof dp);
	for(int i = 1;i <= n;i ++)dp[0][i] = 0;
	int op = 1;
	for(int i = 1;i <= m;i ++){
		int l = 1,r = 0,k = 1;
		
		for(int j = 1;j <= n;j ++){
			for(;k <= min(1ll*n,j + d*(t[i]-t[i-1]));k ++){
				while(l <= r&&dp[op^1][q[r]]<dp[op^1][k])r--;
				q[++r] = k;
			}
			while(l <= r&&q[l] < max(1ll,j-d*(t[i] - t[i-1]))){
				l++;
			}
			dp[op][j] = dp[op^1][q[l]] + b[i] - abs(a[i] - j);
		
		}
		op^=1;
	}
	int ans = -1e18;
	for(int i = 1;i <= n;i++){
		ans = max(ans,dp[op^1][i]);
	
	}
	cout << ans;
}

CF797F

Mice and Holes

题意:n个老鼠,m个洞,告诉你他们的一维坐标和m个洞的容量限制,问最小总距离。
-1e9 <= x[i],p[j]<=1e9
1<= c[j],n,m<=5e3
该题看到容量限制可考虑背包,但由于每只老鼠的“代价”和本身的横坐标以及洞的横坐标有关,所以考虑将两者都按x升序排序,然后可以得到一个比较显然的转移方程
dp[i][j] = min(dp[i-1][k] + sum[i][k] - sum[i][j - 1]) (j - c[i] <= k <= j)
sum[i][j]表示前j个鼠鼠到第i个洞的距离之和,dp[i][j]表示前j个鼠鼠分配到前i个洞洞的最小距离之和,这样做的时间复杂度是O(nm^2),考虑优化,状态方面已经难以优化,发现当i确定时,j增加,则k的上界与下界都增加,然后就很套路了,方程变为
dp[i][j] = min(dp[i-1][k] + sum[i][k]) - sum[i][j - 1] (j - c[i]<= k<= j)
用一个单调队列进行维护就行了,没啥难度,而且该题可以用线段树优化,虽然没有单调队列代码短而且优就是了。

#include<bits/stdc++.h>
using namespace std;
int const maxn = 5e3+10;
#define int long long
int a[maxn],sum[maxn],dp[2][maxn];
struct node{
	int p,c;
}h[maxn];

int q[maxn];

bool cmp(node a,node b){
	return a.p < b.p;
}

signed main(){
	int n,m;
	cin >> n >> m ; 
	for(int i = 1;i <= n;i ++){
		cin >> a[i];
	}
		int sm = 0;
	sort(a+1,a+n+1);
	for(int j = 1;j <= m;j ++){
		cin >>h[j].p>>h[j].c;
		sm += h[j].c;
	}
	sort(h+1,h+m+1,cmp);
if(sm < n){
	cout << - 1;
	return 0;
}
	int now = 0;
	memset(dp[0],0x3f,sizeof dp[0]);
	dp[0][0] = 0;
	for(int i = 1;i <= m;i ++){
		int l = 1,r = 0;
		now^=1;
	
		sum[0] = 0;
		for(int j = 1;j <= n;j++){
		
			sum[j] = sum[j-1] + abs(a[j] - h[i].p); 
		}
		q[++r] = 0;
		for(int j = 1;j <=n;j ++){
				dp[now][j] = dp[now^1][j];
			while(l <= r && q[l] < j - h[i].c)l++;
			
			while(l <= r && dp[now^1][q[r]] - sum[q[r]] >= dp[now^1][j] - sum[j]){
				r--;
			}
			q[++r] = j;
			dp[now][j] =min(dp[now^1][q[l]] + sum[j] - sum[q[l]],dp[now][j]);
		}
	//		cout << dp[now][q[l]]<<'\n';
	}
	if(dp[now][n]==4557430888798830399)cout << -1;
	else cout << dp[now][n];
	return 0;
} 

updating...

posted @ 2023-08-12 15:38  瑞恩尼lower  阅读(80)  评论(1)    收藏  举报