DP总结

DP总结

DP(动态规划)简介

动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
由于动态规划并**不是某种具体的算法**,而是一种解决特定问题的方法,因此它会出现在各式各样的数据结构中,与之相关的题目种类也更为繁杂。

DP基础

1.必要前提
	需要满足三个条件:最优子结构,无后效性和子问题重叠。 
2.基本思路
	1.将原问题划分为若干 **阶段**,每个阶段对应若干个子问题,提取这些子问题的特征(称之为 状态);
	2.寻找每一个状态的可能 **决策**,或者说是各状态间的相互转移方式(用数学的语言描述就是 **状态转移方程**)。
	3.按顺序求解每一个阶段的问题。

各种DP

背包DP

01背包

image

朴素版
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1010;
//f[i][j]表示前i个物品,体积不超过j时的最大价值
//不选第i个物品时,f[i][j] = f[i-1][j]
//选第i个物品时,f[i][j] = f[i-1][j-v[i]]+w[i],保证j>=v[i] 
int f[maxn][maxn] = {};	//默认全为0,这样后面就不需要再初始化
int n = 0, m = 0;	//n件物品,m为背包总容量
int v[maxn] = {}, w[maxn] = {};	//v表示第i件物品体积,w为第i件物品价值
int main()
{	
	scanf("%d%d", &n, &m);
	for(int i=1; i<=n; i++) scanf("%d%d", &v[i], &w[i]); 
	
	for(int i=1; i<=n; i++)
	{
		for(int j=0; j<=m; j++)
		{
			f[i][j] = f[i-1][j];
			if(j>=v[i]) f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i]);
		}
	}
	printf("%d", f[n][m]);

	return 0;
}
滚动数组优化版
因为每次的动态转移只与i-1层(前i-1个物品)的dp值相关 所以可用二维数组模拟滚动数组以减少内存
终极版

我们发现物品枚举顺序跟结果无关,枚举体积时先枚举体积大的还是小的也不影响最后结果,如果我们枚举体积的时倒序枚举,那在第二层循环中f[] 之前的位置(f[1]~f[j-1])都是在选前i-1件物品是的无后效性的dp值,这样我们就可以省去一维数组,我们用f[j]表示处理当前第i件物品时体积为j的最大价值,递推公式:f[j]=max(f[j],f[j-v[i]]+w[i])。表达式右边的f[j],f[j-v[i]]表示处理完上个物品之后的结果,由于倒序处理处理体积j的时候f[j], f[j-v[i]]还保留着上一行的状态

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1010;
//f[i][j]表示前i个物品,体积不超过j时的最大价值
//不选第i个物品时,f[i][j] = f[i-1][j]
//选第i个物品时,f[i][j] = f[i-1][j-v[i]]+w[i],保证j>=v[i] 
int f[maxn][maxn] = {};	//默认全为0,这样后面就不需要再初始化
int n = 0, m = 0;	//n件物品,m为背包总容量
int v[maxn] = {}, w[maxn] = {};	//v表示第i件物品体积,w为第i件物品价值
int main()
{	
	scanf("%d%d", &n, &m);
	for(int i=1; i<=n; i++) scanf("%d%d", &v[i], &w[i]); 
	for(int i=1; i<=n; i++)
	{
		for(int j=0; j<=m; j++)
		{
			f[i][j] = f[i-1][j];
			if(j>=v[i]) f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i]);
		}
	}
	printf("%d", f[n][m]);
	return 0;
}

完全背包

	完全背包模型与 0-1 背包类似,与 0-1 背包的区别仅在于一个物品可以选取无限次,而非仅能选取一次。
	我们可以借鉴 0-1 背包的思路,进行状态定义:设 f_{i,j} 为只能选前 i 个物品时,容量为 j 的背包可以达到的最大价值。
朴素版 与01背包类似 不过需要k来表示选出的多种物品

状态转移方程 f[i][j] = max(f[i][j], f[i-1][j-kv[i]] + kw[i]);

