01背包 + 完全背包 · 彻底告别DP黑盒系列

总的思路是这样的(如下图),我们今天来解决第二个问题中的第一个问题——背包(其实之前有出过相关专题,但是不是说,书读百遍其意自现嘛)我觉得我自己是每次学习一遍都会有新的体会(比如期末复习时这种感觉尤为强烈,我觉得总有些新的领悟,正是这些平常没有get到的领悟帮助我在期末考试中取得较好成绩!)废话不多说,我们从背包开始!
在这里插入图片描述

背包问题我们将从如下几个方面切入,多的用不到嗷!(之前为了求大而全反而把自己搅糊涂了,其实用的时候往往是一些小而精的点)

在这里插入图片描述

01背包

理论基础

重点是01背包,以及如何将问题转化为01背包

重量价值
物品0115
物品1320
物品2430

在这里插入图片描述

此处两个for循环的顺序是可以颠倒的,因为我们所要的结果在右下角,都是由左方和上方的结果推导而来,所以先遍历物品或是先遍历背包已经无所谓。

悟了悟了,接下来看看怎么优化。

滚动数组

为什么可以将二维变成一维?

有个因素叫 “时间” ,如果空间维度没有压缩空间,就要考虑其他维度。

如何利用时间来压缩空间呢?

细看上面那个二维数组,我们可以发现,每一行其实是考虑了同列上一行的数值(就是自己正上方的那个数字,也即没有放自己这个物品)和放自己这个物品,哪一个价值更大来填的空。

我们实际上是利用了之前填过的结果,也就是说,将第(i-1)行复制到第 i 行,然后再用更大的新值覆盖。

如果我们还是按照物品顺序来遍历背包重量,并且从后往前更新,那么更新时,我们可以拿到之前的结果,因为这一行前面的结果还是第(i - 1)个物品时填的旧值。

什么 “把上一行复制到下一行” 也好,什么 “只与上一行相关” 也罢。我们都是利用了上一行暂存了历史的结果,如果我们可以只用一行,同样也可以做到暂存历史结果,那么就可以只用一行。倒序就机智地解决了这个问题!

我在想这个问题的隐喻:
想要获得某些方面的进步,就必须对现有的解决方式做出改变(eg:逆序)。紧紧围绕我们需要的是什么(eg:历史数据)来采取改变。先将其保留,为我所用。那么就只有先不动它。那么就需要逆序。
怎样才能获得历史的结果?只有站在现在。怎样获得现在的结果?只有站在未来。

vector<int> dp(bagweight+1, 0);
for(int i = 0; i < weight.size(); i++){
    for(int j = bagweight; j >= weight[i]; j--){
        dp[j] = max(dp[j], dp[j-weight[i]] + value[i]);
    }
}
return dp[bagweight];

此处的for循环必须先遍历物品,dp[j]的含义是装满 j 的背包的最大价值;如果先遍历背包,每次只取一个物品,就没有意义了,我们本来就是拿不同的物品去试。

分割等和子集

LC 第416题

这个题我做过的,顺手就凭着直觉写了。反正每种物品只能选一次,且有选和不选两种选项,有一个目标重量,这不就是01背包嘛!

在这里插入图片描述

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for(int i = 0; i < nums.size(); i++) sum += nums[i];
        if(sum & 1) return false;
        else{
            int tar = sum / 2;
            vector<int> dp(tar+1, 0);
            for(int i = 0; i < nums.size(); i++){
                for(int j = tar; j >= nums[i]; j--){
                    dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
                }
            }
            if(dp[tar] == tar) return true;
            else return false;
        }
    }
};

最后一块石头的重量

LC 第1049题

被推导过程误导了,这个题实际上就是上一题,只不过最后对 dp[tar] 的处理稍有变化。

说得天花乱坠,但还是要把握本质。

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int s = stones.size();
        if(s == 1) return stones[0];
        else if(s == 2){
            if(stones[0] == stones[1]) return 0;
            else return stones[0] < stones[1] ? stones[0] : stones[1];
        }
        else{
            int sum = 0;
            for(int i = 0; i < s; i++) sum += stones[i];
            int tar = sum / 2;
            vector<int> dp(tar+1, 0);
            for(int i = 0; i < s; i++){
                for(int j = tar; j >= stones[i]; j--){
                    dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
                }
            }
            return sum - 2 * dp[tar];
        }
    }
};

目标和

LC 第494题

这题问方法数,不是问最大价值,人傻了,方寸大乱。

怀疑过要不要重新定义dp数组,但是不敢妄加定论,解法就是需要重新定义dp数组!

在这里插入图片描述

后面那个goal就是我自己想出来的,用 (sum - target) / 2的结果。

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int s = nums.size();
        int sum = 0;
        for(int i = 0; i < s; i++) sum += nums[i];
        if(sum < target || (sum - target)&1) return 0;
        int goal = (sum - target) / 2;
        vector<int> dp(goal+1, 0);
        dp[0] = 1;
        for(int i = 0; i < s; i++){
            for(int j = goal; j >= nums[i]; j--){
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[goal];
    }
};

一和零

LC 第474题

在这里插入图片描述

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        int s = strs.size();
        int num[s][2];
        for(int i = 0; i < s; i++){
            int num1 = 0, num0 = 0;
            for(int j = 0; j < strs[i].size(); j++){
                if(strs[i][j] == '1') num1++;
                else num0++;
            }
            num[i][0] = num0, num[i][1] = num1;
        }
        vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
        for(int i = 0; i < s; i++){
            for(int j = m; j >= num[i][0]; j--){
                for(int k = n; k >= num[i][1]; k--){
                    dp[j][k] = max(dp[j][k], dp[j-num[i][0]][k-num[i][1]] + 1);
                }
            }
        }
        return dp[m][n];
    }
};

