动态规划DP 背包问题 多重背包问题(朴素版+二进制优化+单调队列)

news2025/2/5 9:08:45

概览检索
动态规划DP 概览(点击链接跳转)
动态规划DP 背包问题 概览(点击链接跳转)在这里插入图片描述

多重背包问题1

原题链接

AcWiing 4. 多重背包问题1

题目描述

有 N种物品和一个容量是 V的背包。
第 i 种物品最多有 si件,每件体积是 vi,价值是 wi。

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

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i种物品的体积、价值和数量。

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

数据范围
0<N,V≤100
0<vi,wi,si≤100

输入样例

4 5
1 2 3
2 4 1
3 4 3
4 5 2

输出样例:

10

题目分析

多重背包:对于每个物品可以选择0,1,2,···,s[i]个(有限个)。

闫氏DP分析法

在这里插入图片描述
f [ i , j ] f[i,j] f[i,j] 表示从1~i 个物品中选择且总体积不超过 j 的选法中价值最大的那个。

对于第i个物品,
可以选0个 i,即在 0~i -1 个物品中选择,体积不超过j,即 f [ i − 1 , j ] f[i-1,j] f[i1,j]
可以选1个 i,同时在 0~i -1 个物品中选择,体积不超过 j − v [ i ] j-v[i] jv[i],即 f [ i − 1 , j − v [ i ] ] + w [ i ] f[i-1,j-v[i]]+w[i] f[i1,jv[i]]+w[i]
···
可以选 s[i] 个 i,同时在 0~i -1 个物品中选择,体积不超过 j − s [ i ] × v [ i ] j-s[i] \times v[i] js[i]×v[i],即 f [ i − 1 , j − s [ i ] × v [ i ] ] + s [ i ] × w [ i ] f[i-1,j-s[i] \times v[i]]+s[i] \times w[i] f[i1,js[i]×v[i]]+s[i]×w[i]

其中,选择物品i 的个数由限定有限的s[i]背包体积共同限定。

综上,
f [ i , j ] f[i,j] f[i,j] 即为上述所有结果的最大值。
f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − k × v [ i ] ] + k × w [ i ] ) f[i,j]=max(f[i-1,j],f[i-1,j-k \times v[i]]+k \times w[i]) f[i,j]=max(f[i1,j],f[i1,jk×v[i]]+k×w[i])

完整代码

#include <iostream>
#include <algorithm>
using namespace std;
const int N=110;
int n,m;
int v[N],w[N],s[N];
int f[N][N];
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>v[i]>>w[i]>>s[i];
    //遍历n个物品
    for(int i=1;i<=n;i++){
        //遍历背包体积
        for(int j=0;j<=m;j++){
            //遍历物品i的个数
            for(int k=0;k<=j/v[i]&&k<=s[i];k++){
                f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
            }
        }
    }
    cout<<f[n][m]<<endl;
    return 0;
}

多重背包问题2

原题链接

AcWing 5.多重背包问题2

题目分析

与上述完全背包1的描述完全相同,不同点在于数据范围的变化。
数据范围
0<N≤1000
0<V≤2000
0<vi,wi,si≤2000

数据范围变大,由原来的100变为1000/2000,需对原先代码进行优化,采用 二进制优化

在原先的代码中,我们枚举了0~s[i]的所有选法,
是否存在其他的办法让我们不用一项一项枚举呢?
采用 打包

情况1:s[i]恰为 2 k − 1 2^{k}-1 2k1

例如,s[i]=127,
原来我们需枚举:1,2,3,···,127,127,共s=127个。
优化后,我们采用打包,打包为1个,2个,4个,8个,···,64个(2的次方),各位一组,共7组。
这相当于7组物品,每组物品可以选择 用 or 不用,相当于转化为 01背包问题
复杂度由 s s s 转化为 log ⁡ s \log_{}s logs

而这些打包数能否组合表示从1~127的所有数呢?
显然是可以的,
1,2可以组合成1~3的任意数;
1,2,4可以组合成1~7的任意数;
1,2,4,8可以组合成1~15的任意数;
···
1,2,4,···,64恰好可以组合成1~127的任意数。
因此,上述打包数可以组合表示1~127的所有数。

示意图如下:
在这里插入图片描述

情况2:s[i]不为 2 k − 1 2^{k}-1 2k1