终极版
考虑做一个简单的优化。可以发现,对于 f(i,j),只要通过 f(i,j-w_i) 转移就可以了

image

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1010;
int f[maxn] = {};	//默认全为0,这样后面就不需要再初始化
int n = 0, m = 0;	//n件物品,m为背包总容量
int v[maxn] = {}, w[maxn] = {};	//v表示第i件物品体积,w为第i件物品价值
int main()
{	
	scanf("%d%d", &n, &m);
	for(int i=1; i<=n; i++) scanf("%d%d", &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]);
		}
	}
	printf("%d", f[m]);
	return 0;

!!!第二维倒序是0/1背包,正序是完全背包!

多重背包

01背包类似 不过要多放相应的物品数量

想到二进制可以表示所有数 所以用二进制对物品数量进行拆分
此处二进制主要解决超时的问题
但要注意 每次分的一种二进制拆分的个数只能有一个,并且是连续(除了无法整分的所剩的部分);
比如说
虽然 8可以用二进制表示为1000;
但不能只用一个8倍的该物品解决
应该改为用1倍 2倍 4倍 1倍解决 以完整表示1~8所有数

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 15000;
const int maxm = 2010;
int n = 0, m = 0;	 
int f[maxm] = {};	 
int v[maxn] = {}, w[maxn] = {}, s[maxn] = {}, cnt = 0;	 
int main()
{	
	int vi = 0, wi = 0, si = 0;
	scanf("%d%d", &n, &m);
	//二进制拆分
	for(int i=1; i<=n; i++)
	{
		scanf("%d%d%d", &vi, &wi, &si);
		if(si > m / vi) si = m / vi;
		for(int j=1; j<=si; j<<=1)
		{
			v[++cnt] = j * vi;
			w[cnt] = j * wi;
			si -= j;
		}
		if(si > 0)
		{
			v[++cnt] = si * vi;
			w[cnt] = si * wi;
		}
	}
	//0/1背包
	for(int i=1; i<=cnt; i++)
	{
		for(int j=m; j>=v[i]; j--)
		{
			f[j] = max(f[j], f[j-v[i]] + w[i]);
		}	
	}
	printf("%d", f[m]);
	return 0;
}

混合背包

之前三种背包的混合 统一转为多重背包
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 15000;
const int maxm = 2010;
int n = 0, m = 0;	 
int f[maxm] = {};	 
int v[maxn] = {}, w[maxn] = {}, s[maxn] = {}, cnt = 0;	 
int main()
{	
	int vi = 0, wi = 0, si = 0;
	scanf("%d%d", &n, &m);
	//二进制拆分
	for(int i=1; i<=n; i++)
	{
		scanf("%d%d%d", &vi, &wi, &si);
		if(si > m / vi) si = m / vi;
		for(int j=1; j<=si; j<<=1)
		{
			v[++cnt] = j * vi;
			w[cnt] = j * wi;
			si -= j;
		}
		if(si > 0)
		{
			v[++cnt] = si * vi;
			w[cnt] = si * wi;
		}
	}
	//0/1背包
	for(int i=1; i<=cnt; i++)
	{
		for(int j=m; j>=v[i]; j--)
		{
			f[j] = max(f[j], f[j-v[i]] + w[i]);
		}	
	}
	printf("%d", f[m]);
	return 0;
}

分组背包(最多选一件)

Ø 每组物品要么一件不取,要么只取其中的一件,跟0/1背包很类似,0/1背包是对单个物品而言,而分组背包是对一组而言
Ø 定义f[i][j]表示第i组物品在背包容量为j对第i组的第k个物品进行决策的最大价值
Ø 动态转移方程为:f[i][j]=max(f[i-1][j],f[i-1][j-w[g[i][k]]]+c[g[i][k]])

