背包问题学习笔记-01背包

2023-09-17 11:21:57

背景

背包问题是动态规划问题中的一个大类,学习背包问题对于掌握动态规划十分重要。背包问题也很容易成为程序员算法面试中的一个槛,但其实背包问题已经被研究,讲解的比较成熟了,在这些丰富的讲解资料的基础之上,大家理解背包问题的难度也被大大减弱了。

acwing背包问题列表

本篇笔记主要参考了 AcWing 上的题目列表以及讲解视频,原因有二:1)上面截图中相关的问题都是免费的,不需要会员。2)AcWing 作者的讲解较为细致,适合新手学习。

题意描述:

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i  件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式
输出一个整数,表示最大价值。

数据范围
0<N,V≤1000

0<vi,wi≤1000

示例:

4 5
1 2
2 4
3 4
4 5

8


解题思路:

Alice: 01 背包有思路吗 ?
Bob: 我记得这个是动态规划,好像还是二维动态规划 ?
Alice: 状态转移方程能找到吗 ?
Bob: 找状态转移方程之前得先明确要记录的状态是啥,dp[i][j] 是什么意思吧 ?
Alice: 说的对,dp[i][j] 的值一般的话,应该就是要求解的值吧,就是最大价值。
Bob: 那 i 和 j 呢 ?还剩下的变量是什么 ?物品的体积,物品的 ID ? 背包的体积 ?
Alice: 背包的体积是个常量,背包中可用的体积在状态转移的时候是个变量。
Bob: 那让 dp[i][j] 是前 i 个物品,在背包剩余体积是 j 的状态下的最大价值 ?
Alice: 不应该绕一道,让 dp[i][j] 是前 i 个物品在消耗了 j 的体积下的最大价值就好了。这样的话, dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[j]] + w[j] )
Bob: 我想想 🤔️,第 i 个物品不装入背包的时候,最大价值就是前 i-1 个物品在 j 的体积下的最大价值,就是 dp[ i - 1 ][j] 。第 i 个物品装入背包的时候,装的下吗 ?如果要装的下的话,应该是 dp[i-1][j - v[j]] + w[j], 也就是说前 i-1 个物品最多消耗 j-v[j] 的空间,这样才放得下,然后加上第 j 个物品的价值 w[j] 就可以了。
Alice: 对,应该没问题。
Bob: 具体的求解过程呢 ?初始化一个二维数组,一维是物体数量,二维是背包体积,初始化都是 0,然后从上到下,从左到右按照上面的 递推公式求解二维数据,最后整个数组的最大值就是答案 ?
Alice: 我去试试 💃 … 果然过了。
Bob: 计算的过程感觉还是要注意一下,初始化的时候还要先求解第一行的。


Alice: 听说还有什么状态压缩 ?
Bob: 减少二维数组消耗的内存 ?把 n*m 的数据改成 2 * m 的,因为每次计算只有两行之间的递推关系。
Alice:有点麻烦,需要来回的覆写数组。
Bob: 难道是能改成一维的动态规划 !!
Alice: 应该是吧,dp[i][j] 表示的是前 i 个物品消耗体积小于等于 j 时的最大价值,改成一维的话,i 和 j 应该保留哪个呢 ?
Bob: 哪个能完成状态转移就保留哪个吧,值肯定还是最大价值。保留体积 j 的话,dp[j] = max(dp[j], dp[j-v[i]] + w[i]) ,是这样的吗 ?
Alice: 怎么还有 i 呢 ?
Bob: 当然还有 i, 状态转移还是第 i 个物品要不要装进去。双循环是不变的,只是优化是有的二维数组的空间。
Alice:你去试试 ?
Bob: 有个麻烦的点,很难理解。计算体积的时候要反向计算,因为 dp[j] 依赖于 dp[j-ivolum],而这两个东西是在一个一维数组里面不断更新的,所要要先更新 dp[j] 再更新 dp[j-ivolum],不然就算不对了。
Alice:确实 🤔️