例如,s[i]=120,
原来我们需枚举:1,2,3,···,64,···,120,共s=120个。
优化后,我们采用打包,打包为1个,2个,4个,8个,···,32个, ( 120 − 1 − 2 − 4 − ⋅ ⋅ ⋅ − 32 ) = 57 (120-1-2-4-···-32)=57 120124⋅⋅⋅32=57个,各为一组,共7组。
这相当于7组物品,每组物品可以选择 用 or 不用,相当于转化为 01背包问题
复杂度由 s s s 转化为 log ⁡ s \log_{}s logs

而这些打包数能否组合表示从1~120的所有数呢?
显然是可以的,
1,2可以组合成1~3的任意数;
1,2,4可以组合成1~7的任意数;
···
1,2,4,···,32恰好可以组合成1~63的任意数。
加上57,则可以组合成1+57 ~ 63+57即 58 ~ 120 的任意数。
拼接起来,
因此,上述打包数可以组合表示1~120的所有数。

综上,
对于任意的s,
打包为 1 , 2 , 4 , 8 , ⋅ ⋅ ⋅ , 2 k , c 1,2,4,8,···,2^{k},c 1248⋅⋅⋅2kc ,其中 c < 2 k + 1 c<2^{k+1} c<2k+1
1 , 2 , 4 , 8 , ⋅ ⋅ ⋅ , 2 k 1,2,4,8,···,2^{k} 1248⋅⋅⋅2k可以凑出 0 ∼ 2 k + 1 − 1 0 \thicksim2^{k+1}-1 02k+11 的所有数,
加上c,则可以凑出 c ∼ 2 k + 1 − 1 + c c\thicksim2^{k+1}-1+c c2k+11+c 的所有数,
又由于 c < 2 k + 1 c<2^{k+1} c<2k+1
即凑出了 0 ∼ 2 k + 1 − 1 + c 0 \thicksim2^{k+1}-1+c 02k+11+c 的所有数,
使 2 k + 1 − 1 + c = s 2^{k+1}-1+c=s 2k+11+c=s 即可凑出 0 ∼ s 0 \thicksim s 0s 的所有数。
自此打包为 log ⁡ s \log_{}s logs 组物品,转化为 01 背包问题

完整代码

#include <iostream>
#include <algorithm>
using namespace std;
const int N=20000,M=2010;
int v[N],w[N];
int f[N];
int n,m;
int main(){
    cin>>n>>m;
    int cnt=0;
    for(int i=1;i<=n;i++){
        int a,b,s;
        cin>>a>>b>>s;  //体积,价值,数量
        int k=1;
        //2^k的打包操作
        while(k<=s){
            cnt++;
            v[cnt]=k*a;  //k个总体积
            w[cnt]=k*b;  //k个总价值
            s-=k;  //总数-k个
            k*=2;  //k更新为2*k
        }
        //如果总数量s不等于2^k-1,则剩余的打包为一组
        //剩余s个
        if(s>0){
            cnt++;
            v[cnt]=s*a;  //剩余s个总体积
            w[cnt]=s*b;  //剩余s个总价值
        }
    }
    //总组数为cnt
    n=cnt;
    //转化为cnt个01背包问题
    //遍历n组物品
    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]<<endl;
    return 0;
}

多重背包问题3

原题链接

AcWing 6. 多重背包问题3

题目描述

与上述完全背包1的描述完全相同,不同点在于数据范围的变化。
数据范围
0<N≤1000
0<V≤20000
0<vi,wi,si≤20000
数据范围的再次扩大,需要对代码进行再次的优化。采用单调队列/滑动窗口

观察状态计算公式,
在这里插入图片描述

f[i,j]与f[i,j-v]有相同项,但f[i,j-v]比f[i,j]多了一项,仔细观察发现,f[i,j]的max计算即为f[i,j-v]的前s项+w,即 f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i , j − v ] 的前 s 项 + w ) f[i,j]=max(f[i-1,j],f[i,j-v]的前s项+w) f[i,j]=max(f[i1,j],f[i,jv]的前s+w)

且对于每个 f [ i , j ] , f [ i , j − v ] , f [ i , j − 2 v ] , ⋅ ⋅ ⋅ f[i,j],f[i,j-v],f[i,j-2v],··· f[i,j],f[i,jv],f[i,j2v],⋅⋅⋅ 都可通过其前一项的前s项进行递推运算。

画出数轴对应示意图如下,每次计算出前s项中的最大值。
在这里插入图片描述
由此观察我们可以发现,与 滑动窗口 及其相似!
滑动窗口主要利用单调队列解决了一组数中滑动的固定大小的窗口中的最大值/最小值。
下面详细说明 滑动窗口 的思想。

