多重背包-暴力法+ 单调队列优化法

本文详细介绍了多重背包问题的暴力法和单调队列优化法。暴力法中,通过三层循环遍历物品、体积和选法,但时间复杂度过高。优化法利用单调队列,减少时间复杂度,通过滚动数组和滑动窗口更新状态,同时维护单调队列确保效率。代码实现中展示了如何应用这些思想解决完全背包问题。
摘要由CSDN通过智能技术生成

多重背包-暴力法+ 单调队列优化法

暴力法

问题:一共 n 类物品,背包的容量是 m 每类物品的体积为v, 价值为w,个数为s,求背包内价值的最大值。

数据范围:
0 < N ≤ 1000 0 < V ≤ 20000 0 < v i , w i , s i ≤ 20000 0 < N \le 1000\\ 0 < V \le 20000\\ 0 < v_i,w_i,s_i \le 20000 0<N10000<V200000<vi,wi,si20000
思路:因为每个物品的选几个受当前背包容量的影响,如果选的话还会改变背包剩下的容量,然后进一步影响后面的物品的选择,所以将背包的体积作为状态,(这是我自己为什么选这个作为状态的原因,,,)

那么定义 d p [ i ] [ j ] dp[i][j] dp[i][j]为从前i个物品中,且体积不超过 j 的选法的集合。

那么先考虑第 i 个物品选多少个,那么就有下面的方程 (v为当前物品的体积,w为当前物品的价值 s i s_i si为最多取几个)

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v ] + w , d p [ i − 1 ] [ j − 2 ∗ v ] + 2 ∗ w , . . . , d p [ i − 1 ] [ j − s i ∗ v ] + s i ∗ w ) dp[i][j] = max(dp[i - 1][j],dp[i-1][j - v] + w,dp[i-1][j -2 * v] + 2*w,...,dp[i-1][j-s_i*v]+s_i*w) dp[i][j]=max(dp[i1][j],dp[i1][jv]+w,dp[i1][j2v]+2w,...,dp[i1][jsiv]+siw)

然后我们发现求 d p [ i ] [ j ] dp[i][j] dp[i][j]的时候涉及到的都是 i − 1 i-1 i1 j j j前面的数据,所以如果我们倒叙遍历 j j j就不需要第一维度了。

通过第一层for遍历物品,第二层for遍历体积,第三层遍历选法就可以得到结果了,但这样做的时间复杂度是时间复杂度是 O ( 物 品 数 ∗ 背 包 的 体 积 ∗ 选 法 ) O(物品数*背包的体积*选法) O() = O ( 1000 ∗ 20000 ∗ 20000 ) O(1000 * 20000 * 20000) O(10002000020000)显然会超时,只不过这里先不管。

下面是省掉一维时候的代码:

#include <stdio.h>
#include <algorithm>
using namespace std;
const int N = 6005;
int dp[N], n, m;
int main()
{
    scanf("%d%d", &n, &m);
	//遍历物品
    while (n--)
    {
        int v, w, s;
        scanf("%d%d%d", &v, &w, &s);
        //遍历体积
        for (int i = m; i >= v; i--)
            //遍历选法
            for (int j = 0; j <= s && v * j <= i; j++)
                dp[i] = max(dp[i], dp[i - j * v] + j * w);
    }
    printf("%d", dp[m]);
    return 0;
}

对应习题:Acwing 1019庆功宴(这道题直接暴力就可以了)

单调队列优化法

因为上面暴力必然超时,所以我们这里要寻找哪里可以优化,先看下最原始的方程

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v ] + w , d p [ i − 1 ] [ j − 2 ∗ v ] + 2 ∗ w , . . . , d p [ i − 1 ] [ j − s i ∗ v ] + s i ∗ w ) dp[i][j] = max(dp[i - 1][j],dp[i-1][j - v] + w,dp[i-1][j -2 * v] + 2*w,...,dp[i-1][j-s_i*v]+s_i*w) dp[i][j]=max(dp[i1][j],dp[i1][jv]+w,dp[i1][j2v]+2w,...,dp[i1][jsiv]+siw)