我想经过了01背包,大家对从二维数组到一维滚动数组的推导过程、如何将一堆数尽量均衡地分成两堆、如何找到组合成某个数字的方法数、二维物品重量等题目有了充分的认识。其中最令我印象深刻的还是如何找到组合成某数的方法数,踌躇了好久要不要换dp数组,但是不换根本死路一条啊,换了还有一线生机,足见我的变通还是不够,有些害怕变化,这是不行的。

在这里插入图片描述
继续继续!

完全背包

理论基础

01背包是为了避免同一个物品添加多次,才采用的从后往前遍历,此处我们就是拥有无限多的物品,所以不再care顺序。(顺序即可)

纯的完全背包可以交换内外循环的顺序。

零钱兑换Ⅱ

LC 第518题

在这里插入图片描述

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount+1, 0);
        dp[0] = 1;
        for(int i = 0; i < coins.size(); i++){
            for(int j = coins[i]; j <= amount; j++){
                dp[j] += dp[j - coins[i]];
            }
        }
        return dp[amount];
    }
};

求组合数:外层遍历物品
求排列数:外层遍历背包

组合总和Ⅳ

LC 第377题

说来就来,“顺序不同被视作不同组合”,这不就是求排列数嘛!

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<int> dp(target+1, 0);
        dp[0] = 1;
        for(int j = 0; j <= target; j++){
            for(int i = 0; i < nums.size(); i++){
                if(j >= nums[i] && dp[j] < INT_MAX - dp[j-nums[i]]) dp[j] += dp[j-nums[i]];
            }
        }
        return dp[target];
    }
};

下面对组合和排列进行了总结:
在这里插入图片描述

可见外层循环很重要。如果固定物品,则是组合(无重复);如果是遍历背包,则是排序(有重复)

再一次爬楼梯(完全背包爬楼梯)

LC 第70题

这题想必大家都再熟悉不过,不过我们今天一步不止可以爬 1 级和 2 级,我们可以一步可以跨上 1 级、2 级、3 级…m级,那么想上到 n 楼,有多少种方法呢?

class Solution {
public:
    int climbStairs(int n) {
        vector<int> dp(n+1, 0);
        dp[0] = 1;
        for(int i = 1; i <= n; i++){ //遍历背包
            for(int j = 1; j <= m; j++){ //遍历物品
                if(i >= j) dp[i] += dp[i - j];
            }
        }
        return dp[n];
    }
};

要装满容量为n的背包,物品有1、2、3… m 的大小,一共有多少种方法?(不同的组合算作不同的方法哦)

因为你要上三个台阶,先跨一步再跨两步,和先跨两步再跨一步这是两种不同的方法,所以需要用到我们上面所说的排列数的求法(即外层循环应该为背包而不是物品)

再一次兑换零钱

LC 第322题

在这里插入图片描述

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount+1, 0x3f3f3f);
        dp[0] = 0;
        for(int i = 0; i < coins.size(); i++){
            for(int j = coins[i]; j <= amount; j++){
                dp[j] = min(dp[j], dp[j-coins[i]]+1);
            }
        }
        if(dp[amount] == 0x3f3f3f) return -1;
        else return dp[amount];
    }
};

后面看题解发现这个题用组合和排序都可以,不影响硬币的最小个数(5 5 1和1 5 5都是三个,不影响结果),因为不是求方法数,只要初始化记得最大,还有递推公式写对就行了,但是我的第一反应还是组合。

完全平方数

LC 第279题

在这里插入图片描述

class Solution {
public:
    int numSquares(int n) {
        vector<int> dp(n+1, 0x3f3f3f);
        int x = sqrt(n) + 1;
        dp[0] = 0, dp[1] = 1;
        for(int i = 1; i <= x; i++){
            for(int j = i * i; j <= n; j++){
                dp[j] = min(dp[j], dp[j - i * i] + 1);
            }
        }
        return dp[n];
    }
};

上道了,上道了,事实证明,只要有一本好书 / 一个好老师,我会是个很出色的学生呢。

单词拆分

LC 139题

傻眼了,我得乖乖去做字符串的题了。。看看题解呜呜

这个题解太妙了呜呜呜,只有拜读的份儿啦!

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
        vector<bool> dp(s.size()+1, false);
        dp[0] = true;
        for(int i = 1; i <= s.size(); i++){
            for(int j = 0; j < i; j++){
                string str = s.substr(j, i - j);
                if(wordSet.find(str) != wordSet.end() && dp[j]) dp[i] = true;
            }
        }
        return dp[s.size()];
    }
};

已经做了重点标记,等我刷完字符串再过来看看;

妙处有三: 对容器、字符串等函数调用很妙;(此为基础) 对dp数组的定义很妙; 递推公示很妙。

还要好好学习呀!尾巴放下来,不可以在天上了~

多重背包理论基础

每件物品最多有M件可用

方法一:

把M件摊开,转化为01背包;

方法二:

二进制优化,将每种物品的数量按照二进制方式,打包成一个个独立的包。

大总结

两天!终于写完了!不得不说这些问题处理起来,已经上道了,变得得心应手了,接下来要做的就是更加熟练 快速捡起字符串等基本知识。

五步曲中最关键的两步:递推公式和递推顺序(当然也不要忘记了初始化)

其实有了套路,DP也显得不🚹了。

在这里插入图片描述
在这里插入图片描述

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值