滑动窗口/单调队列

原题链接
AcWing 154. 滑动窗口

题目描述
给定一个数组。
有一个大小为 k的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k个数字。
每次滑动窗口向右移动一个位置。

以下是一个例子:
该数组为 [1 3 -1 -3 5 3 6 7],k为 3。

窗口位置最小值最大值
[1 3 -1] -3 5 3 6 7-13
1 [3 -1 -3] 5 3 6 7-33
1 3 [-1 -3 5] 3 6 7-35
1 3 -1 [-3 5 3] 6 7-35
1 3 -1 -3 [5 3 6] 736
1 3 -1 -3 5 [3 6 7]37

任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
题目分析
窗口可用队列来维护,队尾插入,队头弹出。
若求最小值,
如下列实例滑动窗口中,3<-3,-1<-3,且-3在后面,则只要-3在,则最小值一定是-3,3与-1的值一定不会被取到,
在这里插入图片描述
因此,提炼出,只要出现逆序对,即前面的数大于后面的数,则一定不会取到,则出队
由此,可知,队列中的数一定是单调递增的( 单调队列 )。
则,队头存储的即为当前的最小值

(可仔细看下方实例加深理解!!!)
在这里插入图片描述
求最大值同理。

完整代码

#include <iostream>
#include <algorithm>
using namespace std;
const int N=1000010;
int n,k;
int a[N];  //存储原数组
int q[N];  //数组模拟队列,存储数组下标
int main(){
    scanf("%d%d",&n,&k);  //n为数组长度,k为滑动窗口长度
    for(int i=0;i<n;i++) scanf("%d",&a[i]);
    int hh=0,tt=-1;
    //最小值
    for(int i=0;i<n;i++){
        //hh<=tt表明队列不为空
        
        //判断队头是否已经滑出窗口
        //窗口最前端的数组下标大于队首,则队头数出列
        if(hh<=tt&&i-k+1>q[hh]) hh++;  //i-k+1(当前数组下标i-窗口长度k+1)为当前窗口最前端的数组下标
        
        //判断队尾数组是否大于当前数,若大于,出现逆序,则队尾的数弹出tt--
        while(hh<=tt&&a[q[tt]]>=a[i]) tt--;
        q[++tt]=i;  //当前数入队
        
        //前k-1个数时,不输出答案
        if(i>=k-1) printf("%d ",a[q[hh]]);
    }
    puts("");
    
    //最大值
    hh=0,tt=-1;
    for(int i=0;i<n;i++){
        //判断队头是否已经滑出窗口
        //窗口最前端的数组下标大于队首,则队头数出列
        if(hh<=tt&&i-k+1>q[hh]) hh++;  //i-k+1(当前数组下标i-窗口长度k+1)为当前窗口最前端的数组下标
        
        //判断队尾数组是否小于当前数,若小于,出现逆序,则队尾的数弹出tt--
        while(hh<=tt&&a[q[tt]]<=a[i]) tt--;
        q[++tt]=i;  //当前数入队
        
        //前k-1个数时,不输出答案
        if(i>=k-1) printf("%d ",a[q[hh]]);
    }
    
}

回到原来的多重背包问题,借用单调队列/滑动窗口的思想。

完整代码

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=20010;
int n,m;
int f[N];  //状态计算
int g[N];  //动态数组,动态存储前一个状态
int q[N];  //数组模拟队列
int main(){
    cin>>n>>m;
    for(int i=0;i<n;i++){
        int v,w,s;
        cin>>v>>w>>s;  //体积,价值,数量
        //将上次计算的f赋给g
        memcpy(g,f,sizeof f);
        
        //遍历体积
        for(int j=0;j<v;j++){
            int hh=0,tt=-1;  //队头hh,队尾tt
            
            //滑动的一组数,j,j+v,j+2v···,每次右滑v,窗口长度为s*v
            for(int k=j;k<=m;k+=v){
                //判断队头是否已滑出窗口
                if(hh<=tt&&q[hh]<k-s*v) hh++;  //当前数k-窗口长度s*v =滑动窗口左端的数组下标
                //状态计算 f[i]=max(f[i],f[i-1]+相差的w)
                if(hh<=tt) f[k]=max(f[k],g[q[hh]]+(k-q[hh])/v*w);  //当前数下标k-队头存储数组下标可得前后状态二者相差的w的个数
                //判断当前数是否大于队尾
                //队尾的状态计算值为g[q[tt]],窗口中为不带w的计算,因此要减去对应数量的w
                //(w数量的计算为当前队尾存储的下标q[tt]-最前端下标j)除以v,即为二者相差的距离,就是对应相差的w个数
                //当前数的状态计算值为g[k],窗口中为不带w的计算,因此要减去对应数量的w
                //(w数量的计算为当前数的下标k-最前端下标j)除以v,即为二者相差的距离,就是对应相差的w的个数
                while(hh<=tt&&g[q[tt]]-(q[tt]-j)/v*w<=g[k]-(k-j)/v*w) tt--;
                //当前数入队
                q[++tt]=k;
            }
        }
    }
    cout<<f[m]<<endl;
    return 0;
}

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

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

