动态规划之4大背包问题详解

动态规划的介绍

动态规划 (DP) 是一种算法技术,它将大问题分解为更简单的子问题,对整体问题的最优解决方案取决于子问题的最优解决方案。

某些问题往往有 2个特征:重叠子问题、最优子结构。而用动规(DP)可以高效率地处理具有这 2个特征的问题。

处理 DP 的大问题和小问题,有两种实现方式 ——自顶向下与记忆化递归 / 自下而上与制表递推(两种实现方式的复杂度相同,但是第二种更为常用)。

以斐波那契为例,两种实现方式的代码分别如下:

// 自顶向下与记忆化递归
int memoize[maxn];   //保存结果
int fib (int n){
    
    
    if (n==1 || n==2)  
        return 1;
    if(memoize[n] != 0)    //直接返回保存的结果,不再递归
        return memoize[n];    
    memoize[n]= fib(n-1) + fib(n-2);   //递归计算结果,并记忆
    return memoize[n];
}
// 自下而上与制表递推
const int maxn = 255;
int dp[maxn];
int fib (int n){
    
    
    dp[1] = dp[2] =1;
    for (int i=3; i<=n; i++)
        dp[i] = dp[i-1] + dp[i-2];
    return dp[n];
}

在这里插入图片描述

01背包

01背包问题最暴力的一个解法:二维动态规划

准备工作:
设第 i个物品的体积是 v[i],价值是 w[i],f[i][j]表示只看前 i个物品,“可用(剩余)的 ”总体积是j的情况下,总价值最大是多少。

result = max{f[n][0~v]}

状态的转移:
每个 i对应的 f[i][j],可以看作一个集合的划分,也就是当前拿与不拿对应两种状态:

当前拿了的话,状态就是:dp[i-1][j-v[i]]+w[i] (+之前的是前i-1个物品对应的最大价值) ; 没拿的话,状态就是:dp[i-1][j]。

f[i][j] = max{1,2} (1和 2分别表示两种状态)

合法的初始化: f[0][0] = 0 一个物品都不选的情况下,总体积和价值都为 0

集合如何划分
一般原则:
不重不漏,不重不一定都要满足 (一般求个数时要满足)。
如何将现有的集合划分为更小的子集,使得所有子集都可以计算出来。

例题
在这里插入图片描述

//AC代码
#include <bits/stdc++.h>
#pragma GCC optimize(3 , "Ofast" , "inline")
using namespace std;
const int N = 1010;

int n,m;
int f[N][N];
int v[N],w[N];

int main()
{
    
    
	cin>>n>>m;
	for(int i=1; i<=n; i++)  cin>>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]);
		} 
	}
	    
	int res = 0;
	for(int i=0; i<=m; i++)  res = max(res,f[n][i]);
	
	cout<<res<<endl;
	return 0 ;
}

01背包的优化
每一个状态只与它的前一个状态有关,不需要把所有的状态记录下来,所以可以用一个 通用的优化方式——滚动数组 进行优化 (针对这道题,可以用一维数组去优化) 。尽量对代码进行 等价代换

优化过的转移方程:f[j] = max(f[j], f[j-w[i]] + v[i])

1. f[i] 仅用到了f[i-1], 
2. j与j-v[i] 均小于j
3. 若用到上一层的状态时,从大到小枚举, 反之从小到大

#include <bits/stdc++.h>
using namespace std;
const int N = 1010;

int n, m;
int v[N], w[N];
int f[N];

int main() 
{
    
    
    cin >> n >> m;
    for(int i=1; i<=n; i++)  cin>>v[i]>>w[i];
    
    for(int i=1; i<=n; i++) 
        for(int j=m; j>=v[i]; j--) 
            f[j] = max(f[j], f[j-v[i]]+w[i]);
            
    cout<<f[m]<<endl;
    return 0;    
}

完全背包

先给出完全背包的一个经典基础例题
在这里插入图片描述

