背包问题(0-1背包、完全背包问题)(动规、滚动数组)——附完整代码

1 多阶段动态规划问题

多阶段动态规划问题:有一类动态规划可解的问题,它可以描述为若干有序序列的阶段,且每一个阶段都只与上一个阶段有关。
在这里插入图片描述
如上图所示状态F属于阶段3,它由状态2的状态C和状态D推得。显然对于这种问题,只需要从第一个问题开始,按照阶段的顺序解决每个阶段中状态的计算,就可以得到最后一个阶段中的状态的解。
0-1背包就是这样一个列子。

2 0-1背包问题

2.1 问题描述

有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价最大。其中每件物品都只有1件。

样例:
5 8 //n==5,V==8
3 5 1 2 2 //w[i]
4 5 1 2 3 //c[i]

2.2 求解

2.2.1 暴力求解

显然用暴力枚举每一件物品放或者不放进背包,显然每件物品都有两种选择,因此n件物品就有2 n种情况,而O(2n)复杂度显然很糟糕,用动态规划可以将复杂度降为O(nV)

实现代码:

#include <cstdio>

const int MAXN = 30;
int w[MAXN],c[MAXN];
int n, V, maxValue;

void DFS(int index, int sumW, int sumC){//当前的物件编号、总重量、总价值
  if(index == n) return;

  DFS(index + 1, sumW, sumC);

  //剪枝,只有加入重量后未超过V,才加入V
  if(sumW + w[index] <= V){
    if(sumC + c[index] > maxValue) maxValue = sumC + c[index];
    DFS(index + 1, sumW + w[index], sumC + c[index]);
  }
}

int main(int argc, char const *argv[])
{

    scanf("%d%d", &n, &V);
    for (int i = 0; i != n; ++i)
    {
        scanf("%d", &w[i]);
    }
    for (int i = 0; i != n; ++i)
    {
        scanf("%d", &c[i]);
    }
    
    DFS(0, 0, 0);//初始为零物件,当前总质量和总价值为0
    printf("%d\n", maxValue);

    return 0;
}

2.2.2 动态规划

2.2.2.1 时间复杂度优化

令dp[i][v]表示前i件物品( 1 \leq i \leq n, 0 \leq v \leq V)恰好装入容量为v的背包中所能获得的最大值。

考虑对i件物品的选择策略,有两种策略:

  • 1 不放第i件物品,那么问题转化为前i-1件物品恰好装入容量v的背包中所能获得的最大价值,也即dp[i-1][v];
  • 2 放第i件物品,那么问题转化为前i-1件物品恰好装入容量v-w[i]的背包中所能获得的最大价值,也即dp[i-1][v - w[i]] + c[i]。

由这两种策略,且要求获得最大价值,因此:
d p [ i ] [ v ] = m a x 1 i n w [ i ] v V { d p [ i 1 ] [ v ] , d p [ i 1 ] [ v w [ i ] ] + c [ i ] } dp[i][v] = max_{ 1 \leq i \leq n,w[i] \leq v \leq V }{\{dp[i-1][v], dp[i-1][v - w[i]]+ c[i]\}}

由于dp[i][v]只与前状态dp[i-1][]有关,所以可以枚举i从1到n,v从0到V,通过边界dp[0][v]=0(0 \leq v \leq V)(前0件物品放入任何容量v的背包中都只能获得价值0)。由于dp[i][v]表示的是恰好为v的情况,所以需要枚举dp[n][v](0 \leq v \leq V),取其最大值才是最后的结果。

代码:

for (int i = 1; i <= n; ++i)
{
    for (int v = w[i]; v <= V; ++v)
    {
        dp[i][v] = max(dp[i-1][v], dp[i-1][v -w[i]]+ c[i]);
    }
}

2.2.2.1 空间复杂度优化

时间和空间复杂度都是O(nV),使用滚动数组的方法可以将空间复杂度优化,降低为O(V)。

  • 滚动数组:滚动数组是DP中的一种编程思想。简单的理解就是让数组滚动起来,每次都使用固定的几个存储空间,来达到压缩,节省存储空间的作用。

在这里插入图片描述

如上图所示,每次计算dp[i][v]时,总是只需要dp[i-1][v]左边部分(图中阴影部分),且当计算dp[i+1][]部分时,dp[i-1]又完全用不到(只需用到dp[i]),因此可以直接开一个一维数组dp[v] (即把第一维省略),枚举方向变为i从1到n,v从V到0(倒序),这样状态方程变为
d p [ v ] = m a x 1 i n w [ i ] v V { d p [ v ] , d p [ v w [ i ] ] + c [ i ] } dp[v] = max_{ 1 \leq i \leq n,w[i] \leq v \leq V }{\{dp[v], dp[v - w[i]]+ c[i]\}}

理解为每计算出一个dp[i][v],将相当于把dp[i-1][v]抹掉,因为后面的运算中dp[i-1][v]再也用不到了。

代码如下

for (int i = 1; i <= n; ++i)
{
    for (int v = V; v >= w[i]; --v)//逆序枚举V
    {
        dp[v] = max(dp[v], dp[v -w[i]]+ c[i]);
    }
}
  • 注意:如果用二维数组枚举,v的枚举正序和倒序都可以,如果使用一维数组存放,v必须倒着枚举(如果还是正序枚举,会导致有物件被重复计算,不符合每个物件只有一个的情况)。

2.2.2.2 完整求解代码

样例:
5 8 //n==5,V==8
3 5 1 2 2 //w[i]
4 5 1 2 3 //c[i]

输入:
5 8
3 5 1 2 2
4 5 2 1 3
输出:
10

代码:

#include <cstdio>
#include <algorithm>

