尺取法の妙用
经典重现
给定一个序列 \(a\),求最长的区间和不超过 \(m\) 的区间的长度,其中 \(a_i \ge0\)。
考虑若左端点固定,则相当于求一个最远的右端点,使得在满足条件的情况下区间长度最长,这个过程显然是 \(O(n)\) 的,那么如果暴力枚举左端点的话,时间复杂度就变成了 \(O(n^2)\),显然不够优秀。
也可以想到利用 \(a\) 数组前缀和的单调性,固定左端点之后二分出最远的右端点,时间复杂度 \(O(n\text{ log }n)\)。依旧不满意,考虑优化。
其实我们可以很容易地发现一个性质,那就是对于两个左端点 \(l_1\),\(l_2\),其中 \(l_1<l_2\),那么它们俩的最优右端点 \(r_1\) 和 \(r_2\),一定满足 \(r_1\le r_2\)。
为什么呢?其实证明很显然。对于一个区间来说,其实只有一个值是对答案有贡献的,即区间的区间和。由于 \(a_i\) 非负,所以若一个区间包含另一个区间,那么前者的区间和一定比后者的更大。而由于 \(l_1<l_2\),所以 \([l_2,r_1]\) 的长度一定小于 \([l_1,r_1]\),故前者一定是一个合法解。若 \(r_2<r_1\),则答案一定没有 \(r_2=r_1\) 时的答案更优,因为区间长度变短了,故 \(r_2\) 一定只会向右扩展,即 \(r_2\ge r_1\)。
所以我们可以考虑用两个指向变量 \(l,r\),指向序列 \(a\),分别表示当前区间的左端点和右端点,然后 \(l\) 设为 \(1\) 后,将 \(r\) 不断向右扩展,直到不合法为止,则不合法前的那个 \(r\) 就是以当前 \(l\) 为左端点时的最优解;然后再将 \(l\) 向右扩展,如果依旧不合法,则当前的区间一定没有刚才的区间优,所以不考虑,扩展到当前区间合法为止。然后再将 \(r\) 向右扩展,重复此过程。
由于每个位置只会被 \(l\) 和 \(r\) 分别经过一次,且区间和可以利用前缀和 \(O(1)\) 来求,所以算法时间复杂度为 \(O(n)\)。
这种利用左右端点从序列左端跑到序列右端的方法,就叫尺取法。
好题应用
1. P1712 [NOI2016] 区间
给定一个数轴以及 \(n\) 个区间 \([l,r]\),找到一个整点,使得该点至少被 \(m\) 个区间包含,且这 \(m\) 个区间中最长区间长度和最短区间长度之差最小。求最小的差是多少。
观察题目,发现题目中包含两个要素:数轴中的位置和区间长度。于是不妨先将区间长度排序,消掉其中一个要素的无序性。考虑将区间 \(1\) 到 \(n\) 的编号看作一个序列,那其实就是让我们将序列中的一段区间的编号所代表的区间放在数轴上,使得其中有 \(m\) 个区间覆盖到同一点。
那么为什么是选一段区间而不是选 \(m\) 个呢?因为这样不仅好计算答案,即最后一个区间的长度减去第一个区间,而且可以很顺利的将本题转化为了尺取法的经典模型——选一段区间,且小区间一定比大区间更可行(区间长度越长,答案越不优)!
设两个指向 \(l,r\),将 \(r\) 不断向右跳,直到当前选择的区间中有 \(m\) 个区间覆盖了同一个点。此时记录答案,然后将 \(l\) 向左跳,边跳边记录答案,直到此时不存在 \(m\) 个区间覆盖同一点,然后再跳 \(r\),循环往复。
那么问题又来了,如何判断当前选择的区间内是否有 \(m\) 个区间覆盖到同一点了呢?可以考虑将原数轴离散化后,用一个线段树维护每段位置被覆盖次数的最大值即可。时间复杂度 \(O(n\text{ log }n)\)。
观察本题与经典模型的差别可知,其实本题就是将原题的前缀和之差改为了区间长度之差,将区间个数不能超过 \(m\) 改为了需要存在 \(m\) 个区间覆盖到同一个点。难点主要在于对于问题的转化以及对于模型的思考。
2. CF1555E Boring Segments
在长为 \(m\) 的数轴上给定 \(n\) 条线段,每个线段都有一个权值,选出若干条线段覆盖整个数轴,求所选线段权值的最小极差。
模仿上一题的套路,我们可以尝试使用双指针,在线段升序之后的权值组成的数轴上来回挪移。
每次挪到一个区间时:
-
若当前区间中包含的线段能将数轴完全覆盖,则用极差更新答案后将左端点加 \(1\);
-
若无法覆盖,则将右端点加 \(1\)。
对于判断数轴是否被完全覆盖,可以考虑用线段树维护区间最小值,若最小值非 \(0\) 即为被完全覆盖;也可以考虑使用扫描线模板的套路,若当前区间被整个覆盖则标记该区间并 pushup,否则从左右儿子区间更新,pushup 时若标记过则直接赋为区间长度。
最后输出答案即可。
3. P7514 [省选联考 2021 A/B 卷] 卡牌游戏
给定 \(n\) 张卡牌,正反面都有数字,且初始都为正面向上,每次操作可以将一张卡牌翻面,最多进行 \(m\) 次操作,求操作过后卡牌朝上的数字的最小极差。
上次比赛上打这道题还是在一年半前,打的还是 \(20\) 分的暴力……时光荏苒啊
再见此题时还是尝试找了许多性质,但好像没几个真的;发现 \(O(n^2)-O(n^3)\) 真的是随便做,又开始感慨当时愚蠢的爆搜(
还是考虑尺取法,将所有卡牌的正反面放一块排序后作为一个数轴,然后开始上左右指针。
- 对于当前指到的区间,若每个卡牌的正面或反面之一都在区间内,且正面不在反面在的卡牌小于等于 \(m\) 个,就更新答案后将左端点右移。
- 否则就将右端点右移。
对于区间内卡牌的判断直接用类似莫队的方法开个桶记录即可。
总结升华
观察这些可以用尺取法解决的题目,可以发现它们都有一个共通的本质——都是与极差有关的问题。
不信我们来一个个分析——尺取法能够解决的经典题目中,当前区间的长度就相当于是在求左右端点极差的最大值;而后面三道例题中,求极差的过程相当于就是把需要求极差的维度变成一个数轴,放两个指针在上面挪移。
而对于这些问题,我们都可以在需要求极差的维度上跑尺取法,并且可以在指针挪动的过程中动态更新当前状态,以及使用恰当的数据结构高效维护当前状态信息,动态更新答案。
为什么这种解法会更加高效呢?其实不难想到——本来较难统计的极差被我们变成了区间的两个端点,可以直接 \(O(1)\) 得到,而取而代之的则是对于该挪动哪个指针的判定性问题——是不是有点像二分的神态了?
关于不带删的尺取法的扩展
有些问题中,加入元素的时的复杂度很小,而删除元素的复杂度很高时,普通的尺取法就不能胜任了——我们需要设计出一种不带删除的尺取法。
考虑维护一个指针 \(mid\) ,初始设为 \(1\),然后两个指针开始按照如下挪移:
- 初始时右指针放在 \(mid\),左指针开始向左挪动,挪动的同时记录当前左指针到 \(mid\) 的答案,直到不符合判定性限制为止。
- 然后开始向右挪动右指针,并判断当前左右指针到 \(mid\) 的答案汇总后是否合法,若合法则更新答案后继续挪动右指针,若不合法则停止挪动。
- 再向右挪动左指针,直到当前左右指针到 \(mid\) 的答案汇总后合法为止(此时左指针到 \(mid\) 的答案已经被我们记录过了),然后跳转到步骤 \(2\);若左指针超过了 \(mid\),则停止挪动。
- 然后将 \(mid\) 重新设为右指针的位置,继续步骤 \(1\),直到右指针超过数轴右端为止。
我们发现在算法过程中,每次更新 \(mid\) 后,左端点最多挪到之前 \(mid\) 的位置,所以每一个位置最多被加入两次,删除一次,总时间复杂度为 \(O(nt)\),其中 \(O(t)\) 为单次加入操作的时间复杂度。空间复杂度也为 \(O(nt')\),\(O(t')\) 为记录一次左端点挪动的空间复杂度。
而通过记录左指针到 \(mid\) 的答案来避免左指针向右移动时的删除操作,其实也是空间换时间思想的一种体现。
2019 五校联考镇海 小 ω 的仙人掌
求最短区间 \([l,r]\) 使得 \((w_i,v_i)\) \((l≤i≤r)\) 做完背包后权值为 \(w\) 的代价 \(≤v\)。
\(n≤10^4\),\(w,w_i\le 5\times10^3\),\(v_i≤2\times 10^5\),\(v≤10^9\)。
需要求的是最短的区间,也就是最小的极差,不妨考虑尺取法。
但是我们发现本题需要求的东西十分不可减,不过合并的时间复杂度是可以接受的,为 \(O(w)\)。
于是考虑使用不带删的尺取法。每次让 \(l\) 从 \(mid\) 向左挪动,在更新当前背包状态的同时把状态记录下来,直到背包代价合法后,开始尝试挪动右指针,挪动的同时让左指针回到其合适的历史版本上,判断当前背包代价是否合法需要枚举左右指针当前各自的背包权值,单次判断时间复杂度为 \(O(w)\)。
等左指针挪到 \(mid\) 右侧时,更新 \(mid\) 到 \(r\),并更新 \(l\) 到 \(mid\),然后继续先挪动左指针,再尝试挪动右指针的同时挪动左指针,循环往复。仍然是由于对于每一个位置,左指针只会加入一次,删除一次,所以总时间复杂度是 \(O(nw)\)。
除此之外,网上有许多做法说的是【双栈优化 dp】,不过我觉得虽然二者做法基本相同,但是本题用【不带删的尺取法】解释更为本质。