可以发现,完全背包和01背包的题目很相似。其实优化处理后,它们的代码也很相似,但是其中蕴含的道理大相径庭

两者的状态转移方程:

  1. 01背包: f[i][j] = max( f[i-1][j], f[i-1][j-v]+w )
  2. 完全背包: f[i][j] = max( f[i-1][j], f[i][j-v]+w )

相似体现在:1.代码 —> 01背包优化后,第二重循环的 j 由从大到小改为从小到大之后,就能直接应用AC。2.原理 —> 状态表示这一部分(往下看)并无大异,大差不离。

两者在题目中的不同主要体现在:01背包每种物品只能用一次,而完全背包的每种物品都有无限件可以使用。

而他们的内在区别主要体现在:状态计算,即集合的划分(在01背包里,是以第 i 个物品选或者不选为界分成两个集合; 而在完全背包里,因为第 i 个物品有无限件可以选择,所以在枚举时,要划分成若干个子集,而不是两个子集)。

完全背包的步骤(结合下方图片理解):
1.状态表示:化零为整,用一个状态表示一类东西。需要搞清楚两件事:
第一个是 f(i,j) 表示哪个集合
第二个是 f(i,j) 这个集合存的是哪个数、哪种属性
2.状态计算:
枚举 f(i,j) 里所有选择了 0~k 个物品的方案数的集合

在这里插入图片描述
在这里插入图片描述

// 朴素做法
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;

int n,m;
int v[N],w[N];
int f[N][N]; 

int main()
{
    
    
	cin>>n>>m;
	for(int i=1; i<=n; i++)  cin>>v[i]>>w[i];
	
	for(int i=1; i<=n; i++)
	    for(int j=1; j<=m; j++) 
	    {
    
    
	    	f[i][j] = f[i-1][j];
	    	if(j>=v[i])  f[i][j] = max(f[i][j], f[i][j-v[i]+w[i]]);
		}
	
	cout<<f[n][m]<<endl;
	return 0; 
} 

//优化做法 (优化成一维的之后,对于体积会有限制)
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;

int n,m;
int v[N],w[N];
int f[N]; 

int main()
{
    
    
	cin>>n>>m;
	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<<f[m]<<endl;
	return 0; 
} 

多重背包

给出多重背包的一个经典基础例题
在这里插入图片描述

这里数据量很小,可以直接暴力去做。可以发现,和01背包例题的主要区别就是每种物品不是固定的一件,而是最多不超过s[i]件,就是所说的 “多重背包是01背包的一个扩展”(在下方的优化方法中会有相应的解释/证明),所以体积在枚举时要从大到小枚举 。

下面讲一下比较重要的三处

状态表示 f[i] 的含义:总体积是 i 的情况下,最大价值是多少

原始 &未经处理的核心步骤
for(int i=0; i<n; i++)
{
for(int j=m; j>=v[i]; j- -)
f[j] = max( f[j], f[j - v[i]] +w[i], f[j -2* v[i]] +2*w[i]…);
}

初始化时有两种操作

  1. 如果把所有状态都初始化为 0了,那么f[m]就是答案
  2. 若只令f[0]=0,其他的都是负无穷,则从f[0~m]中选一个最大值 即:
    2.1. f[i]=0。答案:f[m]
    2.2. f[0]=0, f[i]=-inf & i!=0。答案:max{f[0… m]}
// AC代码
#include <bits/stdc++.h>
using namespace std;
const int N = 110;

int n,m,f[N];

int main()
{
    
    
	cin >> n >> m;
	for(int i=0; i<n; i++)
	{
    
    
	    int v,w,s;
	    cin >> v >> w >> s;
	    for(int j=m; j>=0; j--)
	        for(int k=1; k<=s && k*v<=j; k++)
	            f[j] = max(f[j], f[j-k*v]+k*w); 
	} 
	cout << f[m] << endl;
	return 0;
}

当数据量变大时,再去暴力的话,10^9会超时。 需要用到多重背包的二进制优化方法

