题目描述:
- 有n种体积和价值分别为wi,viw_i,v_iwi,vi的物品。有一个容量为W(体积)的背包。求出背包能装下的最大价值,每种物品的数量是无限的。
解题思路:
- 动态规划法:
- dp数组含义:dp[i][j]dp[i][j]dp[i][j]=从编号为1−i1-i1−i的物品中挑选物品放入容量为jjj的背包中能得到的最大价值。注意:n种物品编号范围为1-n,0做作递推的起点。
- 初始条件:dp[0][0−W]=0dp[0][0-W]=0dp[0][0−W]=0
- 递推公式:dp[i][j]=max{dp[i−1][j−k∗w[i]]+k∗v[i]∣0≤k&k∗w[i]≤j}dp[i][j]=max\{dp[i-1][j-k*w[i]]+k*v[i]|0≤k\&k*w[i]≤j\}dp[i][j]=max{dp[i−1][j−k∗w[i]]+k∗v[i]∣0≤k&k∗w[i]≤j}
- 结果:结果在dp[n][W]dp[n][W]dp[n][W]中
- 代码:
#include <iostream>
#include <string.h>
using namespace std;
#define Max_n 105
#define Max_w 10005
int n,W;
int w[Max_n],v[Max_n];
int dp[Max_n][Max_w];
//初始化
void initialize(int n,int m)
{
for(int i=0; i<=n; i++)
dp[i][0]=0;
for(int j=0; j<=W; j++)
dp[0][j]=0;
}
//dp[i][j]=max{dp[i-1][j-k*w[i]]+k*v[i]|0≤k&k*w[i]≤j}
void solve_1()
{
for(int i=1; i<=n; i++)
for(int j=1; j<=W; j++)
{
dp[i][j]=-1;
for(int k=0; k*w[i]<=j; k++)
dp[i][j]=max(dp[i][j],dp[i-1][j-k*w[i]]+k*v[i]);
}
}
- 时间复杂度:代码的时间复杂度都是:O(nW2)O(nW^2)O(nW2)
- 动态规划法的一些总结:
- 递推动态规划一般情况的步骤:
- 第一步:确定dp数组的含义
- 第二步:确定递推的初始条件
- 第三步:确定递推公式
- 注意一:
- 第三步的递推的公式其实质是确定了递推的方向:dp[i][j]=max{dp[i−1][j−k∗w[i]]+k∗v[i]∣0≤k&k∗w[i]≤j}dp[i][j]=max\{dp[i-1][j-k*w[i]]+k*v[i]|0≤k\&k*w[i]≤j\}dp[i][j]=max{dp[i−1][j−k∗w[i]]+k∗v[i]∣0≤k&k∗w[i]≤j}确定的递推方向就是从上向下的一个递推方向,即只要知道矩阵的第i行就能推出矩阵的第i+1行:
- 可以使用不同的递推方向:dp[i][j]=max{dp[i][j−1],max{dp[i][j−k∗w[i]]+k∗v[i]∣0≤k&k∗w[i]≤j}}dp[i][j]=max \{dp[i][j-1],max\{dp[i][j-k*w[i]]+k*v[i]|0≤k\&k*w[i]≤j\}\}dp[i][j]=max{dp[i][j−1],max{dp[i][j−k∗w[i]]+k∗v[i]∣0≤k&k∗w[i]≤j}}它的递推方向就是从左下到右上方向,即要求dp[i][j]dp[i][j]dp[i][j]只要知道dp[i][0−j](下)dp[i][0-j](下)dp[i][0−j](下)和dp[i−1][j](左)dp[i-1][j](左)dp[i−1][j](左)
- 需要注意的是递推方向一定要以初始条件为基础。像公式1的起始条件是:dp[0][0-W]=0;而公式2的起始条件为:dp[0][0-W]=0和dp[0-n][0]=0
- 代码:
#include <iostream>
#include <string.h>
using namespace std;
#define Max_n 105
#define Max_w 10005
int n,W;
int w[Max_n],v[Max_n];
int dp[Max_n][Max_w];
//初始化
void initialize(int n,int m)
{
for(int i=0; i<=n; i++)
dp[i][0]=0;
for(int j=0; j<=W; j++)
dp[0][j]=0;
}
//dp[i][j]=max{(dp[i][j-k*w[i]]+k*v[i]|0≤k&k*w[i]≤j),dp[i][j-1]}
void solve_2()
{
for(int i=1; i<=n; i++)
for(int j=1; j<=W; j++)
{
dp[i][j]=dp[i-1][j];
for(int k=1; k*w[i]<=j; k++)
dp[i][j]=max(dp[i][j],dp[i][j-k*w[i]]+k*v[i]);
}
}
- 时间复杂度:代码的时间复杂度都是:O(nW2)O(nW^2)O(nW2)
- 注意二:
- 还有一个地方需要注意:就是要保证递推的正确性,像下面公式就是错误的,它的递推反向是从左到右。dp[i][j]=max{dp[i][j−k∗w[i]]+k∗v[i]∣0≤k&k∗w[i]≤j}dp[i][j]=max\{dp[i][j-k*w[i]]+k*v[i]|0≤k\&k*w[i]≤j\}dp[i][j]=max{dp[i][j−k∗w[i]]+k∗v[i]∣0≤k&k∗w[i]≤j}
时间复杂度的优化:
- 基本想法:
- 在dp[i][j]=max{dp[i−1][j−k∗w[i]]+k∗v[i]∣0≤k&k∗w[i]≤j}dp[i][j]=max\{dp[i-1][j-k*w[i]]+k*v[i]|0≤k\&k*w[i]≤j\}dp[i][j]=max{dp[i−1][j−k∗w[i]]+k∗v[i]∣0≤k&k∗w[i]≤j}的递推下其实还是有重叠子问题:假设我们现在按照递推计算dp[i][j]dp[i][j]dp[i][j],并且假设K∗w[i]<=j<(K+1)∗w[i]K*w[i]<=j<(K+1)*w[i]K∗w[i]<=j<(K+1)∗w[i],
- 于是我需要从这些选项:dp[i−1][j],dp[i−1][j−w[i]]+v[i],dp[i−1][j−2∗w[i]]+2∗v[i],…,dp[i−1][j−K∗w[i]]+K∗v[i]dp[i-1][j],dp[i-1][j-w[i]]+v[i],dp[i-1][j-2*w[i]]+2*v[i],…,dp[i-1][j-K*w[i]]+K*v[i]dp[i−1][j],dp[i−1][j−w[i]]+v[i],dp[i−1][j−2∗w[i]]+2∗v[i],…,dp[i−1][j−K∗w[i]]+K∗v[i]中选取最大值作为dp[i][j]dp[i][j]dp[i][j]
- 选项进行变形:dp[i−1][j],{dp[i−1][j−w[i]],dp[i−1][j−2∗w[i]]+v[i],…,dp[i−1][j−K∗w[i]]+(K−1)∗v[i]}+v[i]dp[i-1][j],\{dp[i-1][j-w[i]],dp[i-1][j-2*w[i]]+v[i],…,dp[i-1][j-K*w[i]]+(K-1)*v[i]\}+v[i]dp[i−1][j],{dp[i−1][j−w[i]],dp[i−1][j−2∗w[i]]+v[i],…,dp[i−1][j−K∗w[i]]+(K−1)∗v[i]}+v[i],即将原来后面的K项提取一个公因式v[i]v[i]v[i],现在就变成:先从dp[i−1][j−w[i]],dp[i−1][j−2∗w[i]]+v[i],…,dp[i−1][j−K∗w[i]]+(K−1)∗v[i]dp[i-1][j-w[i]],dp[i-1][j-2*w[i]]+v[i],…,dp[i-1][j-K*w[i]]+(K-1)*v[i]dp[i−1][j−w[i]],dp[i−1][j−2∗w[i]]+v[i],…,dp[i−1][j−K∗w[i]]+(K−1)∗v[i]选取一个最大值,令最大值为:ZZZ,再从dp[i−1][j],Z+v[i]dp[i-1][j],Z+v[i]dp[i−1][j],Z+v[i]中选取最大值作为dp[i][j]dp[i][j]dp[i][j]
- 现在我们考虑第一步:从dp[i−1][j−w[i]],dp[i−1][j−2∗w[i]]+v[i],…,dp[i−1][j−K∗w[i]]+(K−1)∗v[i]dp[i-1][j-w[i]],dp[i-1][j-2*w[i]]+v[i],…,dp[i-1][j-K*w[i]]+(K-1)*v[i]dp[i−1][j−w[i]],dp[i−1][j−2∗w[i]]+v[i],…,dp[i−1][j−K∗w[i]]+(K−1)∗v[i]选取一个最大值,我们将j−w[j]j-w[j]j−w[j]看作一个整体得到:dp[i−1][(j−w[i])],dp[i−1][(j−w[i])−w[i]]+v[i],…,dp[i−1][j−w[i]−(K−1)∗w[i]]+(K−1)∗v[i]dp[i-1][(j-w[i])],dp[i-1][(j-w[i])-w[i]]+v[i],…,dp[i-1][j-w[i]-(K-1)*w[i]]+(K-1)*v[i]dp[i−1][(j−w[i])],dp[i−1][(j−w[i])−w[i]]+v[i],…,dp[i−1][j−w[i]−(K−1)∗w[i]]+(K−1)∗v[i],这样我们可以看出其实它就是dp[i][(j−w[j])]=max{dp[i−1][(j−w[i])−k∗w[i]]+k∗v[i]∣0≤k&k∗w[i]≤(j−w[i])}dp[i][(j-w[j])]=max\{dp[i-1][(j-w[i])-k*w[i]]+k*v[i]|0≤k\&k*w[i]≤(j-w[i])\}dp[i][(j−w[j])]=max{dp[i−1][(j−w[i])−k∗w[i]]+k∗v[i]∣0≤k&k∗w[i]≤(j−w[i])},故这一步其实重复计算一次dp[i][(j−w[j])]dp[i][(j-w[j])]dp[i][(j−w[j])]
- 数学推导:
dp[i][j]=max{dp[i−1][j−k∗w[i]]+k∗v[i]∣0≤k&k∗w[i]≤j}dp[i][j]=max\{dp[i-1][j-k*w[i]]+k*v[i]|0≤k\&k*w[i]≤j\}dp[i][j]=max{dp[i−1][j−k∗w[i]]+k∗v[i]∣0≤k&k∗w[i]≤j}=max{dp[i−1][j],max{dp[i−1][j−k∗w[i]]+k∗v[i]∣1≤k&k∗w[i]≤j}}=ma x\{dp[i-1][j],max\{dp[i-1][j-k*w[i]]+k*v[i]|1≤k\&k*w[i]≤j\}\}=max{dp[i−1][j],max{dp[i−1][j−k∗w[i]]+k∗v[i]∣1≤k&k∗w[i]≤j}}=max{dp[i−1][j],max{dp[i−1][(j−w[i])−(k−1)∗w[i]]+(k−1)∗v[i]∣0≤(k−1)&k∗w[i]≤j}+v[i]}=ma x\{dp[i-1][j],max\{dp[i-1][(j-w[i])-(k-1)*w[i]]+(k-1)*v[i]|0≤(k-1)\&k*w[i]≤j\}+v[i]\}=max{dp[i−1][j],max{dp[i−1][(j−w[i])−(k−1)∗w[i]]+(k−1)∗v[i]∣0≤(k−1)&k∗w[i]≤j}+v[i]}=max{dp[i−1][j],max{dp[i−1][(j−w[i])−k∗w[i]]+k∗v[i]∣0≤k&(k+1)∗w[i]≤j}+v[i]}=max\{dp[i-1][j],max\{dp[i-1][(j-w[i])-k*w[i]]+k*v[i]|0≤k\&(k+1)*w[i]≤j\}+v[i]\}=max{dp[i−1][j],max{dp[i−1][(j−w[i])−k∗w[i]]+k∗v[i]∣0≤k&(k+1)∗w[i]≤j}+v[i]}并且dp[i][j−w[i]]=max{dp[i−1][(j−w[i])−k∗w[i]]+k∗v[i]∣0≤k&(k+1)∗v[i]≤j}dp[i][j-w[i]]=max\{dp[i-1][(j-w[i])-k*w[i]]+k*v[i]|0≤k\&(k+1)*v[i]≤j\}dp[i][j−w[i]]=max{dp[i−1][(j−w[i])−k∗w[i]]+k∗v[i]∣0≤k&(k+1)∗v[i]≤j}所以得到p[i][j]=max{dp[i−1][j],dp[i][j−w[i]]+v[i]}p[i][j]=max\{dp[i-1][j],dp[i][j-w[i]]+v[i]\}p[i][j]=max{dp[i−1][j],dp[i][j−w[i]]+v[i]}
- 代码:
#include <iostream>
#include <string.h>
using namespace std;
#define Max_n 105
#define Max_w 10005
int n,W;
int w[Max_n],v[Max_n];
int dp[Max_n][Max_w];
//初始化
void initialize(int n,int m)
{
for(int i=0; i<=n; i++)
dp[i][0]=0;
for(int j=0; j<=W; j++)
dp[0][j]=0;
}
\\p[i][j]=max{dp[i-1][j],dp[i][j-w[i]]+v[i]}
void solve_3()
{
for(int i=1; i<=n; i++)
for(int j=1; j<=W; j++)
{
dp[i][j]=dp[i-1][j];
if(w[i]<=j)
dp[i][j]=max(dp[i][j],dp[i][j-w[i]]+v[i]);
}
}
- 时间复杂度:代码的时间复杂度都是:O(nW)O(nW)O(nW)
空间复杂度的优化:
- 基本想法:
- 由上面的分析可知:我其实只需要最后保存dp[n][W]dp[n][W]dp[n][W]就可以了,并且由递推公式p[i][j]=max{dp[i−1][j],dp[i][j−w[i]]+v[i]}p[i][j]=max\{dp[i-1][j],dp[i][j-w[i]]+v[i]\}p[i][j]=max{dp[i−1][j],dp[i][j−w[i]]+v[i]}可以知,我们在求dp[i][j]只依赖于dp[i][0-j]和dp[i-1][j]。即我们在求dp数组的第i行时我们只需要知道dp[i][0]dp[i][0]dp[i][0]和第i−1i-1i−1行就可以了。这样我们可以使用滚动数组来节省空间,即定义一个两行的数组滚动的循环使用这两行。
- 代码:
#include <iostream>
#include <string.h>
using namespace std;
#define Max_n 105
#define Max_w 10005
int n,W;
int w[Max_n],v[Max_n];
int dp_roll[2][Max_w];
void solve_4()
{
//初始化
memset(dp_roll,0,sizeof(dp_roll));
for(int i=1; i<=n; i++)
for(int j=1; j<=W; j++)
{
dp_roll[i&1][j]=dp_roll[(i-1)&1][j];//&1相当于%2
if(j>=w[i])
dp_roll[i&1][j]=max(dp_roll[i&1][j],dp_roll[i&1][j-w[i]]+v[i]);
}
}
- 背包问题进一步优化:
- 在完全背包问题中我们的dp[i][j]dp[i][j]dp[i][j]只使用了上一行的一个元素dp[i−1][j]dp[i-1][j]dp[i−1][j]所以可以直接用一维数组实现。
- 代码:
#include <iostream>
#include <string.h>
using namespace std;
#define Max_n 105
#define Max_w 10005
int n,W;
int w[Max_n],v[Max_n];
int dp_reuse[Max_w];
void solve_5()
{
//初始化
memset(dp_reuse,0,sizeof(dp_reuse));
for(int i=1; i<=n; i++)
for(int j=w[i]; j<=W; j++)
dp_reuse[j]=max(dp_reuse[j],dp_reuse[j-w[i]]+v[i]);
}