动态规划(背包九讲 一)

1.背包dp

背包问题有多种其中比较基础的就是01背包

416.分割等和子集1

其它背包问题也都是在01背包的基础上添加了一些条件和限制,所以只要01背包理解了,后面的学习也会比较轻松。

1.背包dp(选择模型)(闫式分析法)

闫式dp分析法:是一种分析dp问题的方法,可以更好理解dp问题。

1.从集合角度来分析dp问题,有限集合中的最值,比如求一个集合中的最大值,或者最小值,

第一个阶段状态表示:化零为整 1.dp[i] 表示一个集合,它代表什么,存的是一个数(只考虑前 i个物品)

2.属性:存的这个值和集合之间是什么关系,max,min。

第二个阶段状态计算化整为零 1.把f[i] 化为若干个部分(若干个子集,子集满足两个条件 1.是不重复,求数量时我们要满足不重复,当我们们求最大值的时候其实是可以重复的,就比如求a,b,c 三个数的最大值,我们先求a,b最大值,再求b,c最大值,b虽然是重复了,但是并不影响我们的最大值,2.不遗漏),每一个部分分别去求,就比如求dp[i] 的最大值,我们可以对每一个子集取max再相加。相当于我们把一个集合的问题分解为每一个子集,然后每一个子集我们都可以再处理一下。

划分的依据找最后一个不同点(选最后一个物品的方法)。

img

dp的优化和分析是分开的这样思路会更加清晰,dp的优化都是对代码做等价变形。

其实dp就是一种递推,我们在做dp有关的题时,要选择好状态参量,同时要想办法构造子问题也就是状态转移方程。

1. 01背包问题

在这里插入图片描述

1.题意

有n件物品,每件物品体积是v,价值是w,每件物品只能使用一次,有一个容量为V的背包,物品总体积不超过背包容量,求出最大价值。

2.基本思路

最基础的背包问题,每种物品只有一件,可以选择放或不放

用子问题定义状态:即dp[i] [j] 表示前i件物品恰放入一个容量为j的背包可以获得的最大价值,很明显我们要求最大价值,那么属性就为max。为什么我们要把体积v作为一个状态参量呢,因为,我们把一个物体放入体积为v的背包中,那么剩余的空间又是一个子问题。当前的最优解不仅和当前物品价值有关,还和剩余的空间能装多少价值的物品有关。例如,你把一个体积为 1的物体放入体积为10的背包中,那么问题就转为体积是9的背包的问题了。就是从一个状态转移到另一个状态。

集合的划分就是状态转移方程,那么对于第 i 个物品,我们就有选和不选两种方案,

1.如果不选: dp[i] [j]=dp[i-1] [j](物品的总数返回前面一个状态,当前容纳的重量不变)

2.如果选:dp[i] [j]=dp[i-1] [j-v[i]]+w[i](选当前物品,物品的总数返回前一个状态,当前可容纳的重量减去物品重量,并加上物品的价值),最后我们要对两种情况取max。

例如,第一个物品为(5,1),第二个物品为(4,2)(前面为体积,后面为价值),假如此时我们在求解背包体积为 9 的问题,那么对于第二个物品我们可以选择,那么**此时就为当前物品的价值加上背包剩余空间(9-4==5)所能选择的物品的价值。**所以如果选择当前物品,此时总价值为当前物品价值 w[i]+不选当前物品时背包剩余空间所能选择前 i-1个物品的价值 dp[i-1] [j-v[i]]+w[i]

img

下面一张图方便大家理解,
例如,当i 等于 0时,没有物品所以价值为0;当j 等于0时,背包容量为0。
当 i=1时背包容量 j=1 时,因为物品体积为2>1,所以第 i个物品无法选择,dp[i] [j]=dp[i-1] [j]=0;

当i=1 ,j=2 时,此时可以选择第 i个物品(背包容积 j>=v[i]),dp[i] [j]=max(dp[i-1] [j],dp[i] [ j-v[i]]+w[i]),就进行判断选和不选。以此类推,我们要一行一行的进行递推。为什么我们要从小往大求,因为大的问题求解需要用到子问题的答案。

例如,当背包容量为 6时,i==2时,对于第二个物品我们可以进行选择,那么此时价值为5,背包还剩3个空间,这三个空间可以选择前 i-1个物品;所以就变成了当背包为 3时,选择前 1个物品dp[1] [3]==3,所以dp[2] [6]=max(dp[i-1] [j],dp[i-1] [j-v[i]]+w[i])=max(dp[1] [6],dp[1] [3]+5)=8。

在这里插入图片描述

3.代码

1.下面是朴素代码