Alice: 还有一件事,我听嗦状态压缩之后根本不用求最大值,dp[maxVolumn] 就是最大值,是真的吗 ?
Bob: 这个和你初始化的方式有关系,如果 dp 全都初始化成 0,那 dp[maxVolumn] 就是最大值,否则不一定。
Alice: 这么神奇的吗 ?
Bob: 最的价值不一定把背包填满了,假设最大价值时消耗的空间是 k,且 k < maxVolumn 想一下这个时候的状态转移。
Alice: 如果不把 dp 都初始化为 0,那应该怎么初始化,dp[0] = 0,其他空间初始化成负无穷 ?
Bob: 对,这样在算的时候 dp[j] = max(dp[j], dp[j-v[i]] + w[i]) ,如果 j-v[i] 不是一个有效的填充体积,那 dp[j] 就还是负无穷,只有有效的填充体积才能有价值。这个时候是需要求最大值的。
Alice: 我去试试
Bob: 给你个测试用例

5 4
2 2
2 4
3 4
4 5
3 7

dp 算完应该是这样的 [ 0, -Infinity, 4, 7, 6 ]


代码:

二维动态规划 + js

const fs = require('fs');
let buffer = '';

process.stdin.on('readable', () => {
    const chunk = process.stdin.read();
    if (chunk) {
        buffer += chunk.toString()
    }
});

// 输入的字符串转换为数字
const convert = (inputString) => {
    const list = [];
    inputString.split('\n').forEach((line) => {
        const tokens = line.split(' ');
        list.push(tokens.map(num => parseInt(num, 10)));
    });
    return list;
}

// 批量调用
const batchCall = (list, solve) => {
    // 划分数据
    const data = [];
    let countAndVolumIndex = 0;
    while(countAndVolumIndex < list.length) {
        const [count, volum] = list[countAndVolumIndex];
        data.push({
            volum: volum,
            count: count,
            volumAndWeight: list.slice(countAndVolumIndex + 1, countAndVolumIndex + 1 + count)
        });
        countAndVolumIndex += count + 1;
    }
    
    data.forEach(item => {
        // 防止空行或者无效数据
        if(solve && item && item.count && item.volum) {
            solve(item.count, item.volum, item.volumAndWeight);
        }
    });
}


const solve = (count, maxVolum, volumAndWeight) => {
    const dp = [];
    for(let i=0; i<=count; ++i) {
        dp.push(new Array(maxVolum + 1).fill(0));
    }
    
    // 初始化第一行
    const [firstThingVolum, firstThingWeight] = volumAndWeight[0];
    for(let j=0; j<=maxVolum; ++j) {
        dp[0][j] = j >= firstThingVolum ? firstThingWeight : 0;
    }
    
    let result = 0;
    for(let i=1; i<count; ++i){
        for(let j=0; j<=maxVolum; ++j) {
            // 第 i 个物品的体积和价值
            const [ivolum, iweight] = volumAndWeight[i];
            dp[i][j] = dp[i-1][j];
            if (j >= ivolum) {
                dp[i][j] = Math.max(
                    dp[i-1][j],
                    dp[i-1][j-ivolum] + iweight
                )
            }
            result = Math.max(result, dp[i][j]);
        }
    }
    console.log(result);
}

process.stdin.on('end', function() {
    batchCall(convert(buffer), solve)
});

状态压缩 + js

const fs = require('fs');
let buffer = '';

process.stdin.on('readable', () => {
    const chunk = process.stdin.read();
    if (chunk) {
        buffer += chunk.toString()
    }
});

// 输入的字符串转换为数字
const convert = (inputString) => {
    const list = [];
    inputString.split('\n').forEach((line) => {
        const tokens = line.split(' ');
        list.push(tokens.map(num => parseInt(num, 10)));
    });
    return list;
}

