【算法】01背包和完全背包

news2024/11/17 15:42:55

文章目录

  • 背包问题概览
  • 01背包
    • 二维dp数组写法
    • 一维dp数组写法
  • 完全背包
  • 关于遍历顺序
  • 相关题目
    • [416. 分割等和子集](https://leetcode.cn/problems/partition-equal-subset-sum/)
    • [279. 完全平方数](https://leetcode.cn/problems/perfect-squares/)
    • [518. 零钱兑换 II](https://leetcode.cn/problems/coin-change-ii/)
  • 总结

本文主要用于通过两个例题记录 01背包完全背包 问题的模板。

背包问题概览

直接先去看他的:动态规划:01背包理论基础
在这里插入图片描述
在这里插入图片描述

01背包

494. 目标和
在这里插入图片描述
这道题目等价于:选一部分数字,前面是正号,剩下一部分数字前面是负号。
我们记这两部分数字集合的和分别是 a 和 b ,那么有:
a + b = t o t a l S u m a + b = totalSum a+b=totalSum a − b = t a r g e t a - b = target ab=target ,得到 a = ( t o t a l S u m + t a r g e t ) / 2 a = (totalSum + target) / 2 a=(totalSum+target)/2
除此之外,还有两个额外条件,1 是 (totalSum + target) 需要是偶数,2 是 totalSum 至少也要达到 target 的绝对值(也就是数字集足够组成 target)。

在这里插入图片描述
这样这道题目就可以转变成 : 恰好装 capacity,求方案数

二维dp数组写法

不熟练的可以先从 二维数组 出发。

  1. 确定 dp 数组以及下标的含义
    定义 dp 数组 dp[][] ,其中 dp[i][j] 表示从下标为[0-i]的物品里任意取,放进恰好容量为j的背包,方案数是多少
  2. 确定递推公式
    对于每个物品,无非是放和不放两种方式。
    不放对应着 f[i + 1][c] = f[i][c] (其实不是不放,是放不了)
    放对应着 f[i + 1][c] = f[i][c] + f[i][c - x] (f[i][c]是不放的方案数,f[i][c-x]是放i+1的方案数,需要加起来)
  3. dp 数组初始化
    dp[][0] = 1; // 也就是只有都不选,价值和才是 0 .
  4. 确定遍历顺序
    这里无论是先遍历物品还是先遍历背包都是可以的,唯一需要注意的是在遍历物品时的顺序需要从前往后。(因为递推公式中 dp[i][ ] 依赖 dp[i - 1][])
  5. 举例推导 dp 数组
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int totalSum = Arrays.stream(nums).sum(), n = nums.length;
        if ((totalSum + target) % 2 != 0 || totalSum < Math.abs(target)) return 0;
        target = (totalSum + target) / 2;
        int[][] dp = new int[n + 1][target + 1];
        dp[0][0] = 1;   // 0个数字的时候,有1种可能得到0价值
        for (int i = 1; i <= n; ++i) {  // 遍历1个数字到n个数字
            int num = nums[i - 1];
            for (int j = 0; j <= target; ++j) {
                dp[i][j] = dp[i - 1][j];
                if (j >= num) dp[i][j] += dp[i - 1][j - num];
            }
        }
        return dp[n][target];
    }
}

一维dp数组写法

一维 dp 数组的写法和二维 dp 数组的写法很不一样,因为背包容量一定要倒序遍历!,所以必须先遍历物品嵌套遍历背包容量

其中背包容量一定要倒序遍历的原因是:本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖

(即必须先遍历物品嵌套遍历背包,且物品正序遍历,背包倒序遍历)

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int totalSum = Arrays.stream(nums).sum(), n = nums.length;
        if ((totalSum + target) % 2 != 0 || totalSum < Math.abs(target)) return 0;
        target = (totalSum + target) / 2;
        int[] dp = new int[target + 1];
        dp[0] = 1;
        for (int i = 0; i < n; ++i) {	// 遍历物品
            for (int j = target; j >= nums[i]; --j) {	// 遍历背包
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[target];
    }
}

Q:为什么可以从二维数组变成一维数组?
A:因为 无后效性 。也就是这一行的二维数组的取值,只和紧挨着的上一层数组有关。

完全背包

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。

322. 零钱兑换
在这里插入图片描述

class Solution {
    public int coinChange(int[] coins, int amount) {
        int n = coins.length;
        int[] dp = new int[amount + 1];
        Arrays.fill(dp, amount + 1);
        dp[0] = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = coins[i]; j <= amount; ++j) {
                dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
            }
        }
        return dp[amount] == amount + 1? -1: dp[amount];
    }
}

