背包问题入门

开门见山

背包问题是动态规划中非常典型的例题,新手较为难懂,因为背包问题的动态转移方程不是那么容易直接看出来的,但是理解后会发现实现起来变得非常机械,直接套用动态转移方程即可。看到的大多数资料都是各种奇怪的语法写伪代码,不太好理解,记忆反而不方便,下面直接给出转态转移方程得到的部分代码,可以直接记忆,后续再逐步分析推导,深入理解,加深记忆。

01背包

问题:给定n种物品和一背包。物品 i 的重量为w[i],其价值为 v[i],背包的容量为c。问应该如何选择装入背包中的物品,使得装入背包中物品的总价值最大?

定义dp[i][j]表示 把前i个物品装进容器为j的背包可以获得的最大价值

01背包问题部分代码:

for (int i = 1; i <= n; ++i) {
    for (int j = 1; j <= c; ++j) {
        if (j >= w[i - 1]) {
            dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i - 1]] + v[i - 1]);
        }
    }
}
// 空间优化后
for (int i = 1; i <= n; ++i) {
    for (int j = c; j >= 0; --j) {
        if (j >= w[i - 1]) {
            dp[j] = max(dp[j], dp[j - w[i - 1]] + v[i - 1]);
        }
    }
}
完全背包

完全背包与01背包不同就是每种物品可以有无限多个。

完全背包问题部分代码:

for (int i = 1; i <= n; ++i) {
    for (int j = 1; j <= c; ++j) {
        if (j >= w[i - 1]) {
            dp[i][j] = max(dp[i - 1][j], dp[i][j - w[i - 1]] + v[i - 1]);
        }
    }
}
// 空间优化后
for (int i = 1; i <= n; ++i) {
    for (int j = 1; j <= c; ++j) {
        if (j >= w[i - 1]) {
            dp[j] = max(dp[j], dp[j - w[i - 1]] + v[i - 1]);
        }
    }
}

分析

01背包

背包问题的状态的转移方程是不太可能直接看出来的,需要通过表格的方式逐步推导,了解了具体的计算过程,下面使用具体的计算数据为例,背包容量为10,3件物品,重量分别为3,4,5,价值分别为4,5,6

012345678910
重量价值物品00000000000
34100044444444
45200045559999
56300045669101111

上表中第一列代表背包容量为1-10的,表中数据为对应容量下,背包可以装下物品的最大价值。为了计算方便,同样也考虑了没有物品和背包容量为0时候的值,

在只有物品1的情况下,第一行在容量3以上全部为4,也即物品1的价值,因为这个时候只能拿物品1。为啥3以下为0,很容易理解,因为背包装不下,因此可以可到一个条件就是背包容量需要大于当前物品的重量,否则不用考虑。

在引入物品2后,判断变得不一样,在容量为4的情况下,背包既可以装物品1,也可以装物品2,显然这个时候需要选择物品2,因为价值更大,在只考虑物品1的情况,容量为4的时候,最大价值为4,而在不选物品1,选物品2的情况下,最大价值是5,这里存在比较,比较条件为,不选物品2和选择物品2,不选的值为之前不考虑物品2时,和当前容量一样的值,也即图中蓝色的4,选的值为当前容量减去物品2的重量时背包可以装下的最大值,也即图中蓝色的零,在加上物品2的价值,发现选物品2的价值更大,于是选择物品2,这样便得到图中红色数字5,为此可以得到如下规律。

i个装不下时,所得价值为dp[i - 1][j]
i个可以装的下,所得价值为max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);

根据这个规律便可以得到全表,也即可以得到考虑全部物品,不同容量背包得到的物品的最大价值。

完全背包

与01背包不同是,同一个物品可以无限制取,但是毕竟容量有限,一件物品不可能无限装,有最大上限,即c/w[i],如此就可以将物品i扩展成c/w[i]个,然后再按照01背包的规律便可以得到如下的表格。

012345678910
重量价值物品00000000000
341-100044444444
341-200044488888
341-30004448881212
452-10004558991213
452-200045589101213
563-100045689101213
563-200045689101213

这里分割的方式也可以使用二进制的方式进行优化,这里不做详述

按照这样扩展可以得到最终答案,但是性能有些不足,需要做些优化,对于上表只保留每种物品最后一个物品的值,去除其他行数据,如此便可以得到下面的表格。

012345678910
重量价值物品00000000000
3410004448881212
45200045589101213
56300045689101213

