十五周算法训练营——背包问题

news2024/11/13 10:48:20

今天是十五周算法训练营的第十三周,主要讲背包问题专题。(欢迎加入十五周算法训练营,与小伙伴一起卷算法)

「背包问题:给你一个可装载重量为W的背包和N个物品,每个物品有重量和价值两个属性。其中第i个物品的重量为wt[i],价值为val[i],现在让你用这个背包装物品,最多能装的价值是多少?」

0-1背包动态规划思路

  1. 明确状态和选择

状态有两个:背包的容量和可选择的物品

选择就是:装进背包或者不装进背包

  1. dp数组的含义

刚才明确了状态,现在需要用dp数组把状态表达出来,刚才找到的「状态」,有两个,也就是说我们需要一个二维dp数组,一维表示可选择的物品,一维表示背包的容量。

dp[i][w]表示的就是对于[0……i]个物品,当前背包容量为w时的最大价值

  1. 根据选择,思考状态转移的逻辑

dp[i][w]表示:对于前i个物品,当前背包的容量为w时,这种情况下可以装下的最大价值是dp[i][w]。

如果你没有把这第i个物品装入背包,那么很显然,最大价值dp[i][w]应该等于dp[i-1][w]。你不装嘛,那就继承之前的结果。

如果你把这第i个物品装入了背包,那么dp[i][w]应该等于dp[i-1][w-wt[i-1]] + val[i-1]。

首先,由于i是从 1 开始的,所以对val和wt的取值是i-1。

  1. 明确base case: 此处的base case就是dp[ 0 ][ …… ]和dp[……][0]的时候,这个时候没有物品或者背包没有容量,此时价值为0

背包问题动态规划的结构

for 状态1 in 状态1的所有取值:
    for 状态2 in 状态2的所有取值:
        for ...
            dp[状态1][状态2][...] = 择优(选择1,选择2...)

0-1背包解题思路

/**
 * 0-1背包问题解题思路
 * 
 * 给你一个可装载重量为W的背包和N个物品,每个物品有重量和价值两个属性。其中第i个物品的重量为wt[i],价值为val[i],现在让你用这个背包装物品,最多能装的价值是多少?
 */

// 该问题是一个典型的动态规划问题
// 1. 明确状态和选择
// 状态就是背包的容量和可选的物品
// 选择就是要不要装该物品
// 2. dp数组含义
// dp[i][w]表示前i个物品、当前背包容量为w时,能装的最大价值
// 3. 状态转移逻辑
// 为了获取dp[i][w]的时候需要考虑当前要放入物品的重量是否可以放到背包中,即比较当前背包容量和要放入物品重量
// 若不能放进去:dp[i][w] = dp[i - 1][w]
// 若可以放进去,则此时就需要比较放入和不放入后的价值dp[i][w] = Math.max(dp[i - 1][w], dp[i - 1][w - wtPresent] + valPresent)

function knapsack(W, N, wt, val) {
    // 定义dp
    const dp = new Array(N + 1);
    for (let i = 0; i < dp.length; i++) {
        dp[i] = (new Array(W + 1)).fill(0);
    }

    // base case
    // 当在定义dp的时候已经进行了初始化为0,0就是起base case
    for (let i = 1; i < dp.length; i++) {
        for (let w = 1; w < dp[0].length; w++) {
            // 判断当前物品是否可以放到背包中,如果不能放进去
            if (w - wt[i - 1] < 0) {
                dp[i][w] = dp[i - 1][w];
            } else {
                // 如果可以放进去
                dp[i][w] = Math.max(dp[i - 1][w], dp[i - 1][w - wt[i - 1]] + val[i - 1]);
            }
        }
    }

    // 最终结果
    return dp[N][W];
}

分割等和子集

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:

输入:nums = [1,5,11,5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11] 。

// 对于经典背包问题,是给你一个可装载重量为W的背包和N个物品,每个物品有重量和价值两个属性。其中第i个物品的重量为wt[i],价值为val[i],现在让你用这个背包装物品,最多能装的价值是多少?

// 该问题其实是经典背包问题的变形,可以先对集合求和,得出sum,该问题就可以转换为背包问题:
// 给一个可装载重量为sum / 2的背包和N个物品,每个物品的重量为nums[i],现在让你装物品,是否存在一种装法,能够恰好将背包装满

