[模板] 多重背包

2 篇文章 0 订阅
2 篇文章 0 订阅

DP 背包类型

在开始多重背包以前,先要了解各种背包的类型.
总体抽象模型概述:
有一"容量 V V V",现有 a 1 , a 2 ⋯ a n {a_1,a_2\cdots a_n} a1,a2an .有以下三种信息:
"体积 " : w 1 , w 2 ⋯ w n {w_1,w_2\cdots w_n} w1,w2wn
"价值 " : v 1 , v 2 ⋯ v n {v_1,v_2\cdots v_n} v1,v2vn
元素数量数 : c 1 , c 2 ⋯ c n {c_1,c_2\cdots c_n} c1,c2cn
通俗来讲,就是一个容量为 V V V的背包,有 n n n种物品,各有其价值,体积,数量.
要求:

  • ∑ i = 1 n w i ⩽ V \sum_{i=1}^nw_i\leqslant V i=1nwiV
  • m a x ∑ i = 1 n k i v i ( 0 ⩽ k i ⩽ c i ) max{\sum_{i=1}^nk_iv_i(0\leqslant k_i \leqslant c_i)} maxi=1nkivi(0kici)

下文是背包的各种具体形式:

  • 0 − 1 0-1 01背包: c = 1 c=1 c=1.
    即所有物品只有一个(选/不选).
  • 完全背包: c = ∞ c= \infty c=.
    即所有物品有无限个.
  • 多重背包: c i = k i c_i=k_i ci=ki.
    即所有物品有 x x x个( 0 ⩽ x ⩽ k 0\leqslant x\leqslant k 0xk)

还有分组背包,二维(多维)背包等多种变种背包.
其中多重背包问题是最难的常见背包.

朴素算法

在之前已经介绍过 0 − 1 0-1 01背包的计算法:

int dp[N][M];
int main()
{
	for (int i=1;i<=n;i++)
		for (int j=0;j<=m;j++)
			if (j>=w[i]) dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
			else dp[i][j]=dp[i-1][j];
	printf("%d\n",dp[n][m]);
}

所以不难以想到多重背包的计算法则:

int dp[N][M];
int main()
{
	for (int i=1;i<=n;i++)
		for (int k=1;k<=c[i];k++)
			for (int j=0;j<=m;j++)
				if (j>=w[i]) dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
				else dp[i][j]=dp[i-1][j];
	printf("%d\n",dp[n][m]);
}

这样的复杂度是 O ( n m K ) O(nmK) O(nmK),这是不能被接受的.
所以,我们需要优化.

优化

这里主要介绍两种优化.

二进制优化

所谓二进制优化,就是利用了二进制的性质.
其实利用了数据的无序性.
也就是说,使用第一、三、四个物品和第二、八、二十四个物品没有区别.
所以可以比较自然地想到用二进制位的代表性数字.
类似于压位,只要把所有二进制位变成一个物品.
这样,第一个物品代表1个物品,第二个物品代表2个物品…
复杂度降到了 O ( n m l o g K ) O(nmlogK) O(nmlogK),并且常数很小,满足我们的需求了(?).

 for (int i=1;i<=n;i++)
    {
        int v=a[i].cnt,tmp=0;
        for (int j=1;j<=v;v-=j,j*=2)
            x[++tmp]=j*a[i].w,y[tmp]=j*a[i].val;//用新"物品"代替旧物品
        if (v>0) x[++tmp]=v*a[i].w,y[tmp]=v*a[i].val;
        for (int j=1;j<=tmp;j++)
            for (int k=V;k>=x[j];k--)
                dp[k]=max(dp[k],dp[k-x[j]]+y[j]);//按原方法进行BF算法
    }

能做到这个程度,已经很不容易.
但目前最优的单调队列算法,已经能将这个多重背包优化到 O ( n m ) O(nm) O(nm)的复杂度.

单调队列算法

单调队列算法,进一步利用的数据的无序性.
我们先作一个铺垫.
现有数据,其体积,价值都相等.
如果原状态为 d p [ i − 1 ] [ p ] dp[i-1][p] dp[i1][p].
那么由这个状态所推出的"最佳状态"只能在 d p [ i ] [ p + k ∗ w i ] + k ∗ v i dp[i][p+k*w_i]+k*v_i dp[i][p+kwi]+kvi中拓展.
换句话说,这些状态扩展的第二维
都属于一个同余类
.
所以我们可以换用这样的遍历方式:

for (int j=0;j<a[i].w;j++)
    for (int k=0;j+k*a[i].w<=V;k++)

这样也将 1.. n 1..n 1..n遍历了一遍,只不过更具有特殊性.
下面我们可以进入真正的精华部分——单调队列.
考虑刚才的 d p [ i ] [ p + k ∗ w i ] + k ∗ v i dp[i][p+k*w_i]+k*v_i dp[i][p+kwi]+kvi.
可以表示为 d p [ i − 1 ] [ p + ( k − 1 ) ∗ w i + w i ] + ( k − 1 ) ∗ v i + v i dp[i-1][p+(k-1)*w_i+w_i]+(k-1)*v_i+v_i dp[i1][p+(k1)wi+wi]+(k1)vi+vi
拓展一波之后可得:
d p [ i ] [ p + u ∗ w i ] = d p [ i − 1 ] [ p + k ∗ w i ] − k ∗ v i + u ∗ v i ( u − c n t i ⩽ k ⩽ u − 1 ) dp[i][p+u*w_i]=dp[i-1][p+k*w_i]-k*v_i+u*v_i\\(u-cnt_i \leqslant k \leqslant u-1) dp[i][p+uwi]=dp[i1][p+kwi]kvi+uvi(ucntiku1)
这里就很玄妙了嘿嘿嘿.
回顾单调队列的问题,有比较典型的滑动窗口问题.
在这里,可以视前面的 d p [ i − 1 ] [ p + k ∗ w i ] − k ∗ v i dp[i-1][p+k*w_i]-k*v_i dp[i1][p+kwi]kvi为定值.(在此设为 x k x_k xk)
则将所有 x k x_k xk增序推入队列,按照滑动窗口做就行了.

优秀!
(独秀你坐下)

总体复杂度,因为只遍历所有元素一遍,所以是 O ( n m ) O(nm) O(nm)

for (int i=1;i<=n;i++)
    {//q 为元素名
        for (int j=0;j<a[i].w;j++)
        {
            int L=0,R=-1;
            for (int k=0;j+k*a[i].w<=V;k++)
            {
                int p=j+k*a[i].w;
                q tmp;
                tmp.cnt=k,tmp.val=dp[j+k*a[i].w]-k*a[i].val;
                while (L<=R&&tmp.val>Q[R].val) R--;
                while (L<=R&&k-Q[L].cnt>a[i].cnt) L++;
                Q[++R]=tmp;//套用单调队列
                dp[p]=max(Q[L].val+k*a[i].val,dp[p]);
            }
        }
    }

这个算法理论上比二进制优秀,但是由于…各种常数原因,加上二进制代码易懂易写,所以还是因地制宜,选择恰当的才好.

后记

由于这几天较忙,所以写得慢.
希望各位奆老谅解! qwq ?

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值