对比与01背包问题,该表格也有一定的计算规律。在只考虑第一种物品时,由于可以再取,在容量为6时与01背包有所不同,可以取两次物品1得到最大价值为8,以图中红色的9为例,当背包容量为7时,考虑是否取物品2时,需要比较的是,不考虑物品2,值为图中上一列蓝色的8,考虑物品2,图中同列蓝色的4,位置为当前背包容量减当前物品重量,值再加上物品2的价值。于是便得到如下规律

i个装不下时,所得价值为dp[i - 1][j]
i个可以装的下,所得价值为max(dp[i - 1][j], dp[i][j - w[i]] + v[i]);
与01背包二者区别在于使用同一列数据做计算。

空间优化

在计算的过程中,对于已经计算过数据其实后续不会再使用,最终也不会再用到,于是可以将二维数组压缩成为一维,这种方式便是滚动数组,得到优化后的状态转移方程。

dp[j] = max(dp[j],dp[j - w[i]] + v[i])

这时会惊奇的发现01背包和完全背包是一样的,确实是一样,但是计算方式上存在差异,对于01背包因为每次计算需要使用上一列的数值,所以在计算时需要反向计算,这样便可以保证,使用之前数值时是未更新之前的,而完全背包需要正向计算,这样使用前值便是更新后的值。

例题

通过上述表格逐步计算,得到状态转移方程,为了强化记忆,需要一些例题巩固,因为背包问题是一种思想,而不是一类问题,解决问题时,可能不太容易看出是背包问题,或者可以使用背包的思想,需要自己去抽象。什么对应背包,什么对应物品,需要是情况而定。

最后一块石头的重量 II

https://leetcode-cn.com/problems/last-stone-weight-ii/

可以抽象为将石头分为两组,对应这物品取和不取,两组总重量差值最小,也就是让需要取的那组重量最大,这样的最大重量便是一种抽象的背包。

int lastStoneWeightII(int *stones, int stonesSize)
{
    int sum = 0;
    for (int i = 0; i < stonesSize; ++i) {
        sum += stones[i];
    }
    int bagSize = sum / 2;
    int *dp = calloc(bagSize + 1, sizeof(int));
    for (int i = 0; i < stonesSize; ++i) {
        for (int j = bagSize; j >= stones[i]; --j) {
            dp[j] = fmax(dp[j], dp[j - stones[i]] + stones[i]);
        }
    }
    return sum - 2 * dp[bagSize];
}
零钱兑换

https://leetcode-cn.com/problems/coin-change/

int coinChange(int *coins, int coinsSize, int amount)
{
    int dp[amount + 1];
    for (int j = 0; j <= amount; ++j) {
        dp[j] = (j == 0) ? 0 : INT_MAX / 2;
    }
    for (int i = 0; i < coinsSize; ++i) {
        for (int j = coins[i]; j <= amount; ++j) {
            dp[j] = fmin(dp[j], dp[j - coins[i]] + 1);
        }
    }
    return dp[amount] == INT_MAX / 2 ? -1 : dp[amount];
}
一和零

https://leetcode-cn.com/problems/ones-and-zeroes/

0和1是两种不同维度的约束条件,是二维背包问题。

int findMaxForm(char ** strs, int strsSize, int m, int n)
{
    int *count0 = calloc(strsSize, sizeof(int));
    int *count1 = calloc(strsSize, sizeof(int));
    for (int i = 0; i < strsSize; i++) {
        for (int j = 0; j < strlen(strs[i]); j++) {
            strs[i][j] == '0' ? count0[i]++ : count1[i]++;
        }
    }
    int dp[m + 1][n + 1];
    memset(dp, 0, sizeof(int) * (m + 1) * (n + 1));
    for (int i = 0; i < strsSize; i++) {
        for (int j = m; j >= count0[i]; j--) {
            for (int k = n; k >= count1[i]; k--) {
                dp[j][k] = fmax(dp[j][k], dp[j - count0[i]][k - count1[i]] + 1);
            }
        }
    }
    return dp[m][n];
}

总结

背包问题是动态规划中非常典型的例题,新手较为难懂,理解后编码会变得非常机械,

不是所有问题都可以明显看出可以使用背包的思想,需要具体问题具体分析,对不同概念抽象成背包和物品,再套用动态转移方程即可。

01背包和完全背包都可以进行空间优化将二维压缩成一维,二者的状态转移方程相同,不同的是计算方向。

实际编码时,可以通过遍历的范围和边界优化下判断条件,dp数组一般从1开始计数,防止在减一时候出错。

更多背包问题可以参考: 背包九讲

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值