DP总结复习回顾

参考了Peppa_Even_Pig相关文章

什么是DP呢

 动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。 动态规划算法的基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。

能采用DP去做的前提一般具有的性质

1、最优子结构;2、无后效性(不被前面影响);

背包DP

<1>01背包

有N件物品和一个容量是V的背包,每件物品只能使用一次。 第i件物品的体积是Vi,价值是Wi. 求将哪些物品装入背包,使其总体积不超过背包容量,且总价值最大。输入N,V后,接下来N行,输入Vi,Wi。


问题分析:


01背包本质是由上层状态转移而来(满足条件的前提),由于其只能选一次,状态的转移沿对角线方向;初始化为题目所要求;(一般为f[0][0] = 0; memset(f, 0, sizeof(f)););
当某一层状态转移满时,其后的状态可能会出现不可预料的错误(一般是由于初始化导致的)(初始化非常重要!!!)(其实最优解就在最后一行的某个地方);当初始化为0xcf时, 可能会出现f[][]数组中有0xcf+符合条件的最优价值的情况;cout << f[n][m]; 也无法出现最优解;(其实f数组中可能根本没有最优解,(初始化已经出现错误,体现了初始化的重要性),max也没用);


朴素做法:

点击查看代码

#include <iostream>
#include <cstring>
using namespace std;
int f[2005][2005]; //f[i][j]表示前i件物品,容量为j时最大价值; 
int m, n;
int w[10005], c[10005];
int main() {
	cin >> m >> n;
	memset(f, 0, sizeof(f));
	for (int i = 1; i <= n; i++) {
		cin >> w[i] >> c[i];
	}
	f[0][0] = 0;
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j <= m; j++) {
			f[i][j] = f[i - 1][j];
		}
		for (int j = w[i]; j <= m; j++) {
			f[i][j] = max(f[i][j], f[i - 1][j - w[i]] + c[i]);
		}
	}
	cout << f[n][m];
	return 0;
}

*也可以优化成滚动数组做法:

点击查看代码
#include <bits/stdc++.h>
using namespace std;
int c[10005], w[10005]; //w[i]为第i件物品的重量,c[i]为第i件物品的价值; 
int f[10005]; //f[i]为 容量为i时的最大价值; 
int m, n; //m为背包容量,n为总物品数量;
int main() {
	cin >> m >> n;
	memset(f, 0, sizeof(f)); //初始化,等待赋值; 
	for (int i = 1; i <= n; i++) {
		cin >> w[i] >> c[i];
	}
	f[0] = 0; //初始化,当容量为0时最大价值为0; 
	for (int i = 1; i <= n; i++) {
		for (int j = m; j >= w[i]; j--) {
			f[j] = max(f[j], f[j - w[i]] + c[i]);
		}
	}
	cout << f[m];
	return 0;
}

Q:为什么要内层循环要倒序呢?

A:因为每种物品只能选一次, 其实就是第i层的状态只能由第i-1层的状态转移一次得来,若顺序,则当第i层第j个状态被更新时,它就到了第i-1层,后面的第i层的j + w[i]个状态又可能被此状态更新,这样一个物品就拿了两次、 如此循环往复,则物品都能哪无限次了。

接下来是最终优化版:

点击查看代码
#include<bits/stdc++.h>
#define N 1145
using namespace std;
int v[N],w[N];
int n,V;
int f[N];
int main()
{
	cin>>n>>V;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
	for(int i=1;i<=n;i++)
	{
		for(int j=V;j>=v[i];j--)
		{
			f[j]=max(f[j],f[j-v[i]]+w[i]);
		}
	}
	cout<<f[V];
	return 0;
}

<2>完全背包

也就是将物品改成无限个。


在上面Q&A中已经提及,只需把内层循环顺序即可。

点击查看代码
#include<bits/stdc++.h>
#define N 100000
using namespace std;
int m,n;
int f[N],v[N],w[N];
int ans;
int main()
{
	cin>>m>>n;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
	for(int i=1;i<=n;i++)
	{
		for(int j=v[i];j<=m;j++)
		{
			f[j]=max(f[j],f[j-v[i]]+w[i]);
		}
	
	}
	
	cout<<"max="<<f[m];
	return 0;
}

<3>多重背包

也就是一个物品取有限次。


朴素版本:

点击查看代码
#include<bits/stdc++.h>
#define N 1010
using namespace std;
int f[N],v[N],w[N],c[N];
int n,m;
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i]>>c[i];
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=c[i];j++)
		{
			for(int k=m;k>=v[i];k--)
			{
				f[k]=max(f[k],f[k-v[i]]+w[i]);
			}
		}
	}
	cout<<f[m];
	return 0;
}