// 批量调用
const batchCall = (list, solve) => {
    // 划分数据
    const data = [];
    let countAndVolumIndex = 0;
    while(countAndVolumIndex < list.length) {
        const [count, volum] = list[countAndVolumIndex];
        data.push({
            volum: volum,
            count: count,
            volumAndWeight: list.slice(countAndVolumIndex + 1, countAndVolumIndex + 1 + count)
        });
        countAndVolumIndex += count + 1;
    }
    
    data.forEach(item => {
        if(solve && item && item.count && item.volum) {
            solve(item.count, item.volum, item.volumAndWeight);
        }
    });
}


const solve = (count, maxVolum, volumAndWeight) => {
    const dp = new Array(maxVolum + 1).fill(0);
    
    let result = 0;
    for(let i=0; i<count; ++i){
        // 为何这里是从大到小反向计算呢 ?
        // 从第二个物品开始思考,第一个物品计算完了的时候,dp[j] 就是 dp[i-1][j], 
        // 现在我们要更新 dp[j] 的数据,从右往左更新,是因为右依赖左,
        // dp[j] 的计算要依赖 dp[j-ivolum],所以要保证计算 dp[j] 的时候,
        // dp[j-ivolum] 还是 i-1 的时候的值。
        for(let j=maxVolum; j>=0; --j) {
            // 第 i 个物品的体积和价值
            const [ivolum, iweight] = volumAndWeight[i];
            if (j >= ivolum) {
                dp[j] = Math.max(
                    dp[j],
                    dp[j-ivolum] + iweight
                )
            }
            
            result = Math.max(result, dp[j]);
        }
    }

    console.log(result);
}

process.stdin.on('end', function() {
    batchCall(convert(buffer), solve)
});

状态压缩 + 是否精确求解 + js

dp[j] 为体积恰好等于 j 时的最大价值

const solve = (count, maxVolum, volumAndWeight) => {
    const dp = new Array(maxVolum + 1).fill(-1 * Infinity);
    dp[0] = 0;
    
    for(let i=0; i<count; ++i){
        for(let j=maxVolum; j>=0; --j) {
            // 第 i 个物品的体积和价值
            const [ivolum, iweight] = volumAndWeight[i];
            if (j >= ivolum) {
                dp[j] = Math.max(
                    dp[j],
                    dp[j-ivolum] + iweight
                )
            }
        }
    }
    console.log(Math.max(...dp));
}

dp[j] 为体积 <= j 时的最大价值

const solve = (count, maxVolum, volumAndWeight) => {
    const dp = new Array(maxVolum + 1).fill(0);
    
    for(let i=0; i<count; ++i){
        for(let j=maxVolum; j>=0; --j) {
            // 第 i 个物品的体积和价值
            const [ivolum, iweight] = volumAndWeight[i];
            if (j >= ivolum) {
                dp[j] = Math.max(
                    dp[j],
                    dp[j-ivolum] + iweight
                )
            }
        }
    }

    console.log(dp[maxVolum]);
}

参考:

更多推荐

Android 12 源码分析 —— 应用层 五(SystemUI的StatusBar类的启动过程和三个窗口的创建)

Android12源码分析——应用层五(SystemUI的StatusBar类的启动过程和三个窗口的创建)更新历史日期内容12023-9-18修改关于mLightsOutNotifController的错误注释在前面的文章中,我们介绍了SystemUIApp的基本布局和基本概念。接下来,我们进入SystemUI应用的各

java面试题基础第七天

一、java面试题第七天1.throw和throws的区别?throw:用于抛出一个异常对象throws:写在方法体上面,将方法体里面的异常,抛给上层2.通过故事讲清楚NIO下面通过一个例子来讲解下。假设某银行只有10个职员。该银行的业务流程分为以下4个步骤:1)顾客填申请表(5分钟);2)职员审核(1分钟);3)职员

