0-1背包问题
N个物品(编号1,2,…,N),
重量 int w[N+1]
价值 int v[N+1]
背包容量 int S
- 具有重叠子问题
- 具有最优子结构
枚举(+剪枝)
每个物品选和不选,形成二叉树,再DFS
动态规划
递归写法:
dp(i,x)表示从1~i号物品中选,背包剩余容量为x,此状态下能达到的最大价值。
- 递推关系
dp(i,x) = max{ dp(i-1,x), dp(i-1,x-w[i])+v[i] } - 递归边界
i=0或x=0时,dp=0
x<0时,dp=-∞,表示不可能出现这种情况,在max中淘汰
#include <climits>
#include <algorithm>
const int NINF = INT_MIN;
int DP(int i, int x){
if(i==0 || x==0) return 0;
if(x<0) return NINF;
return std::max(DP(i-1,x), DP(i-1, x-w[i])+v[i]);
}
int main(){
int ans = DP(N, S);
}
递归中存在子问题的重复计算。实际计算量和枚举+剪枝相同,只不过递归是带着“剩余容量”从后往前枚举,后者是带着“已累计重量”从前往后枚举。
递推写法:
- 状态
dp[i][x]表示从1~i号物品中选,背包剩余容量为x,此状态下能达到的最大价值。 - 状态转移方程
d p [ i ] [ x ] = { m a x { d p [ i − 1 ] [ x ] , d p [ i − 1 ] [ x − w [ i ] ] + v [ i ] } if x ≥ w [ i ] d p [ i − 1 ] [ x ] if x < w [ i ] dp[i][x]=\begin{cases} max\lbrace dp[i-1][x], dp[i-1][x-w[i]]+v[i] \rbrace &\text{if } x\geq w[i] \\ dp[i-1][x] &\text{if } x<w[i] \end{cases} dp[i][x]={ max{ dp[i−1][x],dp[i−1][x−w[i]]+v[i]}dp[i−1][x]if x≥w[i]if x<w[i] - 边界
dp[0][x]=0, (0<=x<=S)
dp[i][0]=0,(1<=i<=N) - 处理顺序
i \ x | 0 | 1 | 2 | 3 | … | S |
---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | … | 0 |
1 | 0 | |||||
2 | 0 | |||||
3 | 0 | |||||
… | … | |||||
N | 0 |
状态转移方程中涉及的状态都在(i,x)点的左上方,或上方,因此处理顺序为从左到右,从上到下。
#include <algorithm>
int dp[N+1][S+1]={
}; //边界
for(int i=1; i<=N; i++){
for(int x=1; x<=S; x++){
if(x>=w[i]) dp[i][x] = std::max(dp[i-1][x], dp[i-1][x-w[i]]+v[i]);
else dp[i][x] = dp[i-1][x];
}
}
int ans = dp[N][S];
最终答案点(N,S)在最右下角,可能涉及的点在其左上方、上方形成路径,但在计算之前无法知道是怎样的路径,因此可能计算了大量的对最终答案无用的点。
动态规划依然无法避免对所有情况的枚举,其效率提升的本质是通过“存储状态+状态的无后效性+状态递推公式”加快了状态的计算,省去了重复计算。
- 时间复杂度上的优化:O(NS)是极限,顶多省去(N,0)~(N,S-1)
- 空间复杂度上的优化:
在计算到中间某一点时,其可能用到的点只有蓝框框住的部分,因此可以把二维数组缩小为一维数组,以x从大到小,i从小到大的顺序更新数组,直到点(N,S)。
int dp[S+1]={
};
for(int i=1; i<=N; i++){
for(int x=S; x>=1; x--){
if(x>=w[i]) dp[x] = std::max(dp[x], dp[x-w[i]]+v[i]);
//else dp[x]=dp[x]
}
}
//进一步优化写法
for(int i=1; i<=N; i++){
for(int x=S; x>=w[i]; x--){
dp[x] = std::max(dp[x], dp[x-w[i]]+v[i]);
}
}
完全背包问题
把每件物品的数量修改为无穷多个。
动态规划
递推写法
- 状态
同上 - 状态转移方程
d p [ i ] [ x ] = { m a x { d p [ i − 1 ] [ x ] , d p [ i ] [ x − w [ i ] ] + v [ i ] } if x ≥ w [ i ] d p [ i − 1 ] [ x ] if x < w [ i ] dp[i][x]=\begin{cases} max\lbrace dp[i-1][x], dp[i][x-w[i]]+v[i] \rbrace &\text{if } x\geq w[i] \\ dp[i-1][x] &\text{if } x<w[i] \end{cases} dp[i][x]={ max{ dp[i−1][x],dp[i][x−w[i]]+v[i]}dp[i−1][x]if x≥w[i]if x<w[i] - 边界
同上 - 处理顺序
新的状态转移方程涉及的点在当前点的左方、上方。因此处理顺序不变。
空间优化后,以i与x都从小到大的顺序处理,
int dp[S+1]={
};
for(int i=1; i<=N; i++){
for(int x=1; x<=S; x++){
if(x>=w[i]) dp[x] = std::max(dp[x], dp[x-w[i]]+v[i]);
//else dp[x]=dp[x]
}
}
//进一步优化写法
for(int i=1; i<=N; i++){
for(int x=w[i]; x<=S; x++){
dp[x] = std::max(dp[x], dp[x-w[i]]+v[i]);
}
}