本文始发于个人公众号:TechFlow,原创不易,求个关注
今天是算法与数据结构的第14篇文章,也是动态规划专题的第三篇。
在之前的文章当中,我们介绍了多重背包的二进制拆分的解法。在大多数情况下,这种解法已经足够了,但是如果碰到极端的出题人可能还是会被卡时间。这个时候只能用更加快速的方法,也就是今天我们要一起来看的单调优化。
单调优化是单调队列优化的简称,单调栈我们在之前的LeetCode专题已经介绍过了。它的本质只是一个简单的栈,通过在插入元素时候对栈顶的部分元素进行弹出,从而保证了栈内元素的有序。而单调队列也类似,只是插入元素的位置不同而已。栈是只能从栈顶插入,队列则是从队尾插入。
比如我们看下下图,下图就是一个典型的单调队列,队列中的元素是[10, 6, 3]。队首是10,队列当中的元素从队首开始往队尾递减。
如果这个时候我们从队尾插入9,由于9大于队尾的3,所以3出队列,我们继续判断,发现9依然大于6,所以6再次出队列。最后得到的结果是[10, 9]。准确的说,由于我们进出队列的操作可以同时在队首或者队尾进行,所以严格说起来这并不是普通的队列,而是一个双端队列。
单调队列或者说单调栈的最大用处是由于容器内元素递增或者递减,所以栈顶或者是队首的元素就是最值。我们通过使用单调栈可以在常数时间内获得某一个区间内的若干个最值,在一些问题当中只获得一次最值还是不够的。因为种种条件的限制,所以可能使得最值不一定能够成立,这个时候需要求第二最值或者是第三最值,在这种问题下, 使用单调栈或者是单调队列就是非常有必要的了。
比如今天要讨论的多重背包问题,就是这样的情况。
基础分析
我们先把单调队列的事情先放在一边,先来仔细分析一下题目。
在之前的文章当中,我们曾经讨论过动态规划算法的复杂度问题。对于动态规划算法而言,我们要做的是遍历所有的决策,以及决策可以应用的状态,找到每个状态最佳的转移,记录这些最好的转移结果。在所有的结果当中的最值,就是我们要找的整个问题的答案。那么,我们可以很方便地推导出动态规划的复杂度,等于状态数乘上决策数。
我们记住这个简单的结论,它可以帮助我们很方便地分析动态规划算法的复杂度,尤其在一些通过传统方法不方便分析的时候。
我们把复杂度结论带入多重背包问题,对于多重背包问题来说,我们的决策是由两个条件决定的。其中一个是物品,另一个是这个物品的数量。所以决策的数量等于物品数乘上物品的个数,状态是背包的容量。我们假设物品的数量是N,物品的个数为M,背包的容量是V,那么它的复杂度就是 O ( N M V ) O(NMV) O(NMV)。显然,在绝大多数情况下,这个复杂度是我们不能接受的,也是我们需要引入种种优化的原因。
在之前的文章当中,我们通过二进制表示法,将物品的数量拆分成了若干个2次幂的和。所以对于二进制表示法而言,它的复杂度是 O ( N l o g M V ) O(NlogMV) O(NlogMV)。我们通过二进制表示将M这一维降到了 l o g M logM