码迷,mamicode.com
首页 > 其他好文 > 详细

再谈单调队列优化 & 背包九讲

时间:2020-07-12 22:20:57      阅读:60      评论:0      收藏:0      [点我收藏+]

标签:先来   pre   限制   如何   tps   数据   复杂度   不能   目标   

CSDN同步

前置知识:

这篇文章我们主要研究 单调队列优化 \(\text{dp}\) 如何用于背包问题

\(\text{01}\) 背包

\(n\) 个物品,背包体积为 \(V\),每个物品有 \(v_i\)(价值)和 \(w_i\)(重量),每个物品只有 \(1\) 个。求在不超过背包体积的情况下能获得的最大价值。
\(n,V,v_i,w_i \leq 1 \times 10^3\),时间限制 \(1s\),空间限制 \(16MB\).

简单的考虑,用 \(f_{i,j}\) 表示前 \(i\) 个物品,背包体积为 \(j\) 的答案,则易得:

\[f_{i,j} = \max(f_{i-1,j} , f_{i-1 , j-w_i} + v_i) \]

这样可以在 \(\mathcal{O}(nV)\) 的时间内解决问题。

但是你发现空间只有 \(16 \text{MB}\),由于 \(i\) 的决策只取决于上一行同一列,所以可以直接降维,得:

\[f_j = \max(f_j , f_{j-w_i} + v_i) \]

注意 \(j \geq w_i\) 的隐约限制。

伪代码如下:

for (i=1;i<=n;i++)
for (j=V;j>=w[i];j--)
	f[j] = max(f[j] , f[j-w[i]] + v[i]);

\(\mathcal{O}(nV)\) 是最优的算法。今天我们所要实现的,仅仅是,把所有的背包算法复杂度都降为 \(\mathcal{O}(nV)\).

完全背包

\(n\) 个物品,背包体积为 \(V\),每个物品有 \(v_i\)(价值)和 \(w_i\)(重量),每个物品有无限个。求在不超过背包体积的情况下能获得的最大价值。
\(n,V,v_i,w_i \leq 1 \times 10^3\),时间限制 \(1s\),空间限制 \(16MB\).

这样你会发现一个问题,你不知道有多少个!

当然你可以用 \(\lfloor \frac{V}{v_i} \rfloor\) 来表示数量,考虑枚举。

同样的 \(f_{i,j}\),我们可以知道:

\[f_{i,j} = \max_{k=0}^{\lfloor \frac{V}{v_i} \rfloor}(f_{i-1,j-k \times w_i} + k \times v_i) \]

大前提是 \(j \geq k \times w_i\),这样转移的复杂度是 \(\mathcal{O}(nVw_i)\),降维之后应该可以通过。

考虑一个简单的加强:

\(n,V,v_i,w_i \leq 1 \times 10^4\),空间限制 \(128MB\).

我们将在下面的研究中,解决这个问题。

多重背包

\(n\) 个物品,背包体积为 \(V\),每个物品有 \(v_i\)(价值)和 \(w_i\)(重量),每个物品有 \(\text{num}_i\) 个。求在不超过背包体积的情况下能获得的最大价值。
\(n,V,v_i,w_i \leq 1 \times 10^4\),时间限制 \(1s\),空间限制 \(128MB\).

显然我们按照完全背包的方法就可以了,但是 \(\mathcal{O}(nVw_i)\) 是不可能通过 \(10^4\) 的!所以我们需要考虑,如何优化这个问题。

实际上不用单调队列也可以优化,我们先来讲著名的背包优化的几个方法。

二进制拆分

二进制!这是个熟悉的东西。

实际上我们背包的瓶颈在于两个:背包物品过多,背包容积过大,背包物品数量过多。

容积过大可能会想到离散,但是这不是排序之类只用知道大小的问题啊!容积是不能突破的,考虑优化物品个数。物品个数怎么可能优化呢?数量?

那你可能会说,这和二进制又有什么关系?

下面我们来说一说吧。

用二进制表示数

用集合 \(s\) 表示二的幂次集,则 \(s = \{ 2^0 , 2^1 \cdots 2^{\infty}\}\).每个正整数都可以用若干个 \(s\) 集合内的元素相加而成。就是说每个数一定能被分解成若干二的幂次的数之和。