我们可以发现该方程的第二维是 j − v , j − 2 ∗ v , j − 3 ∗ v , j − s i ∗ v j-v,j-2*v,j-3*v,j-s_i*v jv,j2v,j3v,jsiv,因为受到完全背包的感染,所以写以下 d p [ i ] [ j − v ] dp[i][j-v] dp[i][jv]的方程来对比看一下。
d p [ i ] [ j − v ] = m a x ( d p [ i − 1 ] [ j − v ] , d p [ i − 1 ] [ j − 2 ∗ v ] + w , . . . , d p [ i − 1 ] [ j − s i ∗ v ] + ( s i − 1 ) ∗ w ) + d p [ i − 1 ] [ j − ( s i + 1 ) v + w ∗ s i ] dp[i][j - v] =max(dp[i-1][j - v],dp[i-1][j -2 * v]+w\\ ,...,dp[i-1][j-s_i*v]+(s_i-1)*w) + dp[i-1][j-(s_i+1)v + w*s_i] dp[i][jv]=max(dp[i1][jv],dp[i1][j2v]+w,...,dp[i1][jsiv]+(si1)w)+dp[i1][j(si+1)v+wsi]

在这里插入图片描述

从图中可以看出来,一开始是匹配的,但是最后一个不匹配,也就是$ j 没 有 没有 j-1$的最后一项,所以
m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v ] + w , d p [ i − 1 ] [ j − 2 ∗ v ] + 2 ∗ w , . . . , d p [ i − 1 ] [ j − s i ∗ v ] + s i ∗ w ) ≠ m a x ( d p [ i − 1 ] [ j − v ] , d p [ i ] [ j − v ] ) max(dp[i - 1][j],dp[i-1][j - v] + w,dp[i-1][j -2 * v] + 2*w,...,dp[i-1][j-s_i*v]+s_i*w)\\ \not=\\ max(dp[i-1][j - v],dp[i][j - v]) max(dp[i1][j],dp[i1][jv]+w,dp[i1][j2v]+2w,...,dp[i1][jsiv]+siw)=max(dp[i1][jv],dp[i][jv])
虽然猜想不对,但是我们发现了 d p [ i ] [ j ] 对 比 d p [ i ] [ j − v ] dp[i][j]对比dp[i][j-v] dp[i][j]dp[i][jv]的方程中间有一大部分是匹配的,而且是后面少一项和前面多一项的关系,想利用这一部分,就想到了一个非常匹配这个过程的方法—滑动窗格。

为了方便,我们这里使用滚动数组,将 d p [ i − 1 ] [ j ] dp[i -1][j] dp[i1][j]的值全部存在 p r e pre pre数组里面。 p r e [ j ] = d p [ i − 1 ] [ j ] pre[j] = dp[i-1][j] pre[j]=dp[i1][j]

p r e [ j ] pre[j] pre[j]的含义是从前 i − 1 i-1 i1个物品中选,且体积不超过 j j j的最大价值。

那么假设 s i s_i si==3, d p [ i ] [ j ] = m a x ( p r e [ j ] , p r e [ j − v ] + w , p r e [ j − 2 ∗ v ] + 2 ∗ w , p r e [ j − 3 ∗ v ] + 3 ∗ w ) dp[i][j] = max(pre[j],pre[j-v]+w,pre[j-2*v]+2*w,pre[j-3*v]+3*w) dp[i][j]=max(pre[j],pre[jv]+w,pre[j2v]+2w,pre[j3v]+3w)

在这里插入图片描述

可以看出来,当前的窗格需要从前面往后推,然后边推还可以边把 d p [ i ] [ j − v ] , d p [ i ] [ j − ? ∗ v ] dp[i][j - v],dp[i][j-?*v] dp[i][jv],dp[i][j?v]全给赋值了,然后可以发现以下两点

  • 不能像暴力法那样,直接丢掉一维,不然你求 j − 5 ∗ v j-5*v j5v的时候,要用到 j − 6 ∗ v j-6*v j6v已经被覆盖掉了,所以用一个滚动数组pre来存储

  • 因为前往后推,所以我们先要找到起点的体积比如说上图就是 j − 6 ∗ v , 假 设 0 < j − 6 ∗ v < v j-6*v,假设0<j-6*v<v j6v,0<j6v<v

