首页 > 其他分享 >动态规划-背包01问题推理与实践

动态规划-背包01问题推理与实践

时间:2024-11-10 15:20:00浏览次数:4  
标签:01 sizes int sum nums storage 背包 推理 dp

动态规划-背包01问题推理与实践

背包01问题描述:

有storage大小的背包和weights.size()数量的物品,每个物品i对应的物品大小为sizes[i],价值为values[i],在不超过storage大小的情况下,如何装载物品使背包中的values和最大.


物品大小: vector<int> sizes;

物品价值: vector<int> values;

背包容量: <int> storage;


理解(状态转移公式的推理):

构建二维数组dp[i][j],定义式为 std::vector<std::vector<int>> dp(sizes.size(),std::vector<int>(storage + 1,0));默认值为0;

dp[i][j] 的意义是在前 i 个物品中选择任意少于等于 i 个物品,其总大小不超过 j 的最大价值和.

则同理 dp[i - 1][j] 为在前 i - 1 物品中选择任意少于等于 i - 1 个物品,其总大小不超过 j - 1的最大价值和,此即为子状态.

假设求取dp[i][j],则考虑两种情况:

①第i个物品取的可能

②第i个物品不取的可能

针对上述情况进行分类讨论:

①已知dp[i-1][j],此时背包已满,则应腾出某物品以装在第 i 物品,则装载前必须知道dp[i - 1][j - sizes[i]]的值,即背包扣除第i个物品的大小后的最大价值装载方式[1],则可推导出dp[i][j] = dp[i - 1][j - sizes[i]] + values[i];

②已知dp[i - 1][j],此时第 i 个物品的价值过低或大小过大,不适合替换背包中的物品,则推理出dp[i][j] = dp[i - 1][j];

因为无法知道上述两种情况何时会发生,应该取两者的较大值:

dp[i][j] = std::max(dp[i - 1][j - sizes[i]] + values[i],dp[i - 1][j]);

此即为背包01问题的状态转移公式.

代码实现dp数组如下[2]:

//初始化第一个物品放入背包的子状态,即dp[0][j],此处的j代表的是>=第一个物品的大小的位置处,都填充values[0],注意子数组的额存放索引为增长的storage.
for(int j = sizes[0]; j <= storage; j++) {
    dp[0][j] = values[0];
}

//构建dp数组
for(int i = 1; i < sizes.size(); i++) {
    for(int j = 0; j <= storage; j++) {
        //该装载的物品过大,即使只装它一个也装不下,直接返回不装载的情况即可
        if(j < sizes[i]) dp[i][j] = dp[i - 1][j];
        else dp[i][j] = std::max(
        			dp[i - 1][j - sizes[i]] + values[i],
            		dp[i - 1][j]
            );
    }
}

完整代码(对应2. 01背包问题 - AcWing题库):

#include <iostream>
#include <vector>
#include <algorithm>

int main(int argc,char** argv) {
    std::vector<int> sizes;
    std::vector<int> values;
    int storage;
    int size;

    std::cin >> size >> storage;
    for(int i = 0; i < size; i++) {
        int t_size;
        int t_value;
        std::cin >> t_size >> t_value;
        sizes.push_back(t_size);
        values.push_back(t_value);
    }

    std::vector<std::vector<int>> dp(sizes.size(),std::vector<int>(storage + 1,0));

    for(int j = sizes[0]; j <= storage; j++) {
        dp[0][j] = values[0];
    }

    for(int i = 1; i < sizes.size(); i++) {
        for(int j = 0; j <= storage; j++) {
            if(j < sizes[i]) dp[i][j] = dp[i - 1][j];
            else dp[i][j] = std::max(
                        dp[i - 1][j - sizes[i]] + values[i],
                        dp[i - 1][j]
                );
        }
    }

    //Debug查看部分
    // for(int i = 0; i < size; i++) {
    //     for(int j = 0; j <= storage; j++) {
    //         std::cout << "[" << i << "]" << "[" << j << "]" << " = " << dp[i][j] << std::endl;
    //     }
    // }

    std::cout << dp[size - 1][storage];

    return 0;
}

一维数组简化理解:

构建dp[j]一维数组,定义式为 std::vector<std::vector<int>> dp(storage + 1,0);

dp[j]数组意义为背包容量为j时,背包的最大价值为dp[j];

一维数组的遍历代码:

for(int i = 0; i < sizes.size(); i++) {
    for(int j = storage; j >= sizes[i]; j--) {
        dp[j] = std::max(
        	dp[j - sizes[i]] + values[i],
            dp[j]
        );
    }
}

