【算法】小汉堡再探动态规划,01背包智取等和子集

【算法】小汉堡再探动态规划,01背包智取等和子集

参考:代码随想录 (programmercarl.com)

原题链接:416. 分割等和子集 - 力扣(LeetCode)

Part 1.介绍01背包问题

背包问题:

先略后详,一句话概括背包问题,就是如何让背包内物品价值达到最大

有n种物品,每个物品有自己的重量w,有自己的价值v,有一个承重能力为质量m的背包,每种物品有一个或多个,求解这个背包最多可以装载价值为多少的物品。

不同于别的算法思想,01背包这个名字似乎很难“望名生意”,像二分、前缀和等算法,都可以在接触之前通过猜测名称的由来,从而大致了解算法的用途。本篇博客就由01背包名称由来说起。

由刚刚的简介可以得知,背包问题有几个关键要素:背包容量、物品价值、物品重量、物品数量。其中关于物品数量的描述显然具有歧义。实际上根据物品数量的不同,背包问题也被分解为了几个子问题,如图所示:

请添加图片描述

今天我们介绍其中最基础的01背包,上图同时也解释了01背包名称的由来:在01背包问题中,每种物品数量只有一个,故而一个物品也仅有两种状态(被选中或不被选中)。

Part 2.如何用DP解决01背包

在开始最关键的解题过程之前,首先需要理解背包问题为什么可以用动态规划解决,让我们回顾一下在什么情况下,可以优先使用动态规划:

动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。

动态规划中每一个状态一定是由上一个状态推导出来的。

在01背包问题中,我们将问题拆解成两个维度,一个是物品,一个是背包容量,通过一步步增加物品或是背包容积,从而推导出问题的解。

略举一例:已知如下物品及其重量和价值,背包可承重质量为5的物品,求解背包最大可以装下多少价值的物品。

请添加图片描述

背包问题既属于动态规划,便离不开动规五部曲(终于可以套公式力):

  1. 确定dp数组(dp table)以及下标的含义

    如图,建立一个二维的dp数组,dp数组的值表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

请添加图片描述

  1. 确定递推公式

    直接确定递推公式似乎有些困难,不妨先顺着刚刚的思路推导几步,像拼图一样先将边角等关键位置拼接好。(其实就是先初始化再试着推导一下)

    当重量为0时,任何物品都无法放入,故而本列的数值都是0,而容纳量大于等于1时的情况下,都可以放入物品1,故而第一行剩下的部分都可以填入10,即物品1的价值。

请添加图片描述
重点从第二行开始,第二行的首个元素刚刚已经写过,而从第二个元素起,我们需要考虑几种情况:

请添加图片描述

简单来说,递推公式将从两种状态里抉择,一是保持原状,二是容纳新物品。则可以得出递推公式为:

dp[i][j] = fmax(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

从刚刚的推导过程以及递推公式都可以看出,某一位置上的dp数组元素,可由其上方左上方元素推出。

若从正上方推出,则证明该元素无变动,并未加入新的物品,而从左上方推出,则证明背包中加入了新的物品。

由此可以发现背包问题的一个特征,即本行元素都由前一行推导出,这里可以引出新的概念:滚动数组。但请先允许笔者买个关子,放在之后详解。

  1. dp数组如何初始化

    针对于本问题的初始化在刚刚的推导过程中已经完成了,此处再着重说明一下,由于遍历时元素的值由其左上方元素确定,故而需要初始化第一行以及第一列的元素。

    至于其他部分的元素,初始化任意值即可,这里默认初始化为0。

  2. 确定遍历顺序

    无论是先遍历背包重量亦或是先遍历物品都可以。

    这里放上先遍历物品再遍历背包的代码(先遍历背包只要两个for的顺序即可):

    // weight数组的大小 就是物品个数
    for(int i = 1; i < weight.size(); i++) { // 遍历物品
        for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
            if (j < weight[i]) dp[i][j] = dp[i - 1][j];
            else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
        }
    }
    

最后得出结果:

请添加图片描述

本场嘉宾:滚动数组

由刚刚推导的过程可以得知,当前层是由上一层推导而来的,既然如此,即可直接将上一层拷贝至当前层,从当前层开始计算。即可将二维的dp数组压缩成一维的dp数组,大大减少了空间复杂度。