在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!

因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。 (即物品和背包的遍历都是正序遍历即可)。(作者个人认为还应该加上:物品for循环嵌套背包容量for循环,解释详见最后的总结章节)

关于遍历顺序

在这里插入图片描述
如果 dp[i + 1][j] 的状态 与 (dp[i][j - c] 或 dp[i + 1][j + c])有关,就是倒序遍历。(来自左上右下)
如果 dp[i + 1][j] 的状态 与 (dp[i + 1][j - c] 或 dp[i][j + c])有关,就是正序遍历。(来自左下右上)

或者这样理解:我不能先覆盖 我之后还会使用到的地方


相关题目

416. 分割等和子集

在这里插入图片描述
实际上就是判断能否取一定数量的数字,使其总和也是全部数字总和的一半。(类似于 目标和 那道题目,只不过这道题目的目标和是 0)。

判断为 01 背包。

写法一:

class Solution {
    public boolean canPartition(int[] nums) {
        int n = nums.length, totalSum = Arrays.stream(nums).sum(), target = totalSum / 2;
        if (totalSum % 2 == 1) return false;
        boolean[] dp = new boolean[target + 1];
        dp[0] = true;
        for (int i = 0; i < n; ++i) {
            for (int j = target; j >= nums[i]; --j) {
                dp[j] |= dp[j - nums[i]];
            }
        }
        return dp[target];
    }
}

写法二:

class Solution {
    public boolean canPartition(int[] nums) {
        int n = nums.length, totalSum = Arrays.stream(nums).sum(), halfSum = totalSum / 2;
        if (totalSum % 2 == 1) return false;
        int[] dp = new int[halfSum + 1];
        for (int i = 0; i < n; ++i) {
            for (int j = halfSum; j >= nums[i]; --j) {
                dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }
        return dp[halfSum] == halfSum;
    }
}

279. 完全平方数

在这里插入图片描述

每个平方数都可以重复选择,所以是完全背包问题。

class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n + 1];
        Arrays.fill(dp, n + 1);     // 因为求最小值,所以都先设成最大值
        dp[0] = 0;                  // dp数组初始化
        for (int i = 1; i * i <= n; ++i) {      // 遍历不同的物品
            for (int j = i * i; j <= n; ++j) {  // 完全背包问题需要:正向遍历背包
                dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
            }
        }
        return dp[n];
    }
}

518. 零钱兑换 II

在这里插入图片描述

和 零钱兑换 相同,都是 完全背包问题。

但这道题目求的是方案数量,所以状态转移方程不同,其他部分均大致相同。

class Solution {
    public int change(int amount, int[] coins) {
        int n = coins.length;
        int[] dp = new int[amount + 1];
        dp[0] = 1;      // 总金额为0的方案数是1
        for (int i = 0; i < n; ++i) {
            for (int j = coins[i]; j <= amount; ++j) {
                dp[j] += dp[j - coins[i]];  // 求方案数,所以是 dp[j] += dp[j - coins[i]];
            }
        }
        return dp[amount];
    }
}

总结

其实 01背包 和 完全背包 问题,最重要的就是两个问题:

  1. 把原问题转换成背包问题。
  2. 背包问题的模板。

关于模板,作者推荐都只记住 一维 dp 数组的方法。