起点有0 到 v − 1 v-1 v1种可能,那么我们这里就可以换一种写法了比如说
假 设 j − 6 ∗ v = 2 , 0 < 2 < v 那 么 j − 5 ∗ v = 2 + v ,    j − 4 v = 2 + 2 ∗ v . . . j = 2 + 6 ∗ v 去 掉 等 号 一 边 就 是 2 , 2 + v , 2 + 2 ∗ v , . . . , 2 + 6 ∗ v 那 么 m a x ( p r e [ j ] , p r e [ j − v ] + w , p r e [ j − 2 ∗ v ] + 2 ∗ w , p r e [ j − 3 ∗ v ] + 3 ∗ w ) 变 成 m a x ( p r e [ 2 + 6 ∗ v ] , p r e [ 2 + 5 ∗ v ] + w , p r e [ 2 + 4 ∗ v ] + 2 ∗ w , p r e [ 2 + 3 ∗ v ] + 3 ∗ w ) 假设j-6*v = 2,0<2<v\\ 那么j - 5*v = 2+v,\;j-4v=2+2*v...j = 2 + 6*v\\ 去掉等号一边就是 2,2+v,2+2*v,...,2+6*v\\ 那么max(pre[j],pre[j-v]+w,pre[j-2*v]+2*w,pre[j-3*v]+3*w)\\ 变成max(pre[2 + 6*v],pre[2 + 5*v]+w,pre[2 + 4*v]+2*w,pre[2 + 3*v]+3*w) j6v=2,0<2<vj5v=2+v,j4v=2+2v...j=2+6v2,2+v,2+2v,...,2+6vmax(pre[j],pre[jv]+w,pre[j2v]+2w,pre[j3v]+3w)max(pre[2+6v],pre[2+5v]+w,pre[2+4v]+2w,pre[2+3v]+3w)
上面是具体的写法,下面将其抽象化: 总 体 积 m = k ∗ v + j , 0 < j < v 总体积m = k*v+j,0<j<v m=kv+j0<j<v,先不管后面加的部分
p r e [ 2 ] , p r e [ 2 + v ] , p r e [ 2 + 2 ∗ v ] , p r e [ 2 + 3 ∗ v ] , . . . , p r e [ k ∗ v + 2 ] pre[2],pre[2+v],pre[2 + 2 *v],pre[2 + 3*v],...,pre[k*v+2] pre[2],pre[2+v],pre[2+2v],pre[2+3v],...,pre[kv+2]
写到这里,我们发现,其实我们枚举 0 < j < v 0<j<v 0<j<v然后一直推到这个 j j j对应的结尾就可以完成这个物品的更新了。这里补全一下抽象化后的整个表示
p r e [ 0 ] , p r e [ v ] , p r e [ 2 ∗ v ] , p r e [ 3 ∗ v ] , . . . , p r e [ k ∗ v ] p r e [ 1 ] , p r e [ v + 1 ] , p r e [ 2 ∗ v + 1 ] , p r e [ 3 ∗ v + 1 ] , . . . , p r e [ k ∗ v + 1 ] p r e [ 2 ] , p r e [ v + 2 ] , p r e [ 2 ∗ v + 2 ] , p r e [ 3 ∗ v + 2 ] , . . . , p r e [ k ∗ v + 2 ] p r e [ 3 ] , p r e [ v + 3 ] , p r e [ 2 ∗ v + 3 ] , p r e [ 3 ∗ v + 3 ] , . . . , p r e [ k ∗ v + 3 ] . . . p r e [ j ] , p r e [ v + j ] , p r e [ 2 ∗ v + j ] , p r e [ 3 ∗ v + j ] , . . . , p r e [ k ∗ v + j ] 每 一 行 滑 动 过 去 , 比 如 s = 3 的 话 , 一 开 始 是 j , v + j , 2 ∗ v + j 下 一 个 就 是 v + j , 2 ∗ v + j , 3 ∗ v + j 这 样 边 滑 边 更 新 pre[0] , pre[v], pre[2*v], pre[3*v], ... , pre[k*v]\\ pre[1], pre[v+1], pre[2*v+1], pre[3*v+1], ... , pre[k*v+1]\\ pre[2], pre[v+2], pre[2*v+2], pre[3*v+2], ... , pre[k*v+2]\\ pre[3], pre[v+3], pre[2*v+3], pre[3*v+3], ... , pre[k*v+3]\\ ...\\ pre[j], pre[v+j], pre[2*v+j], pre[3*v+j], ... , pre[k*v+j]\\ 每一行滑动过去,比如s = 3的话,一开始是j,v+j,2*v+j\\下一个就是v+j,2*v+j,3*v+j这样边滑边更新 pre[0],pre[v],pre[2v],pre[3v],...,pre[kv]pre[1],pre[v+1],pre[2v+1],pre[3v+1],...,pre[kv+1]pre[2],pre[v+2],pre[2v+2],pre[3v+2],...,pre[kv+2]pre[3],pre[v+3],pre[2v+3],pre[3v+3],...,pre[kv+3]...pre[j],pre[v+j],pre[2v+j],pre[3v+j],...,pre[kv+j]s=3,j,v+j,2v+jv+j,2v+j,3v+j
然后现在知道怎么更新,还知道怎么全部都更新,剩下的就是怎么快速的找到滑动窗格的最大值,那么单调队列登场,因为直接用deque会超时,所以这里用数组模拟整个过程,有点小复杂?只不过我会尽力讲清楚的。

