前言:
本人三本菜鸡,在大一的时候,室友选c++期末大作业选的正好是这个,当时也在网上找了一下这个算法的相关代码,但是看不懂。后来自学算法的时候学到了动态规划,正好遇到了这个题,可是时过半年,还是没有明白这个动态规划,或者是说明白了思想,但是不会写代码。后来因为要准备蓝桥杯了(因为学校没有ACM队),所以还是强迫自己弄明白这个,看了很多视频,绕了很多弯路,最后终于明白了这题。因为淋过雨,所以更懂得为别人撑伞,这也是我写这篇文章的原因。
下面进入正题:
0-1背包问题是最经典的dp(动态规划)问题,但是拿它当dp入门感觉还是有点难。而且这个名字经常会误导一些人,有些人看到题目里出现背包二字,就觉得这题是dp问题,实际上dp是一类问题,这类问题往往是在某种条件下找到最优解。
关于这类解,我们需要按照下面的思路:
1.怎么表示当前的状态
2.状态转移方程是什么
3.时间复杂度能否满足
这里可以先看一下点击这里,感受一下动态规划的优点:减少重复计算,降低时间复杂度
那么接下来我们看看0-1背包问题怎么写呢?
题目如下
题目描述
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 v i v_i vi,价值是 w i w_i wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N行,每行两个整数 v i , w i v_i,w_i vi,wi用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0< v i , w i v_i,w_i vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
思路
我们可以看到数据为0~1000
如果用dfs的话,数据量是 2 1000 2^{1000} 21000,数据量大的惊人,如果没有时间限制,我们让计算机一直跑,肯定会出结果的(反正不是我跑)。但是作为小镇做题家,我们是有时间限制的,那么我们怎么做呢?
我们首先定义两个一维数组,分别为w [ ] ,V[ ];这两个数组分别代表价值(worth)和容量(volume)
那接下来按照我们上面写的思路:
1:首先
我们怎么来表示当前的状态呢?我们先思考一下,我们每次选择物品都有两种选择,要么选要么不选。然后有什么限制条件呢?那当然是选的时候,我们得先看看容量够不够啦,容量不够,想放进去也放不进去QAQ。这里通过各位的一顿分析,可以知道有两个变量,也就是
1:选或不选
2:选了容量够不够
这时候我们就可以用一个二维数组来表示一下当前的状态。
我们用dp[i][j]来表示当判断到第i个的时候,我们剩余j个容量,这时候dp[i][j]代表的是这时候我们背包里面物品里面的最大价值。
2:其次
我们该思考一下转移状态方程啦。那我们思考一下状态转移方程是什么呢?我们很容易想到我们判断第i个物品的时候,前i-1个物品我们都判断过了,这时候第i个物品有两种选择
1:不选
2:选
那么我们接下来看一下
第一种情况
也就是不选的情况,不选的话,我们当前的背包的容量和背包里面物品的价值都不会变,也就是
dp[i][j]=dp[i-1][j];
第二种情况
也就是选的情况,选的时候我们得先看看这个背包够不够格对吧,那么我们就需要检查一下背包的容量,背包的容量上面我们定义的是dp[i][j]中的j的值,这时候我们判断的条件是什么呢?很明显就是第i个物品的重量和这时候背包的容量j相比较,如果当前背包的剩余容量可以装下这个物品,也就是j>v[i],这时候我们就可以将它装入了,装入之后我们的体积就需要减少v[i],剩余容量为j-v[i]。这时候价值为dp[i][j]+w[i]。这时候转移方程为
dp[i][j]=dp[i][j-v[i]]+w[i];
到这里就完了嘛?不,还没有完,我们再继续思考一下,貌似有好多种不同的选择方法会走到dp[i][j]这一步,那么我们怎么办呢,当然是取最优的啦,也就是
dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);
最后我们考虑一下时间复杂度是多少呢?很明显是O(m*n)。我们看一下给的数据最大为1000,当m和n取最大值的话,最大的时间复杂度也只是 1 0 6 10^6 106,完全满足题目给的要求。
下面我们附上代码:
#include<iostream>
using namespace std;
const int N=1010;
int w[N];
int dp[N][N];
int v[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][j],dp[i-1][j-v[i]]+w[i]);
}
}
cout<<dp[n][m]<<endl;
return 0;
}
这里的核心代码就是
dp[i][j]=dp[i-1][j];
if(j>=v[i])dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);
那么我们能否再将代码进行优化呢?
答案是可以的,我们可以进行一边输入一边处理。
那么我们又回到了最上面的步骤,既然说了可以优化,那么我们怎么优化呢?我们可以发现上面有一段循环是多余的,就是j在0到v[i]是多余的,这个我们就是处于待优化的状态。既然这一段循环是多余的,那么dp[i][0]~dp[i][v[i]]这段空间我们也可以优化掉,那我们接下来就开始优化啦!
首先
我们想一想这个状态怎么表示呢?我们想到每次输入的时候,这时候只和当前的输入的重量和体积有关,和当前是第多少个物体没有关系,所以我们可以优化掉i的那一层,也就是只用一个一维数组来维护一下当前的状态。也就是dp[j]。那么其中j和dp[j]的含义是什么呢?我们可以思考一下,我们当前只有两个变量,分别是背包的剩余容量和背包的价值,那么根据上面的思路可得,j代表的是当前背包的容量,dp[j]是当前背包的价值。
状态表示有了,状态转移方程是什么呢?我们可以想到上面我们说的,“那么dp[i][0]~dp[i][v[i]]这段空间我们也可以优化掉” 这句话,那么我们怎么优化掉呢?我们只需要对大于当前的价值的dp进行处理,怎么处理呢?
还是有两种情况:
1:不选
2:选
那么这和上面的代码的核心代码差不多,只需要将上面的核心的代码的i给删掉就行了,也就是
dp[j]=max(dp[j],dp[j-v]+w);
最后我们思考一下该代码的时间复杂度是多少呢?很明显是小于O(nm)的,上面的O(nm)都行,那这个代码更行了。同时这段代码优化了一层,空间复杂度由原来的O( n 2 n^2 n2)变成了O(n),直接降了一个度。
代码奉上:
#include<iostream>
using namespace std;
int dp[10010];
int main()
{
int N,M;
cin>>N>>M;
//定义状态dp[i]就是重量为i,价值为dp[i]
for(int i=0;i<N;++i)
{
int v,w;
cin>>v>>w;
for(int j=M;j>=v;--j)
dp[j]=max(dp[j],dp[j-v]+w);
}
cout<<dp[M]<<endl;
return 0;
}
你以为到这里就完了吗?错啦,这里有个我们需要思考的地方,为什么j从m到v,而不是v到m呢?这里就需要读者自己思考啦。
提示:
如果从v到m的话,如果先更新dp[v]的话,dp[v]的值可能会变,然后如果更新dp[v+v]的话可能会重复计算,就提示到这啦,剩下的只能靠读者去悟啦!
如果实在不懂的话我举个例子:
假设有三件物品,背包最大容量为2,价值分别为1,2,3,而它们的体积都是1,这时候我们知道最大价值为3+2=5,而如果j从v到m的话,最后得出来的结果是6,这里需要读者自己模拟一下啦!
至此,本菜鸡花两个小时写的0-1背包问题到此结束!