对于 01背包:必须先遍历物品嵌套遍历背包,且物品正序遍历,背包倒序遍历
对于 完全背包:必须先遍历物品嵌套遍历背包,且物品正序遍历,背包倒序遍历


备注:
有一种说法是:
在这里插入图片描述
来自https://programmercarl.com/%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80%E5%AE%8C%E5%85%A8%E8%83%8C%E5%8C%85.html(代码随想录)
但实际上在 518. 零钱兑换 II 这道题目中,如果调换 两个 for 循环的嵌套顺序,结果会出现问题!

因此建议大家都 先遍历物品嵌套遍历背包

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/707249.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【webrtc】vs2017 重新构建m98

配置了一台13900k的主机,需要重新配置webrtc 构建环境代码已经gclient sync 同步好了,打算重新构建:vs2017 的win10 sdk最大17763 vs2017 环境 set vs2017_install=S:\Program Files (x86)\Microsoft Visual Studio\2017\Communitywin10 SD

Python_上下文管理器

目录 上下文管理器类 多上下文管理器 contextmanager实现上下文管理器 上下文管理器(context manager)是 Python 编程中的重要概念&#xff0c;用于规定某个对象的使用范围。一旦进入或者离开该使用范围&#xff0c;会有特殊操作被调用 (比如为对象分配或者释放内存)。它的语…

【Kubernetes资源篇】ConfigMap配置管理中心详解

文章目录 一、ConfigMap配置中心理论知识1、ConfigMap配置中心简介2、ConfigMap局限性 二、创建ConfigMap的四种方式1、第一种&#xff1a;通过命令行创建ConfigMap2、第二种&#xff1a;通过指定文件创建ConfigMap3、第三种&#xff1a;通过指定目录创建ConfigMap4、第四种&am…

C++线程池(1)理论基础及简单实现

写过CURD的程序员肯定都熟悉“数据库连接池”这个东西&#xff0c;数据库连接是个昂贵的东西&#xff0c;因为它的创建过程比较耗费资源。所以为了节约资源、提高数据库操作的性能&#xff0c;“数据库连接池”就应运而生了。 其实线程池跟数据库连接池类似&#xff0c;一个线…

《PyTorch深度学习实践》第七讲 处理多维特征的输入

b站刘二大人《PyTorch深度学习实践》课程第七讲处理多维特征的输入笔记与代码&#xff1a;https://www.bilibili.com/video/BV1Y7411d7Ys?p7&vd_sourceb17f113d28933824d753a0915d5e3a90 Diabetes Dataset 每一行是一个记录每一列是一个特征&#xff0c;每个样本有8个特征…

为什么我们家里的IP都是192.168开头的?

为什么我们家里的IP都是192.168开头的&#xff1f; 本文为掘金社区首发签约文章&#xff0c;14天内禁止转载&#xff0c;14天后未获授权禁止转载&#xff0c;侵权必究&#xff01; 是的&#xff0c;还是我小白&#xff0c;什么技术博主&#xff0c;老情感博主了。 来讲个故事。…

网络安全合规-数据安全分类分级

数据安全是指保护数据免受未经授权的访问、使用、泄露、破坏或篡改的措施。数据安全包括物理安全、网络安全、应用程序安全、数据备份和恢复等方面。 数据分级分类是指根据数据的重要性和敏感程度&#xff0c;将数据划分为不同的级别&#xff0c;并根据不同级别的数据制定不同…

enote笔记法之附录1——“语法词”(即“关联词”)(ver0.23)

enote笔记法之附录1——“语法词”&#xff08;即“关联词”&#xff09;&#xff08;ver0.23&#xff09; 最上面的是截屏的完整版&#xff0c;分割线下面的是纯文字版本&#xff1a; 作者姓名&#xff08;本人的真实姓名&#xff09;&#xff1a;胡佳吉 居住地&#xff1…

前言-----