另起一个数组为q[N],里面存的是背包的体积,设置队列头为head,尾巴为tail q[头, , , ,…,尾巴],保证队列里面是单调的,也就是head代表的体积的价值>tail代表的体积的价值。

现在问题来到怎么维护单调队列的单调性?

当前的背包体积k,因为要单调队列的单调性,所以要找到一个合适的位置讲当前背包放入队列中,这个合适的位置的体积的最大价值大于体积k的最大价值才行, p r e [ q [ t a i l ] ] pre[q[tail]] pre[q[tail]]取得的就是当前尾部体积的价值相当于 d p [ i − 1 ] [ q [ t a i l ] ] dp[i - 1][q[tail]] dp[i1][q[tail]]和当前的价值 d p [ i − 1 ] [ k ] dp[i-1][k] dp[i1][k]比较,有没有发现什么不对劲?

p r e [ j ] pre[j] pre[j]的含义是从前 i − 1 i-1 i1个物品中选,且体积不超过 j j j的最大价值。 p r e [ h ] = d p [ i − 1 ] [ h ] pre[h] = dp[i-1][h] pre[h]=dp[i1][h]

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v ] + w , . . . , d p [ i − 1 ] [ j − s i ∗ v ] + s i ∗ w ) dp[i][j] = max(dp[i - 1][j],dp[i-1][j - v] + w,...,dp[i-1][j-s_i*v]+s_i*w) dp[i][j]=max(dp[i1][j],dp[i1][jv]+w,...,dp[i1][jsiv]+siw)

看眼原始方程,回忆下变量的含义。没错漏掉了 k − q [ t a i l ] k - q[tail] kq[tail]这部分给 d p [ i − 1 ] [ q [ t a i l ] ] dp[i - 1][q[tail]] dp[i1][q[tail]]带来的价值,所以比较的时候要给加回去继续比较,那么就可以写出这部分的代码了

可以对比起来看一下
d p [ i − 1 ] [ j − ? ∗ v ] + ? ∗ w p r e [ q [ t a i l ] ] + ( k − q [ t a i l ] ) / v ∗ w dp[i-1][j - ?*v] + ?*w\\ pre[q[tail]] + (k - q[tail]) / v * w dp[i1][j?v]+?wpre[q[tail]]+(kq[tail])/vw

while (head <= tail && pre[q[tail]] + (k - q[tail]) / v * w <= pre[k])
             tail--;

接下来直接看完整代码就可以了:

#include <stdio.h>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 20005;
// pre存储i - 1的时候的值
// q为偏移量为?时候的单调队列
int dp[N], pre[N], q[N], n, m;
int main()
{
    //n为物品的总数,背包容量为m
    scanf("%d%d", &n, &m);
    while (n--)
    {
        int v, w, s;
        scanf("%d%d%d", &v, &w, &s);

        //赋值上一层结果
        memcpy(pre, dp, sizeof(dp));

        //枚举偏移量(起点)
        for (int i = 0; i < v; i++)
        {
            //建新的单调队列 头>>>尾巴,里面存的是体积
            int head = 0, tail = -1;

            //更新该起点代表的行,k是此时的体积
            for (int k = i; k <= m; k += v)
            {
                //判断窗格是否超过了k,头需要往后滑动
                if (head <= tail && k - q[head] > s * v)
                    head++;

                //赋值
                //pre[q[head]]+(k - q[head]) / v * w):前i-1个中选,且体积不大于q[head]的最大价值 + 第i个物品选(k - q[head]) / v 个的价值
                if (head <= tail)
                    dp[k] = max(dp[k], pre[q[head]] + (k - q[head]) / v * w);

                //队列中加入k,并且维护队列单调性
                //pre[k]:前i-1个中选,且体积不大于q[head]的最大价值 + 第i个物品选0个
                //pre[q[tail]] + (k - q[tail]) / v * w :前i-1个中选,且体积不大于q[tail]的最大价值 + 第i个物品选(k - q[tail]) / v个
                while (head <= tail && pre[q[tail]] + (k - q[tail]) / v * w < pre[k])
                    tail--;
                q[++tail] = k;
            }
        }
    }
    printf("%d", dp[m]);
    return 0;
}

习题:AcWing 6. 多重背包问题 III

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值