朴素版直接跳过
优化版本1
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 40;
const int maxm = 210;
//分组背包
int n = 0, m = 0, t = 0;	 
int v[maxn] = {}, c[maxn] = {}, g[15][maxn] = {};
int f[maxm] = {};
int main()
{	 
	int x = 0;
	scanf("%d%d%d", &m, &n, &t); 
	for(int i=1; i<=n; i++)
	{
		scanf("%d%d%d", &v[i], &c[i], &x);
		g[x][++g[x][0]] = i;
	}
	for(int i=1; i<=t; i++)
	{
		for(int j=m; j>=0; j--)
		{
			for(int k=1; k<=g[i][0]; k++)
			{
				if(j >= v[g[i][k]]) 
				{
					x = g[i][k];
					f[j] = max(f[j], f[j-v[x]] + c[x]);	
				}
			}
		}
	}
	printf("%d", f[m]);
	return 0;
}
优化版本2
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 110;
const int maxm = 110;
int n = 0, m = 0;	 
int f[maxm] = {};	 
int v[maxn][maxn] = {}, w[maxn][maxn] = {}, s[maxn] = {};	 
int main()
{	 
	scanf("%d%d", &n, &m);
	for(int i=1; i<=n; i++)
	{
		scanf("%d", &s[i]);
		for(int j=1; j<=s[i]; j++)
		{
			scanf("%d%d", &v[i][j], &w[i][j]);
		}
	}
	for(int i=1; i<=n; i++)	//阶段
	{
		//i和j共同构成状态
		for(int j=m; j>=0; j--)
		{
			for(int k=1; k<=s[i]; k++)	//k是决策
			{
				if(j >= v[i][k])
				{
					f[j] = max(f[j], f[j-v[i][k]] + w[i][k]);
				}
			}
		}
	}
	printf("%d", f[m]);
	return 0;
}

分组背包(至少选一件)