但这也意味着在遍历时需要设置更为严苛的条件。

首先需要对递推公式作一定修改:

dp[j] = fmax(dp[j], dp[j - weight[i]] + value[i]);

初始化时,也需要注意:

若背包容量为0,能装的物品自然也是0,所以dp[0] = 0,这点毋庸置疑。但除0以外的下标该如何取值?可以选择参考递推公式,当物品的重量都大于零时,第一次遍历即可让每一位都选取到最大值,故而其余元素都初始化为0即可。

可以得到求解一维dp01背包问题的解:

for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
        dp[j] = fmax(dp[j], dp[j - weight[i]] + value[i]);
    }
}

需要注意的是,此处的内层循环是从后向前的倒序遍历,且仅能这样遍历,之所以需要从后向前更新状态,是因为我们需要使用到之前的状态。如果从前向后更新,原先的状态会被新状态覆盖掉,导致在后面需要使用时无法找到,从而出现计算错误。

Part 3.来一道紧张刺激的算法题

请添加图片描述

按照一般步骤这里应该要开始直接解题了,但我想先讲讲我是如何判断这道题归属于01背包问题的,或者说如何将这道题化解为一道01背包问题

这道题看似与背包问题相去甚远,给出一个集合,让我们将其分割成两个等和子集,如果想去生搬硬套,会发现虽然有物品(即nums数组中的元素),但背包的大小并不明确,物品的重量与价值也不清楚。

且先顺着题意思考一下,要使其能分割为两个等和数组,则分出的两个数组元素和都必然为数组总元素和的一半。如若想化解为背包问题,则是要同时填满两个背包,使得两个背包里面价值相等,设总的元素和等于sum,背包只要有其中一个重量能够达到sum/2,则可以判定这个数组可以分割成两个等和子集

则可以得出,背包的总体积为总元素和的一半,物品的重量与价值均为元素的值。
到这里,这道题实际上就已经被化解为一个01背包问题了,套公式解决即可:

1.确定dp数组以及下标的含义

本题中每一个元素的数值既是重量,也是价值

套到本题,dp[j]表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j]。

那么如果背包容量为target, dp[target]就是装满 背包之后的重量,所以 当 dp[target] == target 的时候,背包就装满了。

2.确定递推公式

01背包的递推公式为:

dp[j] = fmax(dp[j], dp[j - weight[i]] + value[i]);

本题中,物品i的重量是nums[i],其价值也是nums[i],故而将上面式子中的重量和价值替换成数值nums[i]即可。

所以递推公式:

dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);

3.dp数组如何初始化

显然,dp[0]为0,nums数组中的值都为正整数,所以其他值都初始化为0即可。

4.确定遍历顺序

在刚刚介绍滚动数组时已经说明:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历。

代码如下:

// 开始 01背包
for(int i = 0; i < nums.size(); i++) {
    for(int j = target; j >= nums[i]; j--) { // 每一个元素一定是不可重复放入,所以从大到小遍历
        dp[j] = fmax(dp[j], dp[j - nums[i]] + nums[i]);
    }
}

放上我的题解:

bool canPartition(int* nums, int numsSize){
    int sum = 0;
    for(int i = 0;i <= numsSize - 1;i++){
        sum += nums[i];
    }
    
    if(sum % 2 == 1){
        return false;
    }
    
    int* dp = (int*)malloc(sizeof(int) * (sum/2 + 1));
    memset(dp, 0, sizeof(int) * (sum/2 + 1));
    
    for(int i = 0; i <= numsSize - 1; i++) {//遍历每一个数字(物品)
        for(int j = sum/2; j >= nums[i]; j--) {//遍历元素和(背包重量)
            dp[j] = fmax(dp[j], dp[j - nums[i]] + nums[i]);
        }
    }
    return dp[sum/2] == sum/2;
}

Part 4.结语

背包问题是动态规划的经典问题,也是非常难以理解的问题,说老实话,直到书写至此,笔者也尚未完全理解01背包,只能算是理解个大概。动态规划问题大抵都是如此,需要不断通过学习理论和刷题实践来获取一种“经验”,让自己可以快速判断题目、理解题意、解决问题。

  • 31
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值