// 1. 明确状态和选择
// 状态就是背包容量和可选择的物品
// 选择就是装进背包和不装进背包
// 2. dp数组函数
// dp[i][j] = x表示,对于前i个物品,当前背包的容量为j时,若x为true,则说明可以恰好将背包装满,若x为false,则说明不能恰好将背包装满。
// 3. 状态转移逻辑
// 若放不进去,dp[i][j] = dp[i - 1][j]
// 若能够放进去dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]]
// 4. base case
// base case就是:
// (1)有物品但是容量为0,肯定能装满,此时即dp[……][0] = true
// (2)没有物品但是有容量时,肯定装不满,此时即dp[0][……] = false

function canPartition(nums) {
    let sums = 0;
    nums.forEach(num => sums += num);

    // 如果是奇数,不能被划分,直接返回false
    if (sums % 2 === 1) {
        return false;
    }

    const numsLen = nums.length;

    const dp = new Array(numsLen + 1);
    for (let i = 0; i < dp.length; i++) {
        dp[i] = (new Array(sums / 2 + 1)).fill(false);
    }

    // base case
    for (let i = 0; i < dp.length; i++) {
        dp[i][0] = true;
    }

    for (let i = 1; i < dp.length; i++) {
        for (let j = 1; j < dp[0].length; j++) {
            if (j - nums[i - 1] < 0) {
                dp[i][j] = dp[i - 1][j];
            } else {
                dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
            }
        }
    }

    // 最终其dp[n][sum / 2]就是起结果,因为此时能找到和的一半,另一半也是和的一半
    return dp[numsLen][sums / 2];
}

零钱兑换II

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。

假设每一种面额的硬币有无限个。

题目数据保证结果符合 32 位带符号整数。

示例 1:

输入:amount = 5, coins = [1, 2, 5] 输出:4 解释:有四种方式可以凑成总金额: 5=5 5=2+2+1 5=2+1+1+1 5=1+1+1+1+1

// 该问题可转换为有一个背包,最大容量为amount,有一系列物品coins,每个物品的重量为coins[i],每个物品的数量无限。请问有多少种方法,能够把背包恰好装满?
// 与经典背包区别的是每个物品的数量是无限的,这也就是完全背包问题。

// 1. 状态和选择
// 状态:背包的容量和可选择的物品
// 选择:放进背包和不放进背包
// 2. 定义dp
// dp[i][j]:若只使用前i个物品,当背包容量为j时,有dp[i][j]种方法可以装满背包。换句话说:若只使用coins中的前i个硬币的面值,若想凑出金额j,有dp[i][j]中凑法
// 3. 状态转移逻辑
// 若不能放进去:dp[i][j] = dp[i - 1][j]
// 若能够放进去:dp[i][j] = dp[i - 1][j] + dp[i - 1][j - coins[i]]
// 4. base case
// (1)有硬币,但是目标结果为0,即dp[0……n][0] = 1
// (2)没有硬币,即dp[0][0……n] = 0

function change(amount, coins) {
    const n = coins.length;
    const dp = new Array(n + 1);
    for (let i = 0; i < dp.length; i++) {
        dp[i] = (new Array(amount + 1)).fill(0);
    }

    // base case
    for (let i = 0; i < dp.length; i++) {
        dp[i][0] = 1;
    }

    // 遍历dp
    for (let i = 1; i < dp.length; i++) {
        for (let j = 1; j < dp[0].length; j++) {
            // 如果当前硬币不能放进背包
            if (j < coins[i - 1]) {
                dp[i][j] = dp[i - 1][j];
            } else {
                // 能放进去,则结果就是放进去与不放进去的加和
                // 为什么当放进去的时候为i,因为此时已经决定使用coins[i - 1]的值
                dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]];
            }
        }
    }

    // 目标结果就是[N, amount]
    return dp[n][amount];
}

// 通过观察发现dp[i][j]之和dp[i][……]和dp[i - 1][……]相关
// 则可进行状态压缩
function change1(amount, coins) {
    // 定义dp
    const dp = (new Array(amount + 1)).fill(0);

    // base case
    dp[0] = 1;

    // 进行遍历
    for (let i = 0; i < coins.length; i++) {
        for (let j = 1; j < dp.length; j++) {
            if (j >= coins[i]) {
                dp[j] = dp[j] + dp[j - coins[i]];
            }
        }
    }

    return dp[dp.length - 1];
}

