首发csdn,链接:https://blog.csdn.net/Colicsin/article/details/115403831
问题描述:
*现在给你一个容量为V的背包,有N个物品,其中第i件物品的重量为wi,价值为vi,每件物品只可以拿一次,问在有限的容量内,最多可以拿到多少价值的物品。 *
问题分析:
对于每一个物品,都有两种策略:拿或不拿。
读到这里,是不是脑海中有一个清晰的想法?DFS!确实,这不就是我们常见的dfs问题吗,分别枚举拿和不拿两个状态即可。于是写下了如此代码...
#include
#include
#include
using namespace std;
const int MAXN=1e6+7;
int w[MAXN],N,V,v[MAXN],book[MAXN],end=0;
void dfs(int now,int ans,int rest_v)
{
if(now>N)
{
end=max(ans,end);
return;
}
if(book[now]==false&&rest_v>=w[now])
{
ans+=v[now];
rest_v-=w[now];
book[now]=true;
dfs(now+1,ans,rest_v);
ans-=v[now];
rest_v+=w[now];
book[now]=false;
}
dfs(now+1,ans,rest_v);
}
int main()
{
cin>>V>>N;
for(int i=1;i<=N;i++)
{
cin>>w[i]>>v[i];
}
dfs(1,0,V);
cout<
很遗憾对于洛谷P1048的01背包模板题来说,只有30分,剩下70分全部都是TLE(这是什么人间疾苦!)
点此访问洛谷P1048
不过转念一想好像确实,我们平常dfs模板的代码不就是解决小数据问题的么,对于中等数据量和大数据问题,不会吧不会吧不会吧,不会真的有人想写dfs吧?(老阴阳怪气了)
现在我们手动模拟一下...
很显然,当n特别大的时候(我觉得到100之后就已经够呛了)我们的程序变得特别慢,难免TLE(原来是自己老了哈哈哈哈)
不过,车到山前必有路,我们观察这棵横着的递归树,发现是不是有很多重复遍历的部分?
比如选3,选4这个小方案,在选1,选2、选1不选2、不选1选2、不选1不选2的时候都需要访问一次,现在还只是有4个物品的情况,要是有n个物品,那岂不是2^n的数量级了?实在可怕,这样一想,我们之前的代码TLE的关键原因也出来了,就是重复遍历的太多了。
这时候我们就需要一个maxn_数组来辅助剪枝一下,maxn_[i][j]表示前i个物品还剩j点空间下能存放的物品价值的最大值,设想我们现在从不选1选2走到了选3选4这条路上,但是发现还没走之前所选物品的值已经比其他的路到达选3选4的路时所选的物品的值小了,那这条路我们还有必要去走嘛?Ofcourse not!所以就直接return就好咯,这样子就完成了剪枝任务bingo!
如果折现回到代码上,是这样子...
#include
#include
#include
using namespace std;
const int MAXN=1e6+7;
int w[MAXN],N,V,v[MAXN],book[MAXN],end=0,maxn_[1001][1001];
void dfs(int now,int ans,int rest_v)
{
if(ans<=maxn_[now][rest_v])
return;
maxn_[now][rest_v]=ans;
if(now>N)
{
end=max(ans,end);
return;
}
if(book[now]==false&&rest_v>=w[now])
{
ans+=v[now];
rest_v-=w[now];
book[now]=true;
dfs(now+1,ans,rest_v);
ans-=v[now];
rest_v+=w[now];
book[now]=false;
}
dfs(now+1,ans,rest_v);
}
int main()
{
cin>>V>>N;
memset(maxn_,-1,sizeof(maxn_));
for(int i=1;i<=N;i++)
{
cin>>w[i]>>v[i];
}
dfs(1,0,V);
cout<
不过比起递归,01背包最好的方式还是递推,这也是为什么01背包能当作动态规划初步引入例题的原因了。
我们假设dp[i][j]表示前i件物品用j的容量去装,所能达到的最大价值。(是不是很熟悉?没错,就是刚才记忆化搜索用到的那个判断继续不继续的标准)
对于每一次决策,有两个状态:选或不选
对于每一个状态的值,都依赖于上一个状态的值
满足动态规划的条件√
很容易写出状态转移方程:
dp[i][j]=min(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
说得更清楚一些,dp[i-1][j]不就代表这一件不选,那么就相当于前i-1件物品放入j容量的背包的最大价值,dp[i-1][j-w[i]]代表这一件要了,前i-1件物品放入j-w[i]的容量内的最大价值。
因此我们可以写出以下代码....
#include
#include
#include
using namespace std;
const int MAXN=1e3+7;
int w[MAXN],N,V,v[MAXN],dp[MAXN][MAXN];
int main()
{
memset(dp,0,sizeof(dp));
cin>>V>>N;
for(int i=1;i<=N;i++)
{
cin>>w[i]>>v[i];
}
for(int i=1;i<=N;i++)
{
for(int j=V;j>=0;j--)
{
if(j>=w[i])
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
else
dp[i][j]=dp[i-1][j];
}
}
cout<
但是事情就到这里就结束了吗?当然没有!
没错,还可以接着优化!时间优化?并不,空间优化。毕竟二维数组还是很坑的,万一比赛碰到了一些“善良的死神”,直接MLE了....
从上面我们分析注意到,每一层i的状态都和上一层i-1的状态有关,这样子我们是不是并不需要开二维数组呢?这个时候,滚动数组闪亮出场~
啥是滚动数组?这还要从头忆起,说简单点,滚动数组就是一个很小的数组,一直滚...一直滚...好吧其实就是不断更新的数组。
这里用斐波那契数列来举例子确实再好不过,因为斐波那契数列第i项就是前两项之和,所以对于斐波那契数列来说,有以下代码...
f[1]=1;
f[2]=1;
for(int i=2;i<=n;i++)
{
f[0]=f[1];
f[1]=f[2];
f[2]=f[0]+f[1];
}
这样子最后f[n]就是第n项的值咯~
从滚动数组得到经验,我们用滚动数组来优化我们的二维dp代码...
#include
#include
#include
using namespace std;
const int MAXN=1e3+7;
int w[MAXN],N,V,v[MAXN],dp[100001];
int main()
{
memset(dp,0,sizeof(dp));
cin>>V>>N;
for(int i=1;i<=N;i++)
{
cin>>w[i]>>v[i];
}
for(int i=1;i<=N;i++)
{
for(int j=V;j>=0;j--)
{
if(j>=w[i])
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
cout<
Ps:对于一维数组,j循环必须要倒序遍历,为了防止多重迭代现象;
举个小例子;
假设给定数据:
10 3
5 5
8 7
4 6
刚开始数组dp内都是初始化为0,因为没有装入任何东西
第一遍遍历,从前往后
第二遍遍历,从前往后
第三次遍历,从前往后
很显然,出现了什么情况呢?居然最大到了17,这里我们可以看到,根据状态转移方程式dp[j]=max(dp[j],dp[j-w[i]]+v[i])来看,在第三次遍历(i=3),还未遍历时dp[8]=7。开始遍历之后,dp[8]=max(dp[8],dp[4]+v[3]),这个时候发现肯定是dp[4]+v[3]更大一些,但是这个时候要注意,dp[4]是已经拿完了第三件物品的,按理来说,我们并没有办法再去拿第三件物品了,因为每一个物品只能拿一件,所以就行不通咯,自然就不可以用正序的方法。而倒序的方法则是先尽可能地都拿着,在最大化的前提下去判断,自然是可以的。
01背包问题就到此完结