单调性优化
【单调栈】
首先我们有一个最简单的方法:
求最小值,我们考虑到ST表,我们可以用它优化到
接下来我们考虑转换枚举思路:
我们发现,一个矩形的大小,由它的左右端点,和它中间的最低高度。
于是我们考虑枚举每一个矩形为最低高度,求这个矩形能往外“扩展”到哪里。
对于我们的矩形
这就可以用线段树维护,至少为x。
我们顺着上面思考,当一个矩形作为最低点,我们只需要考虑左右两边把这个矩形“拦”下来的地方。
所谓“拦”,就是第一个高度更低的地方。
我们观察到:如果一个矩形高度更低,位置更靠右,那么它就更可能把
换句话说,如果两个矩形
这个时候我们
(完爆更普遍的定义:如果
发现到:我们只需要存下所有没有被“完爆”的矩形即可。
按顺序从
我们观察到:这些不被完爆的矩形,高度一定是单调递增的。
因为如果有一个不递增了,它之前高度更高的都会被完爆,所以高度更高的一定不会被存下来。
现在我们知道了:我们需要保留所有不被完爆的矩形,并且这些矩形的高度是单调递增的。
接下来我们需要知道:如何保留所有不被完爆的矩形。
用一个类似数学归纳法的想法思考:
我们现在已经把
因为第
而
把这些被完爆的矩形去掉,然后加入
因为最末端的反而会先被去掉,所以我们考虑用栈维护。
现在我们知道了:如何保留所有不被完爆的矩形。
接下来我们需要知道:记录不完爆矩形和找左右端点有什么关系。
显然,当我们加入
同理,我们可以反方向再做一遍这个过程,就可以求出每个矩形右边拦下来的矩形。
【单调(双端)队列】
不妨以求最大值为例。
按照我们的“完爆”思想进行思考:
如果窗口内两个数
我们只需要在新来的加入时,把所有比新来的小的元素弹出即可。
因为新来的一定在窗口中活的更久,而且新来的更大,所以只要比新来的小,就被新来的完爆。
同理用数学归纳法可知:不完爆数一定是单调递减的。
所以我们考虑维护一个双端队列
(因为左边可能因为时间被弹出,右边的可能会被完爆弹出,要用双端队列)
首先先把
然后我们把所有比新来的小的数弹出,因为
注意:我们从窗口里装满了 k 个数时开始输出,注意判断队列是否为空、大小关系。
【应用单调队列优化——多重背包】
首先有一个朴素的思路:一重循环枚举每一类物品,一重循环枚举背包容积,一重循环枚举第
接下来我们考虑优化:
不妨
我们发现,第二维就是一个滑动窗口,每次往右滑
如果
但是,
这两个东西并不完全就是滑动窗口,但是我们发现一个特点:
只要
所以我们的
我们只需要枚举首项为
下面详细讨论如何求一个首项为
不如把我们要求的设为
而其中
所以我们的单调队列的判断条件是:若
把所有被完爆的弹出,然后加入
注意:单调队列里存的是 k,即物品
然后我们发现:队头的就是使得
也就是说,最优的
整理一下思路:
-
初值:因为我们的 dp 设的是 “恰好”,所以初值为
。 -
递推:
先一个循环
,枚举前 种物品。然后循环
,循环滑动窗口的首项。再循环
,取 个物品 。接下来我们要求所有形如
的值。(以下滑动窗口)若取了
个物品 ,答案是 。(实际上,只需要做到中括号里少一个
,外面就多一个 ,就可以了)因为我们最内层循环就是枚举
的,所有和 不相干的都相当于常数,可以分离,还剩 。我们需要求这种东西的最大值。
-
如果
,说明物品 取的个数太多了,即滑出窗口; -
如果
,队尾就被新来的完爆; -
所有该弹的弹完之后,加入
; -
现在的队头一定是最大的,用
代入式子求即可。
-
-
我们已经求了
,因为我们的 dp 定义为“恰好”,所以我们需要循环 取最大。 -
复杂度
。
【使用场景】
如果一个 dp 的转移方程可以变成:
注意:这里的
比如多重背包里面的三重循环,队列是开在第二层下的,并且是在第三层下面更新的。
实际上,如果判断条件里面多一个关于
【斜率优化】
斜率、截距等定义就不再赘述。
【引入】
假设我们有一张收入表,上面记录了几家公司在不同年份的收入。
不妨假设每家公司的收入都很稳定,这让它们的收入在表上呈直线,现在我们想研究某年哪家收入最高。
不如把横坐标看为时间,纵坐标看为收入。
我们发现,斜率在这里表示公司收入增长速度,纵截距表示公司初始资金。
显然,紫色公司一定不可能是答案,因为它被其他公司完爆了。
但是我们发现,绿色公司明明没有被任何公司完爆,它却始终无法出头。于是这家绿色公司就是多余状态,需要去除。
但是绿色公司为什么无法出头?我们发现,在绿色超越其他所有公司之前,先被黄色超越了,而被超越意味着以后黄色都比绿色强,于是绿色永无出头之日。
接下来我们研究一个更细的情况。(自动忽略完爆情况)
假设现在只有两条线,分别是 1、2 号。
我们定义
显然,在这张图中
这个时候我们发现,这个时候
并且,因为黄色的斜率更大,绿色以后永远不可能比黄色大,所以在我们发现一条线在超越一切之前又先被超越了,那这条线段就不再有意义。这件事的条件是新来的线段斜率更大。
我们可以把这个说法变得更加具象化:如果一条线与 之前斜率最大的线 的交点 的横坐标
这里进行详细解释:
-
为什么超越一切的地方就在 "与之前斜率最大的线的交点" ?
以红线超越一切的地方为例。
反证法,如果不在这里,说明在这个交点之上,还有另外斜率更小的线(黄线),是截距导致了这条线的交点更靠右。可是,因为黄线在这个交点之上,说明红线与黄线的交点一定比绿线与黄线的交点更靠左。
所以,绿线超越一切的地方,要比红线更晚,但是绿线却在更早之前被红线超越,那么按照我们的说法,绿线根本就不应该存在。矛盾。
所以命题得证,超越一切的地方就在 “与之前斜率最大的线的交点处”。这个命题的得证也证明了另一个命题:如果存在四条线
,那么 的交点一定比 的交点更靠左(不严格)。 -
我们发现了一个很神奇的单调性:随着时间增加,加入的线段的斜率也随之增加,所以新诞生的交点也往右走。
不要忘了我们的目的:求出某时刻最高的线段。
我们发现,这个东西用单调队列可以很好的处理:
一般用单调队列维护的东西只分四个步骤:
-
新加入的会把队尾元素弹出。实际上,不妨设当前队尾元素是
,队尾的倒数第二个元素是 ,新加入的元素是 。如果
与 的交点比 与 的交点更靠左,那么 就可以弹出了。这是因为两点:我们的单调性说明队尾的后两个元素已经是斜率最大的元素了,而我们上面推出的结论可以知道,与 的超越时刻就是 与 的交点 了。而当 在超越一切之前先被别人超越了, 就不再有价值。注意这里涉及了队尾的两个元素。 -
队头会因为过期出队。在这里,过期就代表在我们问的时刻时,队头的元素由于斜率太小,已经被后来的线超越了。这个时候因为队头的斜率小,之后不可能超回来,所以队头此时也可以弹掉。
-
加入新的线。
-
我们想要的答案是什么:队头就是我们想要的线。
-
经过这么分析,我们发现我们可以快速求出某一时刻最高点是哪条线了。
但是这个和 dp 有什么关系?
因为在我们的 dp 中,有时可以把问题变成类似于 “ 若干条直线,求某横坐标上的最高线段 ” 的问题。
【适用场景】
在上面的单调队列优化中,我们强调了一件事:
若我们可以把转移方程改成
在这里,
我们来综合考虑一下斜率优化需要满足什么条件。
-
转移方程改写为
的形式,其中每个函数都是仅关于各自的变量。 -
如果是求
,那么 必须单调不减,这个条件相当于上面这件事的条件就是新加入的斜率更大。 如果是求 就相反。如果不满足这个条件,那么加入线段就要加到中间去,效率降低(当然用优先队列和线段树也可以做)。 -
随着 的增加单调不减,这个无关 还是 ,因为这个的意义在于问问题的时刻是不减的,所以队头的元素不会出队又入队。值得注意的是,我们虽然说是斜率优化,但是
并不一定是一个关于 的一次函数:因为我们不在乎横坐标是什么,之间有什么关系,我们只需要不减。
另外,一开始我们设计出的转移方程可能并不满足斜率优化的形式,但是我们可以变形方程,让它符合条件。
【一些题目】
在这题里面,我们的物品没有体积,价值是
因为是布尔型,所以我们并不关心大小,只关心真假,因此我们的只需要记下来最新的 true 即可。
我们可以用一个 pair 代替双端队列,记录当前窗口内是否有 true,和这个 true 的位置。如果循环中滑出窗口,就改为 false;如果碰到一个新的 true,就更新这个 pair。
在这题中,我们需要做两次背包。
对于 FJ:
我们的物品体积为 1,价值为
对于店主:
硬币数是无限的,这提醒我们要用完全背包。实际上,我们确实按照完全背包的方法求最小体积即可。
最后,因为 FJ 付的钱可能比 FJ 需要付的钱要多很多,所以我们最后求答案的时候需要枚举
注意点:初值是
斜率优化,需要自己把转移方程变形。
定义:
注意到因为在
枚举上一次发车的时间点,注意到发车时间一定是一个人到达的时间,不妨设上一次发车时间为
因为我们在第
有转移方程
其中,
所以,
接下来,我们开始变形!
我们发现现在的转移方程很标准:
于是我们现在就可以进行斜率优化了。
注意,
整理一下:
-
因为我们可能在最后一个人到来前一秒开车,所以我们的 dp 范围应当是
; -
在 dp 过程中,只有
时才考虑往队列中加入新元素( ); -
斜率优化单调队列四步:(左闭右开写法)
-
弹队尾。新加入的元素是
,我们设直线 , , 。(注意,必有 )按照我们上面的结论,如果
交点的横坐标 交点的横坐标,那 永无出 (低) 头之日。所以我们写一个函数
表示 的交点的横坐标。若
,则可弹出队尾; -
加入新元素。我们就加入
,即可能会用来更新的状态; -
弹队头。如果
的交点横坐标 ,就弹队头(队头可能是最低的时间段过去了); -
求答案。此时的队头就是所有可以用来更新的状态中,最小的那一个。
-
-
最终答案:循环
求最小, 为到达时间最晚的那个。
注意:特判
注意:这题刚好
斜率优化。调了 1 week。
考虑 dp:
那么枚举第一个锯木厂的位置
开始变形。
这已经是一个
此时令
接下来有若干个注意点:
-
求
的时候,一定要先求 的前缀(每棵树到山脚的距离),然后立刻循环求 ,不要把 也搞了前缀和再循环求; -
因为可能几棵树堆在一起,
可能相等,要加特判; -
因为
是第二个锯木厂的选址,所以从 2 开始枚举,同时因此我们的单调队列里面一开始就要存一个 作为更新的初始状态(当然,如果就是要从 1 开始循环也不是不行); -
因为枚举状态是
,严格小于,所以要在加入元素 对应的元素之前就更新 ; -
如果判断队头过时的 while 放在 dp 更新之前,应该是
,如果在更新之后,应该是 ; -
在判断队头过时的时候,判断条件应该是
而非 !!!!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!