const amount = 5;
const coins = [1, 2, 5];

console.log(change1(amount, coins));

最后一块石头的重量II

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

如果 x == y,那么两块石头都会被完全粉碎; 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。 最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。

示例 1:

输入:stones = [2,7,4,1,8,1] 输出:1 解释: 组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1], 组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1], 组合 2 和 1,得到 1,所以数组转化为 [1,1,1], 组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

// 该问题和分割等和子集问题(416)处理方式类似,就是背包问题
// 1. 状态和选择
// 状态:背包和当前可选物品
// 选择:是否装进背包
// 2. dp数组含义
// dp[w][i]表示背包容量为w时,前i个物品,最多能够装的物品重量
// 3. 状态转移逻辑
// 不能装进去:dp[w][i] = dp[w][i - 1]
// 能够装进去:dp[w][i] = Math.max(dp[w][i - 1], dp[w - stones[i]][i - 1] + stones[i])
// 4. base case
// 当i = 0时,dp[0……w][0] = 0
// 当w = 0时,dp[0][……] = 0
function lastStoneWeightII(stones) {
    // 得到总重量
    let sum = 0;
    stones.forEach(stone => {
        sum += stone;
    });

    const weight = Math.floor(sum / 2);

    // 定义dp
    const dp = new Array(weight + 1);
    for (let i = 0; i < dp.length; i++) {
        dp[i] = (new Array(stones.length + 1)).fill(0);
    }

    // base case 在初始化时已经完成

    // 循环遍历
    for (let w = 1; w < dp.length; w++) {
        for (let i = 1; i < dp[0].length; i++) {
            // 判断是否可以装进去
            if (w - stones[i - 1] < 0) {
                dp[w][i] = dp[w][i - 1];
            } else {
                dp[w][i] = Math.max(dp[w][i - 1], dp[w - stones[i - 1]][i - 1] + stones[i - 1]);
            }
        }
    }

    return sum - 2 * dp[weight][stones.length];
}

const stones = [31,26,33,21,40];
console.log(lastStoneWeightII(stones));

目标和

给你一个整数数组 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

// 该方法可以用背包解决
// 如何转换为01背包问题呢?
// 假设加法的总和为x,那么减法对应的总和就是sum - x
// 所以我们要求的就是x - (sum - x) = S
// x = (S + sum) / 2
// 此时问题就转化为:装满容量为x背包,有几种方法

// 1. 状态和选择
// 2. dp数组含义
// dp[i][w]:前i个物品、背包容量为w时,有几种方式装满
// 3. 状态转移逻辑
// 装不进去:dp[i][w] = dp[i - 1][w]
// 能装进去:dp[i][w] = dp[i - 1][w - nums[i]] + dp[i - 1][w]
// 4. base case
// dp[0][0] = 1
function findTargetSumWays(nums, target) {
    // 求和
    const sum = nums.reduce((total, num) => total + num);

    // weight必须大于0且为整数
    if (target + sum < 0 || (target + sum) % 2 === 1) {
        return 0;
    }
    // 求weight
    const weight = (target + sum) / 2;

    // dp
    const dp = new Array(nums.length + 1);
    for (let i = 0; i < dp.length; i++) {
        dp[i] = (new Array(weight + 1)).fill(0);
    }

    // base case
    dp[0][0] = 1;

    // 循环
    for (let i = 1; i < dp.length; i++) {
        for (let w = 0; w < dp[i].length; w++) {
            // 不能装进去
            if (w - nums[i - 1] >= 0) {
                dp[i][w] = dp[i - 1][w] + dp[i - 1][w - nums[i - 1]];
            } else {
                dp[i][w] = dp[i - 1][w];
            }
        }
    }

    return dp[nums.length][weight];
}

// 用回溯算法实现一遍
function findTargetSumWays1(nums, target) {
    let result = 0;
    const backtrack = (index, sum) => {
        // 结束条件
        if (index === nums.length) {
            if (sum === target) {
                result++;
            }
            return;
        }

        backtrack(index + 1, sum + nums[index]);
        backtrack(index + 1, sum - nums[index]);
    };

    backtrack(0, 0);

    return result;
}