相关文章

Golang 并发机制-5:详解syn包同步原语

并发性是现代软件开发的一个基本方面&#xff0c;Go&#xff08;也称为Golang&#xff09;为并发编程提供了一组健壮的工具。Go语言中用于管理并发性的重要包之一是“sync”包。在本文中&#xff0c;我们将概述“sync”包&#xff0c;并深入研究其最重要的同步原语之一&#xf…

排序算法与查找算法

1.十大经典排序算法 我们希望数据以一种有序的形式组织起来&#xff0c;无序的数据我们要尽量将其变得有序 一般说来有10种比较经典的排序算法 简单记忆为Miss D----D小姐 时间复杂度 &#xff1a;红色<绿色<蓝色 空间复杂度&#xff1a;圆越大越占空间 稳定性&…

数据结构课程设计(三)构建决策树

3 决策树 3.1 需求规格说明 【问题描述】 ID3算法是一种贪心算法&#xff0c;用来构造决策树。ID3算法起源于概念学习系统&#xff08;CLS&#xff09;&#xff0c;以信息熵的下降速度为选取测试属性的标准&#xff0c;即在每个节点选取还尚未被用来划分的具有最高信息增益的…

python-leetcode-二叉树的层序遍历

102. 二叉树的层序遍历 - 力扣&#xff08;LeetCode&#xff09; # Definition for a binary tree node. # class TreeNode: # def __init__(self, val0, leftNone, rightNone): # self.val val # self.left left # self.right right from coll…

毕业设计:基于深度学习的高压线周边障碍物自动识别与监测系统

目录 前言 课题背景和意义 实现技术思路 一、算法理论基础 1.1 卷积神经网络 1.2 目标检测算法 1.3 注意力机制 二、 数据集 2.1 数据采集 2.2 数据标注 三、实验及结果分析 3.1 实验环境搭建 3.2 模型训练 3.2 结果分析 最后 前言 &#x1f4c5;大四是整个大学…

【Hadoop】Hadoop的HDFS

这里写目录标题 HDFS概述HDFS产出背景及定义HDFS产生背景HDFS定义 HDFS优缺点HDFS优点HDFS缺点 HDFS组成架构HDFS文件块大小 HDFS的Shell操作常用命令实操准备工作上传下载HDFS直接操作 HDFS的API操作客户端环境准备HDFS的API案例实操HDFS文件上传HDFS文件下载HDFS文件更名和移…

C++ Primer 迭代器

欢迎阅读我的 【CPrimer】专栏 专栏简介&#xff1a;本专栏主要面向C初学者&#xff0c;解释C的一些基本概念和基础语言特性&#xff0c;涉及C标准库的用法&#xff0c;面向对象特性&#xff0c;泛型特性高级用法。通过使用标准库中定义的抽象设施&#xff0c;使你更加适应高级…

简单介绍一下什么是OpenFeign

OpenFeign是什么&#xff1f; OpenFeign是一个声明式的Http客户端&#xff0c;它可以用来发起Http请求 它主要用于SpringCloud微服务之间的通讯&#xff0c;让调用另一个服务的Java方法和调用本地方法一样快速和便捷 之前我们是用RestTemplate写一大堆东西发起Http请求远程调…

力扣动态规划-20【算法学习day.114】

前言 ###我做这类文章一个重要的目的还是记录自己的学习过程&#xff0c;我的解析也不会做的非常详细&#xff0c;只会提供思路和一些关键点&#xff0c;力扣上的大佬们的题解质量是非常非常高滴&#xff01;&#xff01;&#xff01; 习题 1.网格中的最小路径代价 题目链接…

Codeforces Round 1002 (Div. 2)(部分题解)