using std::max;

const int maxn = 100;//物品最大件数
const int maxv = 1000;//v的上限
int w[maxn];//重量
int c[maxn];//价值
int dp[maxv];//能获得的最大价值

int main(int argc, char const *argv[])
{
    int n, V;
    scanf("%d%d", &n, &V);
    for (int i = 1; i <= n; ++i)
    {
        scanf("%d", &w[i]);
    }
    for (int i = 1; i <= n; ++i)
    {
        scanf("%d", &c[i]);
    }

    //边界:前0件物品放入任何容量的背包价值都只能是0
    for (int v = 0; v <= V; ++v)
    {
        dp[v] = 0;
    }

    for (int i = 1; i <= n; ++i)
    {
        for (int v = V; v >= w[i] ; --v)
        {
            dp[v] = max(dp[v], dp[v - w[i]] + c[i]);
        }
    }

    int max = 0;
    for (int v = 0; v <= V; ++v)
    {
        if(dp[v] > max){
            max = dp[v];
        }
    }

    printf("%d\n", max);
    return 0;
}

2.2.2.3 总结

动态规划如何避免重复计算在0-1背包问题上非常明显,一开始的暴力枚举每件物品放或者不放入背包时,忽略了第i件物品放或者不放而产生的最大值是完全由前面的i-1件物品的最大值来决定的,而暴力做法无视了它。

0-1背包问题的每一个问题都可以看成一个阶段,这个阶段的状态由dp[i][0]~dp[i][V],它们均由上一个阶段的状态得到。

  • 事实上,对于能够划分阶段的问题来说,都可以尝试把阶段作为状态的一维,这样可以更方便的满足无后效性的状态。

  • 如果当前设计的状态不满足无后效性,不妨把状态进行升维,即增加一维或多维来表示相应的信息,这样可能就能满足无后效性

3 完全背包问题

3.1 问题描述

有n种物品,每种物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价最大。其中每件物品都有无穷件。

3.2 思路

令dp[i][v]表示前i件物品( 1 \leq i \leq n, 0 \leq v \leq V)恰好装入容量为v的背包中所能获得的最大值。

考虑对i件物品的选择策略,有两种策略:

  • 1 不放第i件物品,那么问题转化为前i-1件物品恰好装入容量v的背包中所能获得的最大价值,也即dp[i-1][v];
  • 2 放第i件物品,这里处理和01-背包问题不同,因为0-1背包问题每个物件都只能选择一件,因此选择放第i件物品必须转移到dp[i-1][v-w[i]]这个状态,但是完全背包却不同,可以放任意件,放了第i件物品后还可以放第i件物品,直到二维的v-w[i]无法保持大于等于0为止。
    由上面的分析可以写出状态转移方程:
    d p [ i ] [ v ] = m a x 1 i n w [ i ] v V { d p [ i 1 ] [ v ] , d p [ i ] [ v w [ i ] ] + c [ i ] } dp[i][v] = max_{ 1 \leq i \leq n,w[i] \leq v \leq V }{\{dp[i-1][v], dp[i][v - w[i]]+ c[i]\}}
    边界:d[0][v] = 0 (0 \leq v \leq V)

与0-1背包问题的区别在于max的第二个参数是dp[i]而不是dp[i-1]。

写成一维,
状态转移方程:
d p [ v ] = m a x 1 i n w [ i ] v V { d p [ v ] , d p [ v w [ i ] ] + c [ i ] } dp[v] = max_{ 1 \leq i \leq n,w[i] \leq v \leq V }{\{dp[v], dp[v - w[i]]+ c[i]\}}
边界:d[v] = 0 (0 \leq v \leq V)

与0-1背包问题的区别在于v的枚举必须是正向枚举。

代码:

    for (int i = 0; i <= n; ++i)
    {
        for (int v = w[i]; v <= V; ++v)//正向枚举v
        {
            dp[v] = max(dp[v], dp[v -w[i]] + c[i]);
        }
    }

在这里插入图片描述
如上图所示,计算dp[i][v]需要用到阴影部分的,如果让v从小到大枚举,d[i][v-w[i]]就总是已经计算出来的结果;而计算出dp[i][v]之后dp[i-1][v]就再也用不到了,可以直接覆盖。

3.3 示例

样例:
5 8 //n==5,V==8
3 5 1 2 2 //w[i]
4 5 1 2 3 //c[i]

输入:
5 8
3 5 1 2 2
4 5 2 1 3
输出:
16

代码:

#include <cstdio>
#include <algorithm>

using std::max;

const int maxn = 100;//物品最大件数
const int maxv = 1000;//v的上限
int w[maxn];//重量
int c[maxn];//价值
int dp[maxv];//能获得的最大价值

int main(int argc, char const *argv[])
{
    int n, V;
    scanf("%d%d", &n, &V);
    for (int i = 0; i < n; ++i)
    {
        scanf("%d", &w[i]);
    }
    for (int i = 0; i < n; ++i)
    {
        scanf("%d", &c[i]);
    }

    //边界:前0件物品放入任何容量的背包价值都只能是0
    for (int v = 0; v <= V; ++v)
    {
        dp[v] = 0;
    }

    for (int i = 0; i <= n; ++i)
    {
        for (int v = w[i]; v <= V; ++v)
        {
            dp[v] = max(dp[v], dp[v - w[i]] + c[i]);
        }
    }

    int max = 0;
    for (int v = 0; v <= V; ++v)
    {
        if(dp[v] > max){
            max = dp[v];
        }
    }

    printf("%d\n", max);
    return 0;
}
发布了377 篇原创文章 · 获赞 52 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qq_33375598/article/details/104461218
今日推荐