AcWing动态规划基础学习——背包问题

AcWing动态规划基础学习——背包问题

本文主要参考AcWing算法基础课相关内容,用于学习记录与分享,如有侵权请联系我删除。

一、0-1背包问题

概述:给定一组物品,每个物品有重量和价值,在限定的总重量内选择物品,使得总价值最大每个物品最多只能选择一次(选或不选)

问题描述:物品重量数组 w,物品价值数组 v,背包容量 W。在不超过背包容量的情况下,问能装入物品的最大价值。

思考方式:0-1背包问题都可以按照下图来分析

简单引例:AcWing2.01背包问题

C ++ 代码:

#include <iostream> #include <algorithm> using namespace std; const int N = 1010; //v表示物品体积 w表示物品价值 int v[N], w[N]; //创建状态表示集合 int dp[N][N]; int main() { int n, m; cin >> n >> m; for(int i = 1; i <= n; i ++) cin >> v[i] >> w[i]; for(int i = 1; i <= n; i ++) { for(int j = 0; j <= m; j ++) { //状态计算 //不含i时 dp[i][j] = dp[i - 1][j]; //含i时,只有当前j >= v_i,即能装的下v_i时才含i if(j >= v[i]) dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]); } } cout << dp[n][m]; return 0; }

优化思路:

将状态f[i][j]优化到一维f[j],实际上只需要做一个等价变形。

为什么可以这样变形呢?我们定义的状态f[i][j]可以求得任意合法的i与j最优解,但题目只需要求得最终状态f[n][m],因此我们只需要一维的空间来更新状态。

(1)状态f[j]定义:N件物品,背包容量j下的最优解。

(2)注意枚举背包容量j必须从m开始

(3)为什么一维情况下枚举背包容量需要逆序?在二维情况下,状态f[i][j]是由上一轮i - 1的状态得来的,f[i][j]与f[i - 1][j]是独立的。而优化到一维后,如果我们还是正序,则有f[较小体积]更新到f[较大体积],则有可能本应该用第i-1轮的状态却用的是第i轮的状态。

(4)不理解看看这个 例如,一维状态第 i 轮对体积为 3 的物品进行决策,则f[ 7 ]由f[ 4 ]更新而来,这里的f[ 4 ]正确应该是 f[ i - 1 ][ 4 ],但从小到大枚举 j 这里的 f[ 4 ] 在第i轮计算却变成了 f[ i ][ 4 ],我们需要的是 f [ i - 1 ][ 4 ]而不是 f[ i ][ 4 ],正序的话 f[ i - 1 ][ 4 ]就会被 f[ i ][ 4 ]覆盖。当逆序枚举背包容量 j 时,我们求 f[ 7 ]同样由 f[ 4 ]更新,但由于是逆序,这里的f[ 4 ]还没有在第i轮计算,所以此时实际计算的 f[ 4 ]仍然是 f[ i - 1 ][ 4 ]。

(5)简单来说,一维情况正序更新状态f[j]需要用到前面计算的状态已经被「污染」,逆序则不会有这样的问题。

一维状态转移方程为:f[j] = max(f[j], f[j - v[i]] + w[i])。

for(int i = 1; i <= n; i++) 
    for(int j = m; j >= 0; j--)
    {
        if(j < v[i]) 
            f[i][j] = f[i - 1][j];  // 优化前
            f[j] = f[j];            // 优化后,该行自动成立,可省略。
        else    
            f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);  // 优化前
            f[j] = max(f[j], f[j - v[i]] + w[i]);                   // 优化后
    }    
实际上,只有当枚举的背包容量 j >= v[i] 时才会更新状态,因此我们可以修改循环终止条件进一步优化。

优化后一维代码:

for(int i = 1; i <= n; i++) { //0-1背包 j 要逆序     for(int j = m; j >= v[i]; j--)           f[j] = max(f[j], f[j - v[i]] + w[i]); } 

二、完全背包问题

与0-1背包类似,主要区别在于每种物品可以选取无限次

问题描述:给定一个容量为( W )的背包和( n )种物品,每种物品有重量( w_i )和价值( v_i ),求在不超过背包容量的前提下,能装入物品的最大价值总和。

思考方式(朴素方法):与0-1背包的区别在于集合的划分不同,这里是将集合划分成k + 1段,k 的含义是: j 的空间先装 0 个 i 物品、1 个 i 物品、2 个 i 物品...直至装到 k 个 i 物品时再也装不下。

最终状态计算:f [ i ][ j ] = max(f [ i ][ j - v_i * k] + w_i * k), k = 0, 1, 2, ...

简单引例:AcWing3.完全背包问题

C ++代码(可能会超时):

#include <iostream> #include <algorithm> using namespace std; const int N = 1010; int v[N], w[N]; int f[N][N]; int main() { int n, m; cin >> n >> m; for(int i = 1; i <= n; i ++) cin >> v[i] >> w[i]; for(int i = 1; i <= n; i ++) { for(int j = 0; j <= m; j ++) { for(int k = 0; v[i] * k <= j; k ++) { f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k); } } } cout << f[n][m]; return 0; }

优化思路一:朴素方法困扰我们的主要是三层循环导致超时,这是由于状态计算和 k 相关。由下图我们可以观察到 f [ i ][ j ] 和 f [ i , j - v ]的计算公式部分很相似,只相差 w ,从而我们可以得到优化后的公式:f [ i ][ j ] = Max(f [ i - 1][ j ], f [ i, j - v_i] + w_i),这样就和 k 无关了。

优化后代码(二重循环):我们可以发现,与0-1背包二维代码相比仅有细微区别。

0-1背包状态计算:f [ i ][ j ] = Max(f [ i - 1][ j ], f [ i - 1, j - v_i] + w_i),从 i - 1 层转换

完全背包状态计算:f [ i ][ j ] = Max(f [ i - 1][ j ], f [ i, j - v_i] + w_i),从 i 层转换。

#include <iostream> #include <algorithm> using namespace std; const int N = 1010; int v[N], w[N]; int f[N][N]; int main() { int n, m; cin >> n >> m; for(int i = 1; i <= n; i ++) cin >> v[i] >> w[i]; for(int i = 1; i <= n; i ++) { for(int j = 0; j <= m; j ++) { f[i][j] = f[i - 1][j]; //与0-1背包问题的区别仅在f[i][j - v[i]] + w[i]; if(j >= v[i]) f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i]); } } cout << f[n][m]; return 0; }

于是与0-1背包优化一样,可以对上述代码再进行一维优化得到最终版本。

完全背包最终版代码(一维数组):

#include <iostream> #include <algorithm> using namespace std; const int N = 1010; int v[N], w[N]; int f[N]; int main() { int n, m; cin >> n >> m; for(int i = 1; i <= n; i ++) cin >> v[i] >> w[i]; for(int i = 1; i <= n; i ++) { //这里是 j 是正序,由于是从 i 层转换不存在数据覆盖问题 for(int j = v[i]; j <= m; j ++) { f[j] = max(f[j], f[j - v[i]] + w[i]); } } cout << f[m]; return 0; }

三、多重背包问题

与0-1背包问题(每种物品只能选一次)和完全背包问题(每种物品无限次选择)不同,多重背包问题中每种物品有固定的数量限制

问题描述:给定一个背包容量为 ( W ),以及 ( n ) 种物品,每种物品有三个属性:重量 ( w_i ),价值 ( v_i ),数量 ( s_i )(表示第 ( i ) 种物品最多可以选择 ( s_i ) 次)。目标是在不超过背包容量的前提下,选择物品的组合使得总价值最大。

思考方式(朴素方法):与完全背包问题几乎完全相同,唯一的区别在于完全背包是装到装不下,这里是装到 s_i 个,即 k 的取值为:0,1,2,3,...,s_i;

最终状态计算:f [ i ][ j ] = max(f [ i ][ j - v_i * k] + w_i * k), k = 0, 1, 2, ..., s[ i ]

简单引例:AcWing4. 多重背包问题 I

朴素三重循环(可能会超时)代码:

#include <iostream> #include <algorithm> using namespace std; const int N = 110; int v[N], w[N], s[N]; int f[N][N]; int main() { int n, m; cin >> n >> m; for(int i = 1; i <= n; i ++) cin >> v[i] >> w[i] >> s[i]; for(int i = 1; i <= n; i ++) { for(int j = 0; j <= m; j ++) { for(int k = 0; k <= s[i] && k * v[i] <= j; k ++) { f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k); } } } cout << f[n][m]; return 0; }

尝试完全背包的优化方式列出多重背包的 f[ i ][ j ] 和 f[ i ][ j - v]的表达式:

会发现 f[ i ][ j - v] 最后会多出一项f [ i−1, j−(s_i+1) *v] + s_i * w,这是为什么呢?

不理解看这里 这里是在分析 f[ i, j−w ]这个状态的表达式,首先这个状态的含义是从前 i 个物品中选,且总体积不超过 j-w 的最大价值,我们现在最多只能选 s 个物品,因此如果我们选 s 个第 i 个物品,那么体积上就要减去 s∗v,价值上就要加上 s∗w,那更新到状态中去就是 f[i−1,j−v−s∗v] + s∗w,表现出来就是多出一项(其实不是多,只是二者项数一样,由于错位对齐看起来多)。

那为什么完全背包不会有最后一项?
完全背包由于对每种物品没有选择个数的限制,所以只要体积够用就可以一直选,其结束的条件一样就是当装到不能再装时停止,表现出来就是对齐的样子(其实完全背包的 f[ i, j - v]要比 f[ i, j ]少一项)。

为什么不能用像完全背包一样去优化?简单来说就是我们不能通过总的最大值和最后一个数的最大值来求前面的最大值是多少,可参考视频

正确优化—二进制优化方式 :

首先,借助0-1背包的思想,对每种物品的每一个都进行取或不取的判断,但是这样又太多了,复杂度为N*V*S,很容易超时。

我们知道任意一个实数可以由二进制数来表示:

1可以表示0~1;

1,2可以表示0~3;

1,2,4可以表示0~7;

1,2,4,...,2的k次方可以表示0~2的(k+1)次方 - 1;

比如对于200个物品,我们如果对每一个都进行取不取的判断,需要200次;但借助二进制我们知道200可以由1,2,4,8,16,32,64,73八组来表示,这样只需要判断8次,大幅减少了我们判断的次数。

于是借助二进制优化的思维可将,先将每种物品进行分组,再来一次0-1背包的判断就可以得出结果,复杂度降为N*logS*V。

最终代码:

#include <iostream> #include <algorithm> using namespace std; const int N = 25000, M = 2010; int v[N], w[N]; int f[M]; int main() { int n, m; cin >> n >> m; //新组号 int cnt = 0; //对于每一种物品都进行二进制优化分组 for(int i = 1; i <= n; i ++) { int a, b, s; cin >> a >> b >> s; int k = 1; while(k <= s) { cnt ++; v[cnt] = a * k; w[cnt] = b * k; s -= k; k *= 2; } if(s > 0) { cnt ++; v[cnt] = a * s; w[cnt] = b * s; } } //此时n应该为新分组的组数 n = cnt; //进行0-1背包判断 for(int i = 1; i <= n; i ++) { for(int j = m; j >= v[i]; j --) { f[j] = max(f[j], f[j - v[i]] + w[i]); } } cout << f[m]; return 0; }

四、分组背包问题

分组背包问题是动态规划中背包问题的变种,其特点是将物品分为若干组,每组内的物品互斥,即每组只能选择一个物品或不选。目标是在背包容量限制下,选出物品组的最优组合,使得总价值最大。

分组背包问题其实类似于0-1背包问题,就是每次需要判断每组物品中选哪一个。

状态计算:f[ i ][ j ] = Max( f[ i -1][ j ], f[ i - 1, j - v[ i ][ k ]] + w [ i ][ k ] );

简单引例:AcWing9. 分组背包问题

这里直接上一维优化后的代码:

#include <iostream> #include <algorithm> using namespace std; const int N = 110; int v[N][N], w[N][N], s[N]; int f[N]; int main() { int n, m; cin >> n >> m; for(int i = 1; i <= n; i ++) { cin >> s[i]; for(int j = 1; j <= s[i]; j ++) { cin >> v[i][j] >> w[i][j]; } } //整体还是0-1背包的模板 for(int i = 1; i <= n; i ++) { for(int j = m; j >= 0; j --) { //对每组的每一个物品进行判断 for(int k = 1; k <= s[i]; k ++) { if(j >= v[i][k]) f[j] = max(f[j], f[j - v[i][k]] + w[i][k]); } } } cout << f[m]; return 0; }

Read more

Flutter 三方库 dart_webrtc 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、透明、基于 WebRTC 标准的工业级实时音视频通讯与低延迟流媒体引擎

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 dart_webrtc 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、透明、基于 WebRTC 标准的工业级实时音视频通讯与低延迟流媒体引擎 在鸿蒙(OpenHarmony)系统的跨端视频会议、分布式安防监控、直播连麦或者是需要实现“端到端(P2P)”低延迟数据传输的场景中,如何通过一套 Dart 代码调用底层浏览器级的 WebRTC 算力?dart_webrtc 为开发者提供了一套工业级的、针对 Web 平台(JS 接口)进行高度封装的 WebRTC 适配方案。本文将深入实战其在鸿蒙 Web 入口应用中的音视频能力扩展。 前言 什么是 Dart WebRTC?它不仅是一个简单的。管理过程。由于由接口包装。

By Ne0inhk
前端计算机基础

前端计算机基础

进程和线程的区别 简单记:进程是 “独立的容器”,线程是 “容器里干活的人”,多人共享容器资源,效率更高但也更容易互相影响。 进程:独立可运行的程序,比如微信,留言及,VSCODE 进程是操作系统资源分配的最小单位(资源包括内存、CPU 时间片、文件句柄等),每个进程都有自己独立的内存空间,进程之间互不干扰。 线程:是进程的执行单位,一个进程可以包含多个县城,比如微信进程中,有接收消息线程,渲染界面线程 线程是调度执行的最小单位 ,同一进程内的线程共享进程的内存和资源。 类比:进程像一家 “独立的公司”,有自己的办公场地(内存)、资金(系统资源);线程像公司里的 “员工”,共享公司的场地和资金,各自做不同的工作,协作完成公司整体任务。 维度进程线程资源分配系统资源分配的最小单位资源调度 / 执行的最小单位内存空间每个进程有独立的内存空间共享所属进程的内存空间通信方式复杂(需 IPC:管道、套接字、共享内存等)简单(直接读写进程内共享变量)创建

By Ne0inhk
Qt与Web混合编程:CEF与QCefView深度解析

Qt与Web混合编程:CEF与QCefView深度解析

Qt与Web混合编程:CEF与QCefView深度解析 * 1. 引言:现代GUI开发的融合趋势 * 2. Qt与Web集成方案对比 * 3. CEF核心架构解析 * 4. QCefView:Qt与CEF的桥梁 * 5. 实战案例:智能家居控制面板 * 6. 性能优化策略 * 7. 调试技巧大全 * 8. 安全加固方案 * 9. 未来展望:WebComponent集成 * 10. 结语 1. 引言:现代GUI开发的融合趋势 在当今的桌面应用开发领域,本地GUI框架与Web技术的融合已成为不可逆转的趋势。Qt作为成熟的跨平台C++框架,与Web技术的结合为开发者提供了前所未有的灵活性: * 本地性能 + Web动态性 = 最佳用户体验 * 快速迭代的Web前端 + 稳定可靠的本地后端 * 跨平台一致性 + 现代UI效果 35%25%20%20%混合应用优势分布开发效率UI表现力跨平台性性能平衡 2. Qt与Web集成方案对比 方案优点缺点适用场景Qt WebEngine官方支持,

By Ne0inhk
Flutter for OpenHarmony:web_socket_channel 全平台 WebSocket 通信标准库,从原理到鸿蒙实战(3000字深度解析)

Flutter for OpenHarmony:web_socket_channel 全平台 WebSocket 通信标准库,从原理到鸿蒙实战(3000字深度解析)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在现代 App 开发中,实时通信(Real-time Communication)已成为标配。无论是社交聊天的由“推”变“拉”,还是股票行情的毫秒级跳动,亦或是智能家居的状态同步,传统的 HTTP 轮询(Polling)已无法满足低延迟、高并发的需求。 WebSocket 协议应运而生。它基于 TCP,但在握手阶段利用 HTTP 升级协议(Upgrade Header),成功后建立全双工(Full-Duplex)的长连接。在这条通道上,客户端和服务端可以随时互相推送数据,且头部开销极小。 在 Flutter 生态中,虽然 dart:io 提供了原生的 WebSocket 类,dart:

By Ne0inhk