const nums = [0, 0, 0, 1];
const target = 1;

console.log(findTargetSumWays1(nums, target));

40ee00f54b4e2d321f5ae0daa54a763c.jpeg

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

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

相关文章

以加快工程和科学发展为使命,MathWorks 创新步履不止

一直以来&#xff0c;人类对于宇宙苍穹、高山峻岭的探索永不止步。今年是人类首次登月54周年&#xff0c;毋庸置疑&#xff0c;“登月计划”激发了全世界所有人的想象力。正由于这项计划&#xff0c;让耐高温的金属和合金防火面料、冻干食品及光伏电池、集成电路、计算机以及备…

解密Linux内核神器:内存屏障的秘密功效与应用方法

一、内存屏障简介 现在大多数现代计算机为了提高性能而采取乱序执行&#xff0c;这可能会导致程序运行不符合我们预期&#xff0c;内存屏障就是一类同步屏障指令&#xff0c;是CPU或者编译器在对内存随机访问的操作中的一个同步点&#xff0c;只有在此点之前的所有读写操作都执…

信号链噪声分析14

文章目录 概要整体架构流程技术名词解释技术细节小结 概要 提示&#xff1a;这里可以添加技术概要 虽然噪声的均方根值可通过 q/√12 计算精确近似值&#xff0c;但在某些条件下&#xff0c;频域成分可能 与交流输入信号高度相关。例如&#xff0c;低幅度周期性信号的相关度大…

智慧厕所系统:革新公厕管理的智能解决方案

公厕是城市基础设施的重要组成部分&#xff0c;但由于管理难度大、人员分散等问题&#xff0c;公厕的管理一直是一个难题。智慧公厕系统通过智能化的监控设备和传感器&#xff0c;实时监测公厕的使用情况和卫生状况&#xff0c;并将数据传输到中央控制系统。管理员可以通过该系…

C++学习笔记-第8单元 运算符重载

第8单元 运算符重载 文章目录 第8单元 运算符重载单元导读单元导读1&#xff1a;深度学习与C单元导读2&#xff1a;向量、矩阵、张量 8.1 运算符重载与平面向量类8.1.1 运算符与函数的关系8.1.2 平面向量类8.1.3 C运算符函数8.1.4 [C11]左值、纯右值、将亡值 8.2 重载二元运算符…

极致呈现系列之终章:Vue3中封装Echarts组件

经过前面一系列博文的介绍&#xff0c;我已经详细地讲解了Echarts中涉及的各种图表类型。所以&#xff0c;我认为极致呈现系列文章到此基本告一段落了。作为这个系列文章的终章&#xff0c;我想探讨最后一个问题&#xff1a;如何封装Echarts组件。我觉得这是一个很好的结尾&…

黑马程序员-从0到1学习Linux-第一章 初识Linux

视频学习地址&#xff1a;黑马程序员新版Linux零基础快速入门到精通&#xff0c;全涵盖linux系统知识、常用软件环境部署、Shell脚本、云平台实践、大数据集群项目实战等_哔哩哔哩_bilibili 目录 操作系统概述 初识Linux系统 虚拟机介绍 VMware WorkStation安装 在VMware上…

【Langchain】GPT的高效插件

功能1&#xff1a;让模型具有memory 可以看到&#xff0c;langchain作为访问gpt的一个插件&#xff0c;可以让gpt模型通过memory变量将之前的对话记录下来&#xff0c;从而使模型具有记忆&#xff08;在不改变模型参数的情况下&#xff09;。 查看memory变量包含了什么&#…

在blender中使用python程序化建模

blender中&#xff0c;所有可以在Blender软件中的手动操作&#xff0c;基本都可以通过Python API 完成 那么就可以用这个完成程序化生成 下面我给出一个简单的方块建模程序&#xff1a; 在scripting中&#xff0c;可以添加file&#xff0c;然后向场景中心放置一个正方体 首…

前端面试题Vue答案

1.vue的原理? image.png 关键词: 虚拟DOM树访问器属性 解释一下:响应式原理? 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项&#xff0c;Vue 将遍历此对象所有的 property&#xff0c;并使用 Object.defineProperty把这些 property 全部转为 getter/setter…