#include<bits/stdc++.h>
using namespace std;
int v[1005],w[1005];
int dp[1005][1005]; //代表在容量为j的情况下对前i个物品选择的最大价值 
int main(){
	int n,v1;
	cin>>n>>v1;
	for(int i=1;i<=n;i++){
		cin>>v[i]>>w[i];
	}
	for(int i=1;i<=n;i++){
		for(int j=0;j<=v1;j++){
			//考虑第i个物品选和不选 
			dp[i][j]=dp[i-1][j]; //如果不选第i个物品,
			//那么背包内的价值就等于前i-1个,背包容量不变 
			if(j>v[j]) //如果选第i个 
			dp[i][j]=max(dp[i-1][j],dp[i][j-v[j]]+w[i]);
			//那么就判断一下选择和不选那个价值更大 
		} 
	}
	cout<<dp[n][v1]; 
}

2.优化代码

我们发现dp[i] [j]都是从dp[i-1] [j]推过来的,但我们进行递推的过程就是从上一个状态转移过来的,所以我们可以把这一维给取消掉。但如果我们从前往后遍历的话,一个物品有可能会被计算多次。例如下面表格,当第一个物品为(5,1)时,背包体积为10 时,背包价值为 2,很明显我们选择第一个物品,那么剩余的体积为 10-5=5,那么体积为 5的背包价值为 1,那么就会多算。这个其实就是后面的完全背包,因为完全背包一个物品可以取多次,这也是完全背包体积正向枚举的原因。

在这里插入图片描述

所以我们可以从后面往前遍历,进行空间优化

#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int dp[N],v[N],w[N]; //dp代表当体积下背包的最大价值
int main(){
	int n,V;
	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--){ //只需要遍历到v[i],因为后面比它小的肯定装不下
			dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
		}
	}
	cout<<dp[V]; //所以dp[V]就为我们所求的
	return 0;
}

2.完全背包

1.思路

例题链接

和 01背包类似,只不过每个物品可以选择无数次

第 i个不选(0个):dp[i] [j]=dp[i-1] [j]

第 i个选(多个):dp[i] [j]=dp[i-1] [j-k*v[i]]+k *w[i],选当前的物品,子问题就是不选当前物品时背包剩余空间所能选择前 i个物品的最大价值,k代表我们选几个当前物品,最后我们要对两种情况取max。

img

2.代码

1.朴素写法

#include<iostream>
using namespace std;
const int N = 1010;
int dp[N][N];
int v[N],w[N];
int main()
{
    int n,m;
    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;i++){
    	for(int k=0;k*v[i]<=j;k++)
           dp[i][j]=max(dp[i][j],dp[i-1][j-k*v[i]]+k*w[i]);
		}
	}
    cout<<dp[n][m];
}

2.代码的优化

f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2v]+2w , f[i-1,j-3v]+3w ,)
f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2v] + w , f[i-1,j-3v]+2*w ,)
由上两式,可得出如下递推关系:
f[i][j]=max(f[i,j-v]+w , f[i-1][j])

知道了上面的关系,那么k循环就可以不要了,就可以优化为下面代码

#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int dp[N][N];
int v[N],w[N];
int main(){
	int n,m;
	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++){
			dp[i][j]=dp[i-1][j];
			if(j>=v[i])
			dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+w[i]);
		}
	}
	cout<<dp[n][m];
}

3.深层理解

我们会发现完全背包其实和 01背包很像,只有这一句不同(注意下标)

dp[i] [j] = max(dp[i] [j],dp[i-1] [j-v[i]]+w[i]); //01背包

dp[i] [j] = max(dp[i] [j],dp[i] [j-v[i]]+w[i]); //完全背包问题

其实就相当于我们可以重复选第 i个物品,而 01背包无法重复选,所以当背包选第 i个物品时,空间有剩余我们可以继续选择第 i个物品,而不是直接转化为选前 i-1个物品。

下面一张图方便大家理解

样例为

4 10 //背包空间为10
2 1  //前面为物品的体积,后面为物品的价值
3 3
4 6
8 10

例如,背包容量为 9,i=2时,我们可以选择第2个物品,此时价值为dp[2] [9-3]+3=9,其实就是背包剩余空间我们接着选择当前物品,也就是在当前层进行自我叠加。而01背包只能在上一层叠加。

我们发现 i=3时,和 i=4时,它们的状态一样,因为我们是先继承上一层的状态,然后进行比较看是选择当前物品结果更优,还是选择上一个物品更优。例如 i==4时,物品体积为 8,空间为小于 8时,我们无法进行选择,只能继承上一个状态。当空间为 8时,如果选择当前物品的话,价值为 10,如果不选当前物品的话,价值为 12,所以很明显继承上一个状态价值更大。

dp[i][j]=dp[i-1][j];
if(j>=v[i])
dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+w[i]);

在这里插入图片描述

下面是两个讲解不错的视频,供大家查阅。

背包算法讲解
背包算法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值