显然,时间复杂度飙到了O(n^3),遇到大点的数据就寄。

这里考虑二进制优化:

也就是将一个物品分解成多个二进制数表示的形式,然后再用01背包求解。

如:17=2+4+8+2+1

代码实现:

点击查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m, num;
int w[1000005], c[1000005]; //要开大一点,因为要分解; 
int f[1000005];
int main() {
	cin >> n >> m; //物品数量和背包容积; 
	int a, b, num; //每件物品的重量,价值,个数; 
	int cnt = 0;
	for (int i = 1; i <= n; i++) {
		cin >> a >> b >> num;
		for (int j = 1; j <= num; j <<= 1) { // j *= 2; 
			w[++cnt] = a * j;
			c[cnt] = b * j;
			num -= j; //这样就可以保证每件物品只能选一次(因为如果选多次就超过了原来的num;
		}
		if (num) { //如果num不能完全分解,那就把剩下的直接存入; 
			w[++cnt] = a * num;
			c[cnt] = b * num;
		}
	}
	for (int i = 1; i <= cnt; i++) {
		for (int j = m; j >= w[i]; j--) {
			f[j] = max(f[j], f[j - w[i]] + c[i]);
		}
	}
	cout << f[m];
	return 0;
}

<4>混合背包

混就完事了

点击查看代码
#include<bits/stdc++.h>
#define N 10010
using namespace std;
int m,n;
int f[N],w[N],v[N],s[N];
int main()
{
	cin>>m>>n;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i]>>s[i];
	for(int i=1;i<=n;i++)
	{
		if(s[i]==0)
		{
			for(int j=v[i];j<=m;j++)
			{
				f[j]=max(f[j],f[j-v[i]]+w[i]);
			}
		}else
		{
			for(int j=1;j<=s[i];j++)
			{
				for(int k=m;k>=v[i];k--)
				{
					f[k]=max(f[k],f[k-v[i]]+w[i]);
				}
			}
		}
	}
	cout<<f[m];
	return 0;
}

<5>分组背包

一个旅行者有一个最多能用V公斤的背包,现在有n件物品,它们的重量分别是W1,W2,...,Wn,它们的价值分别为C1,C2,...,Cn。
这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。
求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

其实分着跑01背包,按组号升序排列,然后每组跑倒着的01就行。

点击查看代码

#include<bits/stdc++.h>
#define N 10010
using namespace std;
int m,n,t;
int f[N],v[201][201],w[201][201],s[N];
int tot;
int main()
{
	cin>>m>>n>>t;
	for(int i=1;i<=n;i++)
	{
		int a,b,c;
		cin>>a>>b>>c;
		s[c]++;
		v[c][s[c]]=a;
		w[c][s[c]]=b;
	}
	for(int i=1;i<=t;i++)
	{
		for(int j=m;j>=0;j--)
		{
			for(int k=1;k<=s[i];k++)
			{
				if(j>=v[i][k])
				{
					f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
				}
			}
		}
	}
	cout<<f[m];
	return 0;
}

线性DP

例题:


求最长上升序列

设有由n个不相同的整数组成的数列,记为:b(1)、b(2)、……、b(n)且b(i)<>b(j) (i<>j),若存在i1<i2<i3< … < ie 且有b(i1)<b(i2)< … <b(ie)则称为长度为e的不下降序列。程序要求,当原数列出之后,求出最长的上升序列。例如13,7,9,16,38,24,37,18,44,19,21,22,63,15。例中13,16,18,19,21,22,63就是一个长度为7的不下降序列,同时也有7 ,9,16,18,19,21,22,63长度为8的不下降序列。

点击查看代码
#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
int a[100005];
int f[100005];
int g[100005];
int n;
void output(int x) { //递归输出序列;
	if (x) {
		cout << a[x] << ' '; //倒序输出,因为下面i是倒着找的,所以这里要正序输出;
		output(g[x]);
	}
}
int main() {
	memset(a, 0, sizeof(a));
	memset(g, 0, sizeof(g));
	n = 1;
	while(scanf("%d", &a[n]) != EOF) n++;
	n--;
	for (int i = 1; i <= n; i++) {
		f[i] = 1; //当长度(最后一位下标)为1时,最长上升序列长度为1;
	}
	int ma = -1;
	for (int i = n - 1; i >= 1; i--) { //倒序循环,一步步往前更新;
		for (int j = i + 1; j <= n; j++) {
			if (a[i] < a[j]) { //要求其他序列,改成其它不等符号即可;
				if (f[i] < f[j] + 1) {
					f[i] = f[j] + 1;
					g[i] = j;
				}
			}
			ma = max(ma, f[i]);
		}
	}
	cout << ma << endl; //序列最长长度;
	ma = -1;
	int o = 0;
	for (int i = 1; i <= n; i++) {
		if (ma < f[i]) {
			ma = f[i];
			o = i; //找出最大f的下标;
		}
	}
	output(o); //递归输出;
	return 0;
}