补题链接 A. Milya and Two Arrays 思路&#xff1a;题意还是比较好理解&#xff0c;分析的话我加了一点猜的成分&#xff0c;对a&#xff0c;b数组的种类和相加小于4就不行&#xff0c;蒋老师的乘完后小于等于2也合理。 AC代码&#xff1a; #include <bits/stdc.h> u…

在线销售数据集分析:基于Python的RFM数据分析方法实操训练

一、前言 个人练习&#xff0c;文章用于记录自己的学习练习过程&#xff0c;分享出来和大家一起学习。 数据集&#xff1a;在线销售数据集 分析方法&#xff1a;RFM分析方法 二、过程 1.1 库的导入与一些必要的初始设置 import pandas as pd import datetime import matplo…

小程序设计和开发:要如何明确目标和探索用户需求?

一、明确小程序的目标 确定业务目标 首先&#xff0c;需要明确小程序所服务的业务领域和目标。例如&#xff0c;是一个电商小程序&#xff0c;旨在促进商品销售&#xff1b;还是一个服务预约小程序&#xff0c;方便用户预订各类服务。明确业务目标有助于确定小程序的核心功能和…

【C语言深入探索】:指针高级应用与极致技巧(二)

目录 一、指针与数组 1.1. 数组指针 1.2. 指向多维数组的指针 1.2.1. 指向多维数组元素的指针 1.2.2. 指向多维数组行的指针 1.3. 动态分配多维数组 1.4. 小结 二、指针与字符串 2.1. 字符串表示 2.2. 字符串处理函数 2.3. 代码示例 2.4. 注意事项 三、指针与文件…

手写MVVM框架-构建虚拟dom树

MVVM的核心之一就是虚拟dom树&#xff0c;我们这一章节就先构建一个虚拟dom树 首先我们需要创建一个VNode的类 // 当前类的位置是src/vnode/index.js export default class VNode{constructor(tag, // 标签名称&#xff08;英文大写&#xff09;ele, // 对应真实节点children,…

【Blazor学习笔记】.NET Blazor学习笔记

我是大标题 我学习Blazor的顺序是基于Blazor University&#xff0c;然后实际内容不完全基于它&#xff0c;因为它的例子还是基于.NET Core 3.1做的&#xff0c;距离现在很遥远了。 截至本文撰写的时间&#xff0c;2025年&#xff0c;最新的.NET是.NET9了都&#xff0c;可能1…

C++11中的bind

官方文档对于bind接口的概述解释&#xff1a;Bind function arguments 在C11中&#xff0c;std::bind 是一个非常有用的工具&#xff0c;用于将函数、成员函数或函数对象与特定的参数绑定在一起&#xff0c;生成一个新的可调用对象。std::bind 可以用于部分应用函数参数、改变…

鼠标拖尾特效

文章目录 鼠标拖尾特效一、引言二、实现原理1、监听鼠标移动事件2、生成拖尾元素3、控制元素生命周期 三、代码实现四、使用示例五、总结 鼠标拖尾特效 一、引言 鼠标拖尾特效是一种非常酷炫的前端交互效果&#xff0c;能够为网页增添独特的视觉体验。它通常通过JavaScript和C…

金山打字游戏2010绿色版,Win7-11可用DxWnd完美运行

金山打字游戏2010绿色版&#xff0c;Win7-11可用DxWnd完美运行 链接&#xff1a;https://pan.xunlei.com/s/VOIAYCzmkbDfdASGJa_uLjquA1?pwd67vw# 进入游戏后&#xff0c;如果输入不了英文字母&#xff08;很可能是中文输入状态&#xff09;&#xff0c;就按一下“Shift”键…

爬虫学习笔记之Robots协议相关整理

定义 Robots协议也称作爬虫协议、机器人协议&#xff0c;全名为网络爬虫排除标准&#xff0c;用来告诉爬虫和搜索引擎哪些页面可以爬取、哪些不可以。它通常是一个叫做robots.txt的文本文件&#xff0c;一般放在网站的根目录下。 robots.txt文件的样例 对有所爬虫均生效&#…

(10) 如何获取 linux 系统上的 TCP 、 UDP 套接字的收发缓存的默认大小,以及代码范例

&#xff08;1&#xff09; 先介绍下后面的代码里要用到的基础函数&#xff1a; 以及&#xff1a; &#xff08;2&#xff09; 接着给出现代版的 读写 socket 参数的系统函数 &#xff1a; 以及&#xff1a; &#xff08;3&#xff09; 给出 一言的 范例代码&#xff0c;获取…