这里复用dp[j]的推理是,外层循环可以理解为依次遍历当次放入的物品 i 与放入该物品时容量为 j 的最大价值计算,即dp[j]为复用上一层循环 i = i - 1, j = j 时的最大价值,所变化的是考虑了新加入的 i 物品;内层循环反向遍历的理由因此可以推理得出,若正向进行遍历时,外层的i物品会被内层多次放入背包中(多次被遍历到),而反向遍历则不会重复遍历到,即举例以下情况:

// i:(in) 1 2 3...sizes.size() - 1 当 i = 2时
// j:(in) size[i] size[i] + 1 size[i] + 2...storage 当 j = x - 1(实际值在此例中不重要,合理即可)时
dp[j] = dp[j - sizes[i]] + values[i] == dp[x - 1] = dp[x - 1 - sizes[2]] + values[2];
//当 i = 2时
//当 j = x时
dp[j] = dp[j - sizes[i]] + values[i] == dp[x] = dp[x - sizes[2]] + values[2]; 
//观察可得: 在j:容量增长的情况下重复考虑了 i = 2 的情况,即重复加入了背包
//此时我们反向考虑
//i:(in) 与上文相同, 当i = 2时
//j:(in) storage... size[i] 当 j = x + 1(同上)时
dp[j] == dp[x + 1] = dp[x + 1 - size[2]] + values[2];
//当 i = 2时
//当 j = x时
dp[j] == dp[x] = dp[x - sizes[2]] + values[2];
//观察可得: 在j:容量递减的情况下,dp[x + 1] 依赖于上一次外层循环的dp[x + 1 - sizes[2]]值,而与此次循环的dp[x]值无关,故不会出现重复加入的情况

一维数组的初始化方式:

一维数组dp[j]不需要像二维数组dp[i][j]那样繁琐的初始化

仅仅只需要设置整体默认值(一般为0)即可,可参考定义式[3]

完整代码(对应416. 分割等和子集(LeetCode)):

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

例题理解(对应494. 目标和(LeetCode)):

给你一个非负整数数组 nums 和一个整数 target

向数组中的每个整数前添加 '+''-' ,然后串联起所有整数,可以构造一个 表达式

  • 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1"

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

示例 2:

输入:nums = [1], target = 1
输出:1

提示:

  • 1 <= nums.length <= 20
  • 0 <= nums[i] <= 1000
  • 0 <= sum(nums[i]) <= 1000
  • -1000 <= target <= 1000

解题思路:

将nums数组的选择抽取划分为题干对应的两种状态,"+" 和 "-";我们称 "+" 的成员集成为left数组,"-" 的成员集成为right数组;

显然可见:

① int sum{0}; for: sum += nums.elems; sum = sum(left) + sum(right); sum => const	//对nums数组成员求和,其值为定值,且可转换为left和 right的成员和
② sum(left) - sum(right) = target; target => const	//满足题目要求的left和right,其相减值为定值

①和②可推理出

target + sum = sum(left) * 2 => sum(left) = (target + sum) / 2; //依此可得,sum(left)也为定值

推导出,当我们满足sum(left)为 (target + sum) / 2时,便可满足题干要求,抽象为当我们装满容量为sum(left)的背包时,dp[left]所存储的值即为解

确定背包定义式: std::vector<int> dp (storage + 1,0); storage = (sum + target) / 2;

确定背包状态转移公式: dp[j] = dp[j] + dp[j - nums[i]] + nums[i] == dp[j] += dp[j - nums[i]] + nums[i];解释为在背包容量为j时,当前dp[j]的值为装入选择该i物品加入left数组和不加入left数组后的满足次数;

该题目与许多其他背包问题的思维大致相同,都是将题目问题转换为可理解的背包01问题.

完整代码:

class Solution {
public:
    int findTargetSumWays(std::vector<int>& nums, int target) {
        int sum{0};
        for(int elem : nums) {
            sum += elem;
        }

        if((target + sum) % 2 != 0 || abs(target) > sum) return 0;

        int storage = (sum + target) / 2;



        std::vector<int> dp (storage + 1,0);
        dp[0] = 1;

        for(int i = 0; i < nums.size(); i++) {
            for(int j = storage; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }

        return dp[storage];
    }
};

文章参考于: 代码随想录,CSDN,CNBlog等各优秀编辑者,开发者的优质文章作品.

                                                                                                --2024.11.10 Neko 总结