做此类线性时,不妨多开几维,找到其转移方程,阶段划分明确一些。

区间DP

设f[i][j]表示区间i到j上的最优解,可以运用分治的思想,将此区间分成f[i][k]和f[k + 1][j] + 合并这两个区间的价值,依次枚举k并比较大小,同时更新f[i][j],最后输出所需区间的最大值;
可以发现,区间DP是属于线性DP的,其在线性基础上运行,但和线性DP不同,它的特征是分治,这也是写状态转移方程的重要依据。


这里附上hszxoj上的一些例题:

石子合并

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
int f[501][501],sum[501];
int mmin,mmax;
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		int t;
		cin>>t;
		sum[i]=sum[i-1]+t;
	}
	memset(f,0x3f,sizeof(f));
	for(int i=1;i<=n;i++) f[i][i]=0;
	for(int len=2;len<=n;len++)
	{
		for(int l=1;l<=n-len+1;l++)
		{
			int r=l+len-1;
			for(int k=l;k<r;k++)
			{
				f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]);
			}
			f[l][r]+=sum[r]-sum[l-1];
			mmin=max(mmin,f[l][r]);
		}
	} 
	cout<<mmin<<endl;
	memset(f,0,sizeof(f));
	for(int i=2;i<=n;i++)
	{
		for(int j=1;j<=n-i+1;j++)
		{
			int r=i+j-1;
			for(int k=j;k<r;k++)
			{
				f[j][r]=max(f[j][r],f[j][k]+f[k+1][r]);
			}
			f[j][r]+=sum[r]-sum[j-1];
			mmax=max(mmax,f[j][r]); 
		}
	}
	cout<<mmax;
	return 0;
} 

石子合并 (环)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
int f[114*2+1][114*2+1],sum[114*2+1];
int a[114*2+1]; 
int mmax,mmin=0x3f3f3f;
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		a[n+i]=a[i];
	}
	for(int i=1;i<=2*n;i++) sum[i]=sum[i-1]+a[i]; //用2*n模拟环
//	for(int i=1;i<=n+n;i++)
//	{
//		cout<<sum[i]<<" ";
//	}
	for(int len=2;len<=n;len++)
	{
		for(int i=1;i<=2*n-len+1;i++)
		{
			int j=len+i-1;
			for(int k=i;k<j;k++)
			{
				f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]);
			}
			f[i][j]+=sum[j]-sum[i-1];
		}
	}
	for(int i=1;i<=n;i++)
	{
		mmax=max(mmax,f[i][i+n-1]);
	}
	memset(f,0x3f,sizeof(f));
	for(int i=1;i<=2*n;i++) f[i][i]=0;
	for(int len=2;len<=n;len++)
	{
		for(int i=1;i<=2*n-len+1;i++)
		{
			int j=len+i-1;
			for(int k=i;k<j;k++)
			{
				f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]);
			}
			f[i][j]+=sum[j]-sum[i-1];
		}
	}
	for(int i=1;i<=n;i++)
	{
		mmin=min(mmin,f[i][i+n-1]);
	}
	cout<<mmin<<endl<<mmax; 
	return 0;
}

引用Peppa_Even_Pig对此的板子:

点击查看代码
for (int len = 2; len <= 总长度; len++) { //枚举区间长度;
	for (int i = 1; i + len - 1 <= 总长度; i++) { //枚举区间左端点;
		int j = i + len - 1; //区间右端点;
		for (int k = i; k < j; k++) { //枚举区间中的断点;
			f[i][j] = max(f[i][j], f[i][k] + f[k + 1][j] + value); //这里的+号可以依题意变为其它的,value为合并这两个区间所能产生的价值,因题而异;
		}
	}
}

posted @ 2024-02-17 14:44  MrMorgan_Arthur  阅读(86)  评论(9)    收藏  举报
GenerateContentList();