因要参加电赛&#xff0c;接触到STC89C52RC&#xff08;A51&#xff09;单片机 STC89C52RC引脚功能 1电源: ①VCC - 芯片电源&#xff0c;接5V&#xff1b; ②VSS - 接地端&#xff1b; 2.时钟: XTAL1、XTAL2 - 晶体振荡电路反相输入端和输出端。 3.控制线: 控制线共…

Java 17官方编程手册都针对哪些方面做了更新?

Java 17&#xff0c;官方编程手册&#xff0c; 《International Developer》杂志称为“全世界醉著名的编程书籍创作者之一”的Herbert Schildt倾情解读 《Java官方编程手册》从1996年首次出版以来&#xff0c;已经经历了数次改版&#xff0c;每次改版都反映 了Java不断演化的进…

分享解析,2+1链动模式为何能在市场上经久不衰

​小编介绍&#xff1a;10年专注商业模式设计及软件开发&#xff0c;擅长企业生态商业模式&#xff0c;商业零售会员增长裂变模式策划、商业闭环模式设计及方案落地&#xff1b;扶持10余个电商平台做到营收过千万&#xff0c;数百个平台达到百万会员&#xff0c;欢迎咨询。 随…

服务网格:Istio 架构

什么是服务网格 服务网格(Service Mesh)这个术语通常用于描述构成这些应用程序的微服务网络以及应用之间的交互。随着规模和复杂性的增长&#xff0c;服务网格越来越难以理解和管理。 它的需求包括服务发现、负载均衡、故障恢复、指标收集和监控以及通常更加复杂的运维需求&am…

数据结构--双端队列

数据结构–双端队列 双端队列&#xff08;Double-ended Queue&#xff0c;简称Deque&#xff09;是一种具有队列和栈特性的数据结构&#xff0c;可以在队列的两端进行插入和删除操作。双端队列允许从前端和后端同时进行插入和删除操作&#xff0c;因此可以称为“两端都可以进出…

「STC8A8K64D4开发板」第2-6讲:串口通信

第2-6讲&#xff1a;串口通信 学习目的掌握USB转串口电路的原理和设计。学习STC8A8K64D4的串口通信&#xff0c;包括串口初始化、波特率计算、串口发送和接收。编写串口收发程序&#xff0c;尤其是串口接收的软件缓存处理。编写串口发送命令控制LED指示灯亮灭的程序。 硬件电路…

【电商API接口系列】店铺所有商品数据的采集

API接口允许不同应用程序之间共享数据&#xff0c;在系统之间传输、读取和更新数据。例如&#xff0c;一个电商网站可以通过API接口获取支付系统的支付状态。API接口允许开发人员使用他人开发的功能来扩展自己的应用程序。通过调用第三方API接口&#xff0c;开发人员无需重新实…

二进制部署Kubernetes

二进制部署Kubernetes v1.20 k8s集群master01&#xff1a;192.168.142.10 kube-apiserver kube-controller-manager kube-scheduler etcd k8s集群master02&#xff1a;192.168.142.20 k8s集群node01&#xff1a;192.168.142.30 kubelet kube-proxy docker k8s集群node…

基于Java汽车售票网站设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

VUE_网页自定义右键菜单组件

可以在uni-app或vue脚手架项目使用 引入组件会接管页面右键事件&#xff0c;所有options为空数组时&#xff0c;在页面右键将没有反应 rightMenu.vue <template><view><view v-if"show" class"contextMenu" :style"lay_style"…

Kafka:Kafka资料整理

一、官网 二、博主文章 1、kafka是什么 • Worktile社区 三、源码解读

一文了解云计算

目录 &#x1f34e;云服务 &#x1f34e;云计算类型 &#x1f352;公有云 &#x1f352;私有云 &#x1f352;混合云 &#x1f34e;云计算服务模式 &#x1f352;IaaS基础设施即服务 &#x1f352;PaaS平台即服务 &#x1f352;SaaS软件即服务 &#x1f352;三者之间区别 &…