  1. 这里是在推理在storage为 j - sizes[i] 的情况下的最大价值(dp[i - 1][j - sizes[i]),从而求得装载第i个物品后总sizes <= storage的最大价值,因为dp[i - 1][ j -sizes[i]]为子状态,其已经定义且为已知结果,故能推导出选择第i个物品时的最大价值 ↩︎

  2. 这里的dp[i][j] 初始化代码为 (sizes.size(),std::vector<int>(storage + 1,0)); 此处为兼容语言数组特性,将对其自然数字记录方式;第i个物品的大小在sizes[i - 1]处存放,而重量则符合自然数字规律(0-storage) ↩︎

  3. C++的此定义式本身也初始化了所有元素(为0),语法请自行查略 ↩︎

标签:01,sizes,int,sum,nums,storage,背包,推理,dp
From: https://www.cnblogs.com/NekoBlog/p/18537978

相关文章

  • [CISCN2019 华北赛区 Day2 Web1]Hack World 1
    [CISCN2019华北赛区Day2Web1]HackWorld1打开实例发现是个POST注入框盲猜SQL注入,万能密码检测无果,而且经过测试存在大量sql关键字过滤尝试使用(),出现了bool(false),确定这是一道布尔注入题and被禁用,决定采用异或^注入构建payload脚本梭哈:成功获得flag:flag{a2f7089......
  • 代码随想录算法训练营第19天|235. 二叉搜索树的最近公共祖先 ,701.二叉搜索树中的插入
    235.二叉搜索树的最近公共祖先文章链接:https://programmercarl.com/0235.二叉搜索树的最近公共祖先.html题目链接:https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-search-tree/思路:利用二叉搜索树的特性,当第一次遇到在[p,q]区间或[q,p]区间的元素的节点,则......
  • 代码随想录算法训练营第18天| 530.二叉搜索树的最小绝对差, 501.二叉搜索树中的众数 , 2
    530.二叉搜索树的最小绝对差文章链接:https://programmercarl.com/0530.二叉搜索树的最小绝对差.html视频链接:https://www.bilibili.com/video/BV1DD4y11779/?vd_source=6cb513d59bf1f73f86d4225e9803d47b题目链接:https://leetcode.cn/problems/minimum-absolute-difference-in......
  • 使用 PyTorch 实现并测试 AlexNet 模型,并使用 TensorRT 进行推理加速
    本篇文章详细介绍了如何使用PyTorch实现经典卷积神经网络AlexNet,并利用Fashion-MNIST数据集进行训练与测试。在训练完成后,通过TensorRT进行推理加速,以提升模型的推理效率。本文全部代码链接:全部代码下载环境配置为了保证代码在GPU环境下顺利运行,我们将安装兼容......
  • [GWCTF 2019]babyRSA
    fromCrypto.Util.numberimport*fromgmpy2import*fromsympyimport*p=797862863902421984951231350430312260517773269684958456342860983236184129602390919026048496119757187702076499551310794177917920137646835888862706126924088411570997141257159563952......
  • [lnsyoj1801/luoguP2051/AHOI2009] 中国象棋
    题意在\(n\timesm\)大小的棋盘上放无标号棋子,使得任何一行或一列都不多于\(2\)个棋子,求方案数sol计数题,优先考虑dp。由于每行每列棋子不多于两个,所以我们可以计\(f_{i,j,k}\)表示前\(i\)行中,\(j\)列恰好\(1\)个棋子,\(k\)列恰好\(2\)个棋子的方案数。状态转......
  • LeetCode 3014[输入单词需要的最少按键次数I]
    题目链接LeetCode3014[输入单词需要的最少按键次数I]详情实例实例1实例2提示题解思路一圈下来8个字母,每个字母按1次二圈下来16个字母,前8个字母每个按1次,后8个字母,每个按2次三圈下来24个字母,前8个字母每个按1次,中间8个字母,每个按2次,最后8个字母,每个按3次四圈下来......
  • VMware ESXi 6.7 U3u (ESXi670-202403001) 下载
    VMwareESXi6.7U3u(ESXi670-202403001)下载VMwareESXi6ExtendSupportRelease请访问原文链接:https://sysin.org/blog/vmware-esxi-6/查看最新版。原创作品,转载请保留出处。作者主页:sysin.org产品简介VMwareESXi:专门构建的裸机Hypervisor了解可直接安装到您的物......
  • 25-018、基于STM32单片机智能行李箱设计-LED-BELL-KEY-指纹-LCD1602-GSM-GPS+HX711称
    本设计由STM32F103C8T6单片机核心板电路+LED指示灯电路+蜂鸣器报警电路+按键电路+指纹电路+LCD1602液晶显示电路+GSM模块电路+GPS模块电路组成。1、如果指纹错误。LED灯会闪,同时蜂鸣器发出滴滴声(3声即可)2、如果指纹输入三次失败后,禁止再用指纹解锁,如果指纹打不开,可以输入按键......
  • 【漏洞复现】通达OA 2013、2016、2017 header.inc.php 任意用户登陆漏洞
    免责声明:        本文旨在提供有关特定漏洞的信息,以帮助用户了解潜在风险。发布此信息旨在促进网络安全意识和技术进步,并非出于恶意。读者应理解,利用本文提到的漏洞或进行相关测试可能违反法律或服务协议。未经授权访问系统、网络或应用程序可能导致法律责任或严......