下方为数据量扩大后的一个经典基础例题
在这里插入图片描述
为了帮助理解这种优化方法,我们先来看一下 “怎么把多重背包问题转化为01背包问题”

假设第 i 个物品的体积是 v,价值是 w,件数是 s。实际上可以把这个物品拆开,直接拆成s份,每份的体积是 v’,价值是 w’,放到物品堆中去,每份只能用一次,这就变成了一个01背包问题 。

但是这种拆法的复杂度是十分高的,也会达到1e9。那么我们可以考虑如何使得划分的份数减少的同时,还能保证…

二进制优化方法的由来:
既然每种物品的每一个都去枚举会超时,那么我们考虑把第 i 种物品分成 logS(以2位底上取整)份 ,使得需要枚举的数据量变小【时间复杂度:1000 x logS x 2000 = 2 x 1e7,不会超时 】。

下面用一个例子来说明这样分为何可行:给定任意一个数s,问最少可以把s分成多少个数,使得这些数能组成1~n 的所有数。分成的这些数有2种选法,选和不选,而且它们要能够组成 1~s 的所有数(并且不能凑到>s,即这些数的和不能>s)。

用特例的比较来说明如何取数和放数才具备普适性
s=7,log7上取整为3。2^0=1 , 2^1=2 , 2^2=4, 1 2 4可以组成1~7所有的数
s=10, log10上取整为4…1 2 4 8。但这4个数的和>10,放不下这么多。【这个时候就在 s 够用的情况下,不断减去前面的数,即得到1 2 4 3 这四个数,验证后可以发现符合要求】

结合这个例子就不难推知这种分法是可行的了。

// AC代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2010;

int n,m,f[N];

struct Good {
    
    
    int v,w;
};

int main()
{
    
    
	vector<Good> goods;
	cin >> n >> m;
	for(int i=0; i<n; i++)
	{
    
    
		int v, w, s;
		cin >> v >> w >> s;
		for(int k=1; k<=s; k*=2)
		{
    
    
			s -= k;
			goods.push_back({
    
    v*s,w*s});
		}
		if(s>0)  goods.push_back({
    
    v*s,w*s});
	}
	
	for(auto good:goods)
	    for(int j=m; j>=good.v; j--)
	        f[j] = max(f[j], f[j-good.v]+good.w);
	
	cout<<f[m]<<endl;
	return 0;
}

分组背包

给出分组背包的一个经典基础例题
在这里插入图片描述
下面讲一下比较重要的三处

状态表示f[i][j]的含义:前i组物品,体积是j的情况下,最大价值是多少。

原始 &未经处理的核心步骤
for(int i=0; i<n; i++) //第一重循环枚举物品组
{
for(int j=m; j>=v; j- -) //从大到小枚举体积
f[j] = max( f[j], f[j-v[0]]+w[0], f[j-v[1]]+w[1] ,…, f[j-v[s-1]+w[s-1]] ); //枚举每种物品的s+1种决策
}
最后的f[m]即答案

每种物品所需的决策数目的解释
假设某种物品有s个,则它的决策一共有s+1种:一个都不选->选第0个->…->选第s-1个。

背包问题之间的联系
分组背包也是01背包的一种特殊情况,而多重背包问题可以看作是分组背包的一种特殊情况。

// AC代码(这种背包问题的大分类似乎没有优化方法...)
#include <bits/stdc++.h>
using namespace std;
const int N = 110;

int n,m;
int f[N],v[N],w[N];

int main()
{
    
    
	cin>>n>>m;
	for(int i=0; i<n; i++)
	{
    
    
		int s;
		cin>>s;
		for(int j=0; j<s; j++)  cin>>v[j]>>w[j];
		for(int j=m; j>=0; j--)
		  for(int k=0; k<s; k++)
		    if(j>=v[k])
		      f[j] = max(f[j],f[j-v[k]]+w[k]);
	}
	cout<<f[m]<<endl;
	return 0;
}

猜你喜欢

转载自blog.csdn.net/Luoxiaobaia/article/details/108991702