爬虫 — Bs4 数据解析

目录一、介绍二、使用三、Bs4对象种类1、tag:标签2、NavigableString:可导航的字符串3、BeautifulSoup:bs对象4、Comment:注释四、遍历文档树1、遍历子节点2、获取节点内容3、遍历父节点4、遍历兄弟节点五、常用方法六、CSS选择器七、案例一、介绍Bs4(beautifulsoup

初识自动驾驶技术之旅 第一课 学习笔记

​🎬岸边的风:个人主页🔥个人专栏:《VUE》《javaScript》⛺️生活的理想,就是为了理想的生活!​目录📚前言📘1.自动驾驶人才需求与挑战📘2.Apollo8.0开源平台详解📘3.如何使用Apollo学习自动驾驶[上机学习]📘4.如何使用Apollo学习自动驾驶[上车学习]📚总结📚前言讲师介绍:

股票数据分析应用之可视化图表组件

股市是市场经济的必然产物,在一个国家的金融领域之中有着举足轻重的地位。在过去,人们对于市场走势的把握主要依赖于经验和直觉,往往容易受到主观因素的影响,导致决策上出现偏差。如今,通过数据可视化呈现,便可将历年数据和市场情报进行深度挖掘、分析,从中找到规律和趋势,帮助用户做出更准确的判断。回顾2022年A股市场的表现可谓是

翻牌闯关游戏

翻牌闯关游戏3关:关卡由少至多12格、20格、30格图案:12个玩法:点击两张卡牌,图案一到即可消除掉记忆时长(秒):memoryDurationTime:5可配置:默认5提示游戏玩法:showTipsFlag:1可配置:1:判断localStorage值,仅一次提示游戏玩法2:每次游戏第一关(12格关卡)都提示游戏玩

基于Java演唱会购票系统设计实现(源码+lw+部署文档+讲解等)

博主介绍:✌全网粉丝30W+,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌🍅文末获取源码联系🍅👇🏻精彩专栏推荐订阅👇🏻不然下次找不到哟2022-2024年最全的计算机软件毕业设计选题

JavaScript知识系列(3)每天10个小知识点

目录系列文章目录JavaScript知识系列(1)每天10个小知识点JavaScript知识系列(2)每天10个小知识点知识点**20.AJAX**的概念、作用、原理、特性、优点、缺点、区别、使用场景**,实现一个AJAX请求****21.尾调用**的概念、作用、原理、特性、优点、缺点、区别、使用场景**22.ES6模

互联网数字化管理升级,制造企业一站式智能管理,可定制-亿发

在互联网时代,传统机械制造企业面临着未有的挑战和机遇。信息化管理水平成为企业竞争力的关键因素。然而,许多制造企业在信息化管理中常常陷入以下三大问题:1、盲目随潮流,缺乏总体规划互联网时代,科技发展日新月异,让制造企业感到需要跟上潮流。然而,盲目跟风而行往往导致了技术的不合理使用和资源的浪费。解决这个问题的关键在于建立明

运维:Centos7安装解压版mysql5.7

目录1、卸载Centos7默认自带的mariadb数据库,避免冲突2、下载解压版mysql并安装3、配置mysql4、mysql客户端访问MySQL是一种开源的关系型数据库管理系统(RDBMS),它具有许多优点和一些缺点。以下是MySQL的主要优缺点:优点1.开源免费:MySQL是开源软件,可以免费使用,并且有大量的社

机器视觉常见的问题及解决

应该怎样选择相机?选择相机却往往刻不容缓的的问题摆在机器视觉工程师面前,因此,选择相机了解以下几个方面问题:通常您首先需要知道系统精度要求和相机分辨率,可以通过公式:X方向系统精度(X方向像素值)=视野范围(X方向)/CCD芯片像素数量(X方向);Y方向系统精度(Y方向像素值)=视野范围(Y方向)/CCD芯片像素数量(

热文推荐