Ø 基础的分组背包是每组至多选一件的最优,而此题要求是每组至少选1件Ø 我们把分组背包的第二和第三层循环换一下位置,意义就变成了每组物品可以多选,但每一件至多选一次
Ø 那如何解决每组里至少选一件呢??
Ø 定义f[i][j]表示前i种品牌,每种至少选了一件有j的钱买鞋的价值最大。Ø 动态转移方程:如果当前第i组里已经选了至少一件物品,那么接下来就是追求价值最大化,可以0/1背包一样同一组里可以追加能买的最大价值,如果当前组一件也没有买就需要买当前物品并从上一组转移过来
Ø 那我们如何判断当前组至少买了一件和上一组的至少买了一件呢?Ø 我们初始化f数组为-1,f[0][0...V]=0,动态转移方程可以写为• f[i][j]=max(f[i][j],f[i][j-v[g[i][k]]+c[g[i][k]] (f[i][j-v[g[i][k]]!=-1)
• f[i][j]=max(f[i][j],f[i-1][j-v[g[i][k]]+c[g[i][k]] (f[i-1][j-v[g[i][k]]!=-1)
Ø 坑点:在一些题中卖价可以为0,这样两个转移方程的顺序就不能变,因为如果先转移下面的状态可能第i组的第k件物品被选两次

例题

I love sneakers!

二维费用背包(有两种限制条件的背包)

与01背包类似 但因其需要考虑到两种限制条件 所以需要多加入一层循环 以配合其dp的转移

eg.NASA的食物计划
点击查看代码
//二维费用背包
int n = 0, v = 0, m = 0;	 
int a[maxn] = {}, b[maxn] = {}, c[maxn] = {};
int f[maxm][maxm] = {};
int main()
{	 
	scanf("%d%d%d", &v, &m, &n);
	for(int i=1; i<=n; i++)
	{
		scanf("%d%d%d", &a[i], &b[i], &c[i]);
	}
	for(int i=1; i<=n; i++)
	{
		for(int j=v; j>=a[i]; j--)
		{
			for(int k=m; k>=b[i]; k--)
			{
				f[j][k] = max(f[j][k], f[j-a[i]][k-b[i]] + c[i]);
			}
		}
	}
	printf("%d", f[v][m]);
	return 0;
}

背包内物品的组合方案数

砝码称重
coins

在coin中要注意其剪枝 运用一个used数组 去省略第三层循环对每种硬币的数量的判断

在该类问题中 可用一个vis数组表示其中可以到达的方案 最好统计总数

背包方案的输出

CD
Buy the souvenirs
方法一

建立bool二维pre数组 pre[i][j]表示价值为j的最优方案中是否选到i

点击查看代码
if(f[j]<f[j-v[i]]+v[i])
{
	f[j]=f[j-v[i]]+v[i];
	pre[i][j]=1; 
}
.
.
.
for(int i=m,j=n;i>=1;i--)
{
	if(pre[i][j]) 
	{
		cout<<v[i]<<' ';
		j-=v[i];
		}
}
方法二
点击查看代码
if(状态转移成功) pre[j]=i;
...
while(最后的点t)
{
	...
	t=pre[t];
}

背包问题第k优解

Bone Collector II

参考之前分治算法中的归并算法
求第k大时 不能只考虑将dp[i][j]设为第k优的值
因为最终答案里的第k大的值不一定由每次的k值累加而来
而是考虑将前k优的值都存储起来最终进行选择

点击查看代码
cin>>n>>m>>k;
for(int i=0;i<n;i++)
{
	cin>>w[i];
}
for(int i=0;i<n;i++)
{
	cin>>v[i];
}
for(int i=0;i<n;i++)
{
  	for(int j=m;j>=v[i];j--)
	  {
  		int p,x,y,z;
  		for(p=1;p<=k;p++)
		{   //对k个数进行状态转移 
	      		a[p]=f[j-v[i]][p]+w[i];
	      		b[p]=f[j][p];
	   	}
	    a[p]=b[p]=-1;  //二分合并 
	   	x=y=z=1;
	    while(z<=k&&(a[x]!=-1||b[y]!=-1))
		{
	      	if(a[x]>b[y])
			{
	        	f[j][z]=a[x++];
	       	}
	      	else
			{
	        	f[j][z]=b[y++];
	        }
	      	if(f[j][z]!=f[j][z-1]){
			z++;
			}
		}
	}
}
cout<<f[m][k];

线性dp

状态转移方程是线性关系的,即从前向后线性递推

点击查看代码
for(int i=1;i<=n;i++)
{
	for(int j=1;j<=i;j++)
	{
		转移方程
	}
}
线性dp有三大基本模型
  • [最长上升子序列,最长公共子序列,最长公共上升子序列]
    以这三种模型为基础基本可以解决所有线性dp问题

最长上升子序列(LIS)

定义状态:

dp[i]表示以序列的第 i 位结尾的 LIS

很显然,在处理dp[i]时,如果序列的第 j ( 1 ≤ j < i ) 小于第 i 位,说明是满足更新 LIS 的条件的,此时,如果dp[j]+1(加上的 1 是序列的第 i 位)大于dp[i],选择更新
即状态转移方程式为:

dp[ i ]=max{dp[ j ]+1} (1≤j<i and a[ j ]<a[ i ])

以此类推,还有最长不上升子序列,最长下降子序列等变种,做法大同小异

例题 友好城市

最长公共子序列(LCS)

dp[i][j]表示由 a a序列的前 i 项和 b 序列的前 j 项所组成的 LCS 的长度

\[dp[i][j]= \begin{cases} dp[i-1][j-1]+1&& a[i]==b[j] \\ dp[i][j-1] &&a[i]!=b[j]\;and\;dp[i][j-1]>dp[i-1][j] \\ dp[i-1][j] &&a[i]!=b[j]\;and\;dp[i][j-1]<dp[i-1][j] \end{cases} \]

此算法为的O(n^2)
在一些特殊的情况下复杂度可降低为O(nlogn)
详细见blog

最长公共上升子序列(LCIS)

posted @ 2024-02-17 19:52  程立雪  阅读(37)  评论(3编辑  收藏  举报