开关电源PFC电路原理详解及matlab仿真

PFC全称“Power Factor Correction”&#xff0c;意为“功率因数校正”。PFC电路即能对功率因数进行校正&#xff0c;或者说能提高功率因数的电路。 在电学中&#xff0c;功率因数PF指有功功率P&#xff08;单位w&#xff09;与视在功率S&#xff08;单位VA&#xff09;的比值。…

【Linux】fdisk命令参数详解(图文超详细,内容来自官方文档)

目录 0.环境 1.背景 2.内容--官方文档对fdisk的介绍 1&#xff09;名称 2&#xff09;说明 3&#xff09;具体参数/选项 4&#xff09;举个栗子&#xff0c;我要查fdisk的版本 0.环境 windows linux虚拟机 1.背景 之前发表了一篇文章Linux 用fdisk进行磁盘分区&#xff…

Python语法基础03(输入与while)

用户输入 使用input()函数可以获取输入&#xff0c;同时应说清楚所期待的输入内容 #使用函数input时&#xff0c;要说清楚期望的输入 carinput("Please input your favourite car.\nAnd i will repeat:") print(f"your favourite car is {car}")## 也可…

多元分类预测 | Matlab灰狼算法(GWO)优化混合核极限学习机(HKELM)分类预测,多特征输入模型,GWO-HKELM分类预测

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 多元分类预测 | Matlab灰狼算法(GWO)优化混合核极限学习机(HKELM)分类预测,多特征输入模型,GWO-HKELM分类预测 多特征输入单输出的二分类及多分类模型。程序内注释详细,直接替换数据就可以用。程序语言为matlab…

NFTScan 与 Sender Wallet 达成合作伙伴,双方在多链 NFT 数据方面展开合作!

近日&#xff0c;NFT 数据基础设施 NFTScan 与 Web3 钱包 Sender Wallet 达成合作伙伴关系&#xff0c;成为其官方 NFT 数据供应商。NFTScan 将为 Sender Wallet 的 NFT 部分提供专业的多链 NFT 数据支持&#xff0c;确保用户可以跨多个区块链获得全面和实时的 NFT 数据。 Sen…

Latex 文献引用

来源&#xff1a; 引用 - 为什么叫 citet 和 citep&#xff1f;- TeX - LaTeX 堆栈交换 (stackexchange.com) 来源&#xff1a; latex \cite, \citet, \citep_latex citet_juliosun的博客-CSDN博客 来源&#xff1a;https://www.reddit.com/r/LaTeX/comments/5g9kn1/whats_th…

【网络安全带你练爬虫-100练】第1练:发送请求获取返回内容

目录 一、前置知识1 二、前置知识2 requests库的7个主要方法 语法&#xff1a; 注解&#xff1a; 三、扩展工具 四、网络安全小圈子 一、前置知识1 顾名思义网络爬虫第一步&#xff0c;爬取目标 URL的网页的信息 可以使用 urllib.request 和 requests发送请求&#xff0…

瞄准光储赛道的家电巨头,是没活“硬整”,还是有理有据?

2023年上半年即将结束&#xff0c;家电巨头再次带来跨界的重磅消息。 在A股公司合康新能月底发布的定增公告中&#xff0c;美的集团&#xff0c;成为合康新能定增的包揽者。后者将获得前者提供的高达14.73亿元的募集资金总额&#xff0c;用于电子设备业务能力提升项目、光伏产…

一部手机如何登录多个微信?教你一招轻松搞定

现在大部分人都不止2个微信&#xff0c;有的用于私人社交&#xff0c;有的用于工作&#xff0c;人手多个微信已经很普遍了。那么如何在一个手机上同时登录2个甚至更多微信呢&#xff1f; 01登录2个微信 找到手机上面的【设置】&#xff0c;找到【应用设置】-【应用双开】&am…

Redis通信协议

RESP协议 Redis是一个CS架构的软件&#xff0c;通信一般分两步&#xff08;不包括pipeline和PubSub&#xff09;&#xff1a; ① 客户端&#xff08;client&#xff09;向服务端&#xff08;server&#xff09;发送一条命令 ② 服务端解析并执行命令&#xff0c;返回响应结果…