\(10 = 2 +8\)
\(100 = 64 + 32 +4\)
\(1000 = 512 + 256 + 128 + 64 + 32 + 8\)

如何证明?

显而易见,每个数都能被二进制表示。比方说 \(10_{10} = 1001_2\).

那么,用计数原则,\(1001_2 = 1 \times 2^0 + 0 \times 2^1 + 0 \times 2^2 + 1 \times 2^3\)

这样就证明结束了,你没有发现吗?

拆分原理

将一个 \(\{ v_i , w_i , num_i\}\) 的物品,通过 \(num_i\) 进行拆分。

本来假设一个物品有 \(7\) 个,你可以把它拆开成 \(1,2,4\) 个,对这三个物品进行 \(01\) 背包,你就可以得到 \(1 - 7\) 所有可能的答案。最后你把不取的答案单独做一遍就可以了。

原理就是, \(n\) 最多被拆成 \(\log n\) 个二的幂次的数之和,这样保证了时间复杂度是 \(\mathcal{O}(nV \log w_i)\) 的。

伪代码
for(i=1;i<=n;i++) {
	read(x) , read(y) , read(z);
	for(j=1;j<=z;j<<=1) v[++cnt]=x*j,w[cnt]=y*j,z-=j;
	if(z) v[++cnt]=x*z,w[cnt]=y*z;
}
// use array v and w to do 01 backpack

单调队列

显然,\(10^4\) 的数据可以把二进制拆分卡死。我们需要严格去掉 \(\log\).

再看一眼这个状态转移:

\[f_{i,j} = \max_{k=0}^{\text{num}_i}(f_{i-1,j-k \times w_i} + k \times v_i) \]

你会发现,这不是一段连续的决策!这是断续的。

但是,你会发现这样一个问题。

\(f_{i-1 , j} , f_{i-1 , j - w_i} , f_{i-1 , j - 2 \times w_i} \cdots f_{i-1 , j - \text{num}_i \times w_i}\),这是所有 \(f_{i,j}\) 的决策点。

你会发现,这些决策点是同行等差列的,每两个决策点隔着一个 \(w_i\). 这非常好!

余数构造连续决策

你会发现,你可以把 \(V\) 按照模 \(w_i\) 进行分类,余数相同的肯定是一类的。

那样我们就可以把这一段当成连续的决策去做,因为实际上枚举余数 \(\text{mo}\) 和当前周期编号 \(k\). 因为 \(k\) 是连续的,那么 \(\text{mo} + k \times w_i\) 实际上就是当前的决策点。这样我们构造出了若干段连续的决策。

时间复杂度分析

\(n\) 个物品每次做一次,所以 \(\mathcal{O}(n)\).

然后同时枚举余数和 \(k\),因为 \(\text{mo} \times k \leq V\),所以本质上这两重循环的 \(\sum\) 不会超过 \(V\),是 \(\mathcal{O}(V)\).

按照单调队列每个节点进出一次的原理,\(\mathcal{O}(nV)\) 的目标已然达成。

for(i=1;i<=n;i++) {
		read(v) , read(w) , read(num); 
		num=min(num,V/v);
		for(mo=0;mo<v;mo++) {
			l=r=0;
			for(k=0;k<=(V-mo)/v;k++) {
				x=k,y=f[k*v+mo]-k*w;
				while(l<r && q[l].pos<k-num) l++;
				while(l<r && q[r-1].val<=y) r--;
				q[r].val=y,q[r++].pos=x;
				f[k*v+mo]=q[l].val+k*w;
			}
		}
	}

完全背包

这里我们发现,直接把 \(\lfloor \frac{V}{v_i} \rfloor\) 作为 \(\text{num}\) 就可以直接通过。这样我们实现了完全背包和多重背包的 \(\mathcal{O}(nV)\).

混合背包

这里博主咕咕咕了,因为时间太少了。

课后习题

\(\text{01}\) 背包

完全背包

多重背包

多重背包 \(1\)

多重背包 \(2\)

多重背包 \(3\)

参考资料

背包九讲

再谈单调队列优化 & 背包九讲

标签:先来   pre   限制   如何   tps   数据   复杂度   不能   目标   

原文地址:https://www.cnblogs.com/bifanwen/p/13290049.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!