CSP-S2024 因为不会智障贪心而考崩溃错失一等的小伙不想再被别人看不起,故作此博客以总结解题技巧。
此外,为了增强骗分能力,我还总结了一下随机化算法的一些东西,以及随机化贪心的使用方法。
- 贪心篇
- 基础模型
- 邻位关系的处理方法
- 反悔堆
- 随机化篇
- 普通随机化
- color-coding
- 模拟退火
- 随机化贪心
- 随机化贪心原理
- 经典随机化贪心习题
贪心篇
基础模型
初学者学贪心时必然会接触到四个经典的区间贪心问题:
区间选点问题:数轴上有 \(n\) 个区间 \([l_i,r_i]\),要求标记尽可能少的点,使得每个区间内都至少有 \(k\) 个点被标记。
解决策略:把所有区间 \([l_i,r_i]\) 按照 \(r_i\) 从小到大进行排序。对于排序后的第一个区间 \([l'_1,r'_1]\),我们将这个区间最靠右的 \(k\) 个点进行标记,即对 \([r'_1-k+1,r'_1]\) 的所有点进行标记,接着对于第 \(2\) 个区间 \([l'_2,r'_2]\),我们计算当前区间中已经被标记过的点的数量 \(m\),若 \(k\leq m\),则跳到下一个区间,否则就在 \([l'_2,r'_2]\) 中最靠右的 \(k-m\) 个未被标记过的点进行标记,以此类推,我们就能使得 \(n\) 个区间中都至少包含 \(k\) 个标记点,且标记点数量最少。
实现方法:数据大时,可以使用数线段树维护区间标记点的个数,每次对区间右侧的若干点进行标记时,我们可以二分一个位置 \(x\) 使得 \([r_i-x+1,r_i]\) 在此次修改之后全为标记点。此时我们在线段树上的修改就变成了区间赋 \(1\)。
区间覆盖问题:数轴上有 \(n\) 个区间 \([l_i,r_i]\),要求选出尽可能少的区间,使得它们可以完全覆盖 \([L,R]\) 这个区间。
解决策略:把所有区间 \([l_i,r_i]\) 按照 \(l_i\) 从小到大排序,我们维护一个指针 \(t\) 表示 \([t,R]\) 的区间还未被覆盖,初始有 \(t=L\)。接着对于当前的 \(t\),我们在排序后的区间中枚举每一个 \(l\) 小于等于 \(t\) 的区间,记录这些区间中最大的右端点 \(r_m\),此时我们就贪心的选择这个右端点最大的区间,让 \(t\leftarrow r_m\),然后重复上述过程。
实现方法:没有复杂的细节,只需要移动 \(t\) 指针以及当前枚举到的下标的指针 \(j\)。
区间划分问题:数轴上有 \(n\) 个区间 \((l_i,r_i)\),要求把它们划分成尽可能少的集合,使得每个集合内的区间两两不相交。
解决策略:把所有区间 \((l_i,r_i)\) 按照 \(l_i\) 从小到大排序,然后先开一个集合 \(S_1\),初始时 \(S_1\) 里存放着 \((l'_1,r'_1)\)。接着对于每一个区间 \((l'_i,r'_i)\),如果它能放到已经存在的某一个集合 \(S_j\) 里面,那么我们就放进去,如果有多个满足条件的集合 \(S_j\),我们就随便选一个;否则就新开一个集合,并把 \((l'_i,r'_i)\) 放进去。
实现方法:直接执行上述过程的时间复杂度最劣是 \(\mathcal{O}(n^2)\) 的。我们可以维护一个优先队列,里面存放每一个已经存在的集合 \(S_i\),令 \(R_i\) 表示 \(\max_{(l,r)\in S_i}\{r\}\),我们就按照 \(R_i\) 将它们排序,\(R_i\) 越小就越靠近队首,每次就判断队首的 \(R_i\) 是否大于当前区间的 \(l\),如果是,则在队列里新加一个集合,否则就把队首的 \(R_i\) 替换成 \(r\)。
不相交区间问题:数轴上有 \(n\) 个区间 \((l_i,r_i)\),要求选出尽可能多的区间,使得它们两两之间不相交。
解决策略:把所有区间 \((l_i,r_i)\) 按照 \(r_i\) 从小到大排序,在枚举的过程中,我们面对当前的区间能选就选,即维护当前选择的若干个区间的最大右端点 \(R\),如果 \(l'_i\geq R\),则 \(i\) 可选,否则不选。
实现方法:模拟上述过程。
同时还要另一道非常经典的烙饼问题:
烙饼问题形式化:给定 \(n\) 个正整数 \(a_i\),每次可以至多选择其中的 \(m\) 个数并让它们的值减少 \(1\),问最少需要多少次才能让所有的 \(a_i\) 都等于 \(0\)。
解决策略:每次选择 \(a_i\) 最大的 \(m\) 个数,然后将它们减 \(1\)。
实现方法:直接模拟还是有点难度的,但是我们有一个关于此题的结论:此问题的答案应为 \(\max\{\max_{i=1}^n\{a_i\},\dfrac{\sum_{i=1}^n a_i}{m}\}\)。
在大赛中还是有不少题套了这些贪心模板的。需要注意的一点是,虽然说它们是模板,但是它们的思维难度并不亚于另一批陌生的贪心题,因此无论如何还是要把它们牢牢掌握的,不然场上推不出来的话就会吃大亏。
邻位交换/Exchange Argument
在众多贪心问题中,有大部分都涉及到了邻位关系上的处理。而这种处理的目的基本上都是为了比较两者的优先级,把对答案产生的贡献更大的排在前面。定义 \(val_i\) 为一个权值比较参数 Exchange Argument,对于相邻两位 \(i,i+1\),如果 \(val(i+1)>val(i)\),则 \(i+1\) 的优先级大于 \(i\),因此我们会进行邻位交换。说的直白一点,其实就是按照某种偏序关系对所有元素进行排序。一般来说,我们处理邻位关系的难点就在于 Exchange Argument 上,即我们应该按照什么信息去排序。
那么我们如何找到这个排序的信息呢?我们可以用假设法,假设交换 \(i,i+1\) 的次序,然后比较交换之前两者的整体贡献 \(f\) 和交换之后两者的整体贡献 \(f'\),如果 \(f\) 劣于 \(f'\),那么显然会考虑交换。由此我们可以找到 \(f\) 和 \(f'\) 分别对应的代数式,然后通过不等关系判断 \(f\) 和 \(f'\) 之间的优劣关系,从而进行邻位交换。上文我们有提到“按照某种偏序关系对所有元素进行排序”,因此根据偏序关系的传递性,对于任意两个位置 \(x,y\) 的元素,若 \(x<y\),则 \(x\) 必然是优于 \(y\) 的,如果此时交换 \(x,y\) 位置上的元素必然会使得答案不优,这是邻位交换最核心的性质,我们可以用它去理解很多贪心排序的正确性。
排序完成之后,我们还有可能会面临其它的问题,比如贪心排序后进行 DP,或者在排序后的序列上套数据结构维护各种信息。这些就都要靠读者自己的能力了。
[ABC366F] Maximum Composition
先思考这样一个问题:如果我们已经确定了这 \(k\) 个函数的编号集合 \(P\),那么我们该怎样规定函数嵌套的顺序 \((P_1,p_2,\dots,P_k)\) 使得 \(f_{P_k}(f_{P_k-1}(\dots f_{P_1}(1)\dots))\) 的值最大(注意这里为了方便思考,是跟原题顺序的下标相反的)。
这里我们就可以考虑邻位交换法,通过一种排序方式使得最终形成的序列 \((P_1,P_2,\dots,P_k)\) 按照顺序进行函数嵌套可以得到最优解。对于相邻的两个函数 \(f_i(x)\) 和 \(f_{i+1}(x)\),我们计算交换两者嵌套顺序前后的具体变化。变化之前是 \(f_{i+1}(f_i(x))\),变化之后是 \(f_i(f_{i+1}(x))\),我们直接带入函数式:
若要交换 \(i\) 和 \(i+1\) 的次序,则需满足变化后的函数值更小,即:
因此我们可以把每个函数的 \(a,b\) 放到同一个结构体 node
中,定义两个 node
变量 \(x,y\) 之间的 \(x<y\) 关系表示 \(a_xb_{y}+b_x<a_{y}b_x+b_{y}\),然后我们就能直接 sort
排序了。那么在最开始的问题中,我们就可以直接用排序后的函数来一次嵌套,根据邻位交换的性质,我们就能保证得到最优解。
由此,如果知道了我们要选择的函数的集合 \(P\),那么我们就有固定的方法求的它们的最优解 \(f(P)\)。现在我们重新考虑原问题,其实就是找到一个大小为 \(k\) 的函数集合使得它的 \(f\) 值最大。我们可以用上述方法对所有的 \(n\) 个函数进行排序,然后 DP 一下。设 \(dp_{i,j}\) 表示在排序后的前 \(i\) 个函数中选择 \(j\) 个依次嵌套可以得到的最优解,且保证第 \(i\) 个函数必选,用 \(f'(i)\) 表示排序后的第 \(i\) 个函数。有如下转移:
由于 \(f'\) 是单调的,我们只需要维护一下 \(dp_{i,j}\) 的一个前缀最大值即可进行一轮优化,时间复杂度为 \(\mathcal{O}(n\log n+nk)\)。
[TJOI2013] 拯救小矮人
不难发现一个人梯内部的排列方式也是非常重要的。如果我们确定了人梯要选择的人的集合为 \(S\),我们就应该按照某种顺序将 \(S\) 重排,然后依次组成人梯,这样在使得其余人爬走之后,\(S\) 内部的人依然可以伸出手臂从而够到顶。
如何知道排序的方式?我们可以通过假设法计算一下。对于人体从上往下第 \(i\) 和第 \(i+1\) 个相邻的人,假定他们都可以出去,那么我们要做的就是尽可能的减少 \(i,i+1\) 作为整体时,他们脚底下踩的人梯的高度 \(h\),因为这样就能用更少的人组成高度为 \(h\) 的人梯,同时就有更多人可以逃出去。在交换 \(i,i+1\) 之前,\(i\) 会踩着 \(i+1\) 上去,则有 \(h+a_{i+1}+a_i+b_i\geq H\),等 \(i\) 走后又有 \(h+a_{i+1}+b_{i+1}\geq H\)。交换了 \(i,i+1\) 之后,同理得到 \(h'+a_i+a_{i+1}+b_{i+1}\geq H\) 以及 \(h'+a_i+b_i\geq H\)。整理一下得到:
如果有 \(h'\leq h\),则表示交换之后 \(i,i+1\) 脚下的人梯的高度下界更小,这就代表交换之后是一定不劣的。转换一下:
\(h'\leq h\Rightarrow \min\{a_{i+1}+b_{i+1}+a_i,a_i+b_i\}\geq \min\{a_i+b_i+a_{i+1},a_{i+1}+b_{i+1}\}\)
因此我们就按照上面的关系式判断邻位是否交换,用 sort
排序得到最终的序列。
但是我们并不知道人梯 \(S\) 到底由哪些人组成,以及 \(S\) 在整个过程中的变化。因此考虑 DP,设 \(dp_{i,j}\) 表示排序后前 \(i\) 个人中有 \(j\) 个人已经出去时,剩余的人梯的最大高度,注意这里的人梯应该是所有 \(n\) 个人中没有出去的,即应该包括 \((i,n]\) 的人。那么显然有:
求答案的时候就是找到最大的 \(i\) 使得 \(dp_{n,i}\) 有值,从 \(n\) 到 \(1\) 依次枚举判断即可。
Complete the Projects (easy version)
首先很显然的一点就是我们肯定会先把 \(b_i\) 非负的项目全部完成,然后再去完成 \(b_i\) 为负的项目。那么对于 \(b_i\) 非负的项目,我们可以将它们按照 \(a_i\) 从小到大排序,然后依次完成它们,如果中途有个项目的 \(a_i\) 无法达到,那么就是无解。
接着我们再去考虑 \(b_i\) 为负的,但是注意这里我们就不能按照 \(a_i\) 从大到小排序了,因为有这样一类数据:假设有两种项目 \((a_i,b_i)\) 分别为 \((10,-8),(9,-1)\),初始的 rating 为 \(11\),那么我们就只能先完成 \((9,-1)\) 的项目,再完成 \((10,-8)\) 的项目,如果先完成 \((10,-8)\),那么另一个项目就无法完成了。
这里我们就要考虑邻位交换了。设两个相邻的位置为 \(i,i+1\),表示第 \(i\) 个计划完成的项目和第 \(i+1\) 个计划完成的项目,考虑什么时候需要交换两者的位置。不难想到这样一种判定方式,由于我们的要求是完成每一个项目,那我们肯定是希望到了第 \(i\) 个项目时,当前剩余 rating 要满足条件的限制更宽松,这就和上一道题很像了。设 \(r\) 为原始位置的 rating 下限,\(r'\) 为交换位置后的 rating 下限。有 \(r=\max\{a_i,a_{i+1}-b_i\}\) 和 \(r'=\max\{a_{i+1},a_i-b_{i+1}\}\),若要交换,则需满足:
按照上述不等关系排序,然后在过程中记录当前 rating,如果遇到无法完成的项目那就是无解。再判一下完成所有项目后 rating 是否变成了负数,剩余的情况就是有解了。
Complete the Projects (hard version)
贪心的大致思路和 easy version 是一样的,但是这道题中对于 \(b_i<0\) 的部分要用一下 \(\mathcal{O}(nV)\) 的 DP,设 \(dp_{i,j}\) 表示选到排序后的第 \(i\) 个 \(b_i<0\) 的项目,当前剩余 rating 为 \(j\) 的完成项目数的最大值。首先有 \(dp_{i+1,j}\leftarrow dp_{i,j}\),接着,若 \(j\geq a_{i+1}\) 且 \(j+b_{i+1}\geq 0\),则有 \(dp_{i+1,j+b_{i+1}}\leftarrow \max\{dp_{i+1,j+b_{i+1}},dp_{i,j}+1\}\)。DP 完之后取一个最大值,然后加上 \(b_i\geq 0\) 的答案即可。
皇后游戏
非常经典的一道贪心题。
由于题目是求一种排列顺序使得某个值最小,那我们就可以尝试贪心。考虑邻位交换,对于一对邻位 \(i,i+1\),思考该通过什么信息对它们进行分析。我们认真读一下题,发现需要最小化的值其实就是 \(c\) 序列的末尾元素。又观察到 \(c_i\) 的转移式中存在 \(\max\{c_{i-1},\sum_{j=1}^i a_j\}\) 这一项,如果我们可以使得 \(c_{i-1}\) 尽可能小,那么 \(c_i\) 的值必然不可能更大。因此我们的大致思路是对于一对邻位 \(i,i+1\),计算交换 \((a_i,b_i),(a_{i+1},b_{i+1})\) 前后的 \(c_{i+1}\) 哪个更小。记 \(pre\) 表示当前排列中的 \(c_{i-1}\),\(S\) 表示 \(\sum_{j=1}^{i-1} a_j\),则交换前后的 \(c_{i+1}\) 分别为:
容易发现转换后的两个式子中都涉及到了对 \(pre+b_i+b_{i+1}\) 取 \(\max\),如果我们删掉这一项,最后得到的结果显然不可能更劣。因此直接删掉它。
现在,如果我们要交换 \(i,i+1\) 的信息,那么就需要满足交换后的 \(c_{i+1}\) 更小,令 \(t=S+a_i+b_i+a_{i+1}+b_{i+1}\),则:
因此我们只需要比较 \(\min\{a_{i+1},b_i\}\) 和 \(\min\{a_i,b_{i+1}\}\) 即可。但是还需要注意到另一件事,如果 \(\min\{a_{i+1},b_i\}=\min\{a_i,b_{i+1}\}\),那么我们就要考虑最小化 \(\sum_{j=1}^i a_j\),此时我们就要按照 \(a_i\) 从小到大对它们进行排序。
时间复杂度 \(\mathcal{O}(n\log n)\)。
反悔堆
反悔堆通常会在这样的问题中被使用:要在 \(n\) 个元素中选择尽可能多的元素,且选出来的元素的集合 \(S\) 满足某个条件,求选出来的元素个数的最大值。
试想这样一种情况,假设对于一个集合 \(A\) 我们已经求出了可以选出的元素个数最大值,且这些被选择的元素集合为 \(B\),现在我们考虑在集合 \(A\) 中加入另外一个元素 \(x\) 且需要求出 \(A\) 此时最多可以选出的元素,那么只会有三种情况出现:将 \(x\) 直接加入 \(B\);用 \(x\) 替换 \(B\) 中的某一个元素;不选择 \(x\) 这个元素。我们分别思考一下:
- 将 \(x\) 直接加入 \(B\):我们只需要判断一下 \(B\cup \{x\}\) 这个集合是否满足条件即可。
- 用 \(x\) 替换 \(B\) 中的某一个元素:我们可以找出原来的 \(B\) 中负面影响最大的元素 \(u\)(这里的负面影响因题而异,后面可以借助例题理解),如果我们发现 \(x\) 的负面影响是严格少于 \(u\) 的,即 \(x\) 的决策优于 \(u\),则我们可以直接用 \(x\) 把 \(u\) 替换掉,这个操作称为反悔操作。
- 不选择 \(x\) 这个元素:如果上面两个情况都不能满足,则我们就不再考虑将 \(x\) 放到 \(B\) 里面。
对于第一种情况,我们通常都是可以快速判断的,而对于第二种情况,我们可以维护一个优先队列去储存 \(B\) 集合,然后对 \(B\) 中的元素按照优劣性进行排序,使得堆顶元素为 \(B\) 中最劣的,我们每次就判断 \(x\) 和这个栈顶元素哪一个更优即可。这个堆就是反悔堆,即维护那些可能被执行反悔操作的元素的堆。
运用到题目之中,我们需要在 \(n\) 个元素中选出尽可能多的元素,那我们可以按照某种特定顺序依次将每个下标加入 \(A\),然后同时用反悔堆去维护 \(B\) 集合。这样就能从每一层的局部最优解一直更新到全局最优解。
此外,反悔堆也有可能会使用到“最大化权值之和”这一类的题目之中,不过大致思路基本上都是类似的。下面就用几道例题来详细的展示一下反悔堆的作用。
[JSOI2007] 建筑抢修
这是关于反悔堆的最经典的题目之一。我们的目标是在 \(n\) 个建筑中选择 \(m\) 个建筑,这 \(m\) 个建筑需满足以下条件:在将这 \(m\) 个建筑按照 \(T_2\) 从小到大排序之后,\(\forall i\in [1,m]\),有 \(\sum_{j=1}^i T_1(p_j)\leq T_2(p_i)\)。我们需要最大化这个 \(m\)。因此我们可以把所有建筑先按照 \(T_2\) 从小到大排序,那么问题就相当于选择一个尽可能长的子序列,使得依次加入子序列中的元素之后满足条件。
于是考虑反悔贪心,设排序后在前 \(i\) 个元素中可以选出的最长的合法子序列为 \(S\),我们思考如何得到前 \(i+1\) 个元素的最长合法子序列 \(S'\)。根据反悔操作的基本思路,我们先看 \(S\cup \{p_{i+1}\}\) 是否是合法的,这里要判断的就是 \(\sum_{x\in S}T_1(x)+T_1(p_{i+1})\leq T_2(p_{i+1})\),我们只需要维护一下 \(S\) 内元素的 \(T_1\) 之和即可。如果上面这个条件不满足,我们就尝试反悔。
如何进行反悔操作?设要被替换的元素为 \(a\),此时我们只考虑 \(p_{i+1}\) 的限制 \(\sum_{x\in S}T_1(x)-T_1(a)+T_1(p_{i+1})\leq T_2(p_{i+1})\),由此我们可以找到 \(S\) 中 \(T_1\) 最大的元素 \(u\),如果它满足 \(\sum_{x\in S}T_1(x)-T_1(u)+T_1(p_{i+1})\leq T_2(p_{i+1})\),则我们直接删掉 \(u\) 这一元素,然后在 \(S\) 末尾加入 \(p_{i+1}\),这样便完成了一次反悔操作。那如果 \(S\) 中的次大元素也满足条件,我们是否可以用 \(p_{i+1}\) 替换掉它的?这样做在当前是可行的,但是我们需要考虑到对后面的影响,因此我们在 \(|S|\) 最大的情况下,必须保证 \(S\) 内 \(T_1\) 的和是尽可能小的。
那么问题就可以解决了。用堆维护 \(T_1\) 最大的被选择元素,以及一个变量 \(t\) 表示被选择元素的 \(T_1\) 之和,按照上面的过程模拟一下即可。
[AGC018C] Coins
先思考一下两种权值该怎么做(即 \(z=0\) 的情况)。现在我们需要从 \(x+y\) 个元素中选择 \(x\) 个元素取 \(a_i\) 权值,剩余的都是取 \(b_i\) 权值,那么我们就尝试反悔贪心,令 \(S\) 为取 \(a_i\) 权值的元素集合,保证 \(|S|=x\),初始就把编号在 \([1,x]\) 中的元素全部存入 \(S\),然后依次将 \([x+1,x+y]\) 的元素纳入我们考虑的范围。设当前被纳入考虑范围的元素是 \(i\),如果我们要用 \(i\) 去替换掉 \(S\) 中的某个元素 \(j\),为了保证答案最优,我们需要满足 \(\sum_{x\in S}a_x-a_j+b_j+a_i> \sum_{x\in S}+b_i\),整理可得 \(a_i-b_i>a_j-b_j\)。因此我们用一个反悔堆维护 \(S\) 且将 \(S\) 按照 \(a_x-b_x\) 的值排序,使得堆顶的 \(a_x-b_x\) 最小。每次要考虑一个新的元素 \(i\) 的时候,如果 \(a_i-b_i\) 比堆顶元素的 \(a_x-b_x\) 更大,那就把堆顶元素删掉并加入 \(i\) 元素。
考虑完所有 \(x+y\) 个元素过后得到的 \(S\) 即为最优情况下取 \(a_i\) 权值的元素集合。不难发现,上述反悔贪心的过程所求的其实就是所有 \(x+y\) 个元素中 \(a_i-b_i\) 最小的 \(x\) 个元素。因此我们可以直接把它们按照 \(a_i-b_i\) 从小到大排序。这个思路其实还可以通过邻位交换法得到。
现在回到原问题,我们可以考虑一个转换:先假定所有元素都取 \(a_i\) 权值,现在我们要让其中的任意 \(y\) 个元素取 \(b_i\) 权值,再让剩余部分中的任意 \(z\) 个元素取 \(c_i\) 权值。这显然可以和我们最开始考虑的两种权值的情况挂钩,即令 \(p_i=b_i-a_i,q_i=c_i-a_i\),我们要在所有元素中选 \(y\) 个取 \(p_i\) 权值,选 \(z\) 个取 \(q_i\) 权值,剩余的取 \(0\),要求最大化权值之和。最后加上 \(\sum_{i=1}^{x+y+z}a_i\) 即为最终答案。
根据两种权值的问题的结论,我们按照 \(p_i-q_i\) 的方法对所有元素进行排序,那么必然存在一个分割线 \(mid\) 使得 \(y\) 个取 \(p_i\) 的元素在 \([1,mid]\) 中,\(z\) 个取 \(q_i\) 权值的元素在 \((mid,x+y+z]\) 中。因此我们用反悔堆预处理 \(pre_i\) 和 \(suf_i\) 分别表示 \([1,i]\) 中 \(p\) 最大的 \(y\) 个元素的 \(p\) 权值之和,\([i,x+y+z]\) 中 \(q\) 最大的 \(z\) 个元素的 \(q\) 权值之和。最终的答案即为 \((\sum_{i=1}^{x+y+z}a_i)+(\sum_{i=y}^{x+y}pre_i+suf_{i+1})\)。
Cardboard Box
我们发现星星的数量只有 \(6\times 10^5\),这代表着我们可以对它们进行枚举。假设我们已经知道了收集 \(m\) 颗星星的最优解 \(w\) 以及这个解中各个关卡所收集的星星数量 \(S_i\),那么我们就可以思考收集 \(m+1\) 颗星星的最优解 \(w'\) 以及每个关卡手机的的星星数量 \(S'_i\)。
根据反悔贪心的基本思路,我们会考虑“加入”和“替换”两种情况,这里我们也尝试分开讨论它们:
-
选择一个未获得两颗星的关卡并多收集一颗星星。即令 \(S'=S\),然后选择 \(S'\) 中一个不等于 \(2\) 的值 \(S'_i\) 执行 \(S'_i\leftarrow S'_i+1\) 操作。如果这个值原本为 \(0\),贪心地想,我们答案的增量 \(\Delta\) 必然为 \(\begin{aligned} \min_{S_x=0}\{a_x\} \end{aligned}\);同理,如果这个值原本为 \(1\),增量 \(\Delta\) 的值就会是 \(\begin{aligned}\min_{S_x=1}\{b_x-a_x\} \end{aligned}\)。这两种最小值都是可以用反悔堆去维护的。
-
放弃一个已经被收集的星星,并另外选择两个未被收集的星星。其中,已经被收集的星星必然出现在 \(S_i\geq 1\) 的关卡中,设这个关卡为 \(u\),而两个未被收集的星星应当都出现在同一个 \(S_i=0\) 的关卡中,设这个关卡为 \(v\)。先思考应该放弃哪个被收集的星星,此时必然是选择代价最劣的一颗去作为替换,可以分为 \(S_v=1/2\) 两种情况,如果 \(S_v=1\),则最劣的代价为 \(\begin{aligned}\max_{S_x=1}\{a_x\} \end{aligned}\),如果 \(S_v=2\),则最劣的代价为 \(\begin{aligned}\max_{S_x=2}\{b_x-a_x\} \end{aligned}\)。再思考应该用哪个关卡的星星去替换,此时必然是选择代价最优的,考虑到两颗星星必须在同一个关卡中,则我们的最优代价为 \(\begin{aligned}\min_{S_x=0}\{b_x\} \end{aligned}\)。分 \(S_u=1,S_v=0\) 和 \(S_u=2,S_v=0\) 两种情况讨论,然后取最优值即可。整理一下,这里需要用 \(3\) 个不同的反悔堆去储存三种代价。
综上所述,我们需要维护 \(5\) 个反悔堆去维护 \(S_x=0/1/2\) 时分别需要用到的极值,然后在上面的所有情况中比较出最优解。但是由于 \(S_x=0\) 时各需要用两个堆,如果一个堆中的元素 \(a_i\) 被删除,那么对应的另一个堆中关于 \(i\) 的元素 \(b_i-a_i\) 也会被删除,这样就变成了可删除类型的堆。我们可以把堆换成 multiset
,这样既能维护每种极值,也能简单的用 erase
删除掉对应的元素。时间复杂度 \(\mathcal{O}(n\log n)\)。
[NOI2019] 序列
我们发现题目就是让我们钦定恰好 \(L\) 个下标的 \(a_i,b_i\) 同时被选,然后在剩下的 \(a_i,b_i\) 中选出 \(k-L\) 个最大的,即保证 \(a,b\) 中选出的元素都有恰好 \(k\) 个。普通的反悔贪心题一般都只有一个“恰好”的限制,但是这道题有两个,我们思考怎么兼顾两种限制。
一个经典的思想就是对于两个限制 \(A,B\),我们可以先设定出一种满足 \(A\) 但不一定满足 \(B\) 的状态,然后通过不断调整使得 \(A,B\) 都能被满足,俗称调整法。这个方法也体现在了[省选联考 2021 A 卷] 矩阵游戏,我们可以构造一组 \(a\) 使得 \(b_{i,j}=a_{i,j}+a_{i+1,j}+a_{i,j+1}+a_{i+1,j+1}\) 成立,然后再通过差分约束使得 \(0\leq a_{i,j}\leq 10^6\)。
在这里我们先把所有 \(a_i,b_i\) 分别从大到小排序,并分别选出排序后 \(a,b\) 中的前 \(k\) 个元素,这样就能满足“恰好 \(k\) 个”的限制,设此时有 \(m\) 个下标 \(i\) 满足其对应的 \(a_i,b_i\) 都被选择,若 \(m\geq L\),则现在的状态也满足限制 \(B\),否则我们就可以反悔贪心,把 \(a_i,b_i\) 同时被选的下标 \(i\) 的个数从 \(m\) 转移到 \(m+1\),一直转移到 \(L\) 为止。
现在我们就来思考有哪些情况可以使得 \(m\leftarrow m+1\),一共有 \(4\) 种情况:
- 取消一个被选择的 \(a_i\) 且 \(i\) 需满足 \(b_i\) 没有被选,接着重新选择一个 \(a_j\) 且 \(j\) 需满足 \(b_j\) 已经被选。代价为 \(a_j-a_i\)。
- 取消一个被选择的 \(b_i\) 且 \(i\) 需满足 \(a_i\) 没有被选,接着重新选择一个 \(b_j\) 且 \(j\) 需满足 \(a_j\) 已经被选。代价为 \(b_j-b_i\)。
- 取消一对同时被选择的 \(a_i,b_i\),接着重新选择一个 \(a_j\) 和 \(b_k\),其中 \(j\) 需满足 \(b_j\) 已经被选,\(k\) 需满足 \(a_k\) 已经被选。代价为 \(a_j+b_k-a_i-b_i\)。
- 取消一个被选择的 \(a_i\) 和一个被选择的 \(b_j\),其中 \(i\) 需满足 \(b_i\) 没有被选,\(j\) 需满足 \(a_j\) 没有被选,接着重新选择一对 \(a_k,b_k\)。代价为 \(a_k+b_k-a_i-b_j\)。
注意“重新选择”的一个或两个元素需要满足当前状态下还没有被选择。
整理一下,我们需要维护 \(6\) 个堆:
- 堆 \(1\):储存 \(a_i\) 没有被选但是 \(b_i\) 已经被选的下标 \(i\)。排序方式应使得堆顶为其中 \(a_i\) 最大的 \(i\)。
- 堆 \(2\):储存 \(b_i\) 没有被选但是 \(a_i\) 已经被选的下标 \(i\)。排序方式应使得堆顶为其中 \(b_i\) 最大的 \(i\)。
- 堆 \(3\):储存 \(a_i,b_i\) 都没有被选择的下标 \(i\)。排序方式应使得堆顶为其中 \(a_i+b_i\) 最大的 \(i\)。
- 堆 \(4\):储存 \(a_i\) 已经被选但是 \(b_i\) 没有被选的下标 \(i\)。排序方式应使得堆顶为其中 \(a_i\) 最小的 \(i\)。
- 堆 \(5\):储存 \(b_i\) 已经被选但是 \(a_i\) 没有被选的下标 \(i\)。排序方式应使得堆顶为其中 \(b_i\) 最小的 \(i\)。
- 堆 \(6\),储存 \(a_i,b_i\) 都被选择过的下标 \(i\)。排序方式应使得堆顶为其中 \(a_i+b_i\) 最小的 \(i\)。
维护好每一个堆,在每一轮增加 \(m\) 的时候就选择四种情况中代价最大的一个,一直到 \(m=L\) 的时候输出被选元素的权值之和即可。
[PA2013] Raper
观察到问题的本质是求一个最小值,而这个最小值有一个限制:在恰好生产 \(k\) 张光盘下的最小花费。结合另一个关于反悔贪心的一个性质:反悔贪心的本质是模拟费用流;费用流在 EK 算法中增广路长度不断增长,故费用是关于流量的凸函数。 因此针对这一类“恰好”的问题,我们的答案是一个凸函数且斜率是具有单调性的。
那么我们就可以考虑 WQS 二分,在平面直角坐标系中标出 \(n\) 个点 \((i,f_i)\),其中 \(f_i\) 表示原问题中生产出 \(i\) 张光盘的最小花费。我们二分一个斜率 \(a\) 表示凸函数上的某一条切线的斜率,设切点为 \((u,f_u)\),则这条切线的截距 \(l=f_u-a\times u\)。根据 WQS 二分的原理,这个截距 \(l\) 就表示让所有 \(b_i\leftarrow b_i-a\) 之后,生产任意数量光盘可以得到的最小花费(不再有光盘数量的限制)。
在求得 \(l\) 的值之后,我们找到此时可能的生产光盘数最大值为 \(m\),如果 \(m\geq k\),则我们的斜率 \(a\) 应该调小,否则就应该调大。这种我们就能得到过 \((k,f_k)\) 的切线以及此时的截距 \(l_k\),直接用 \(f_k=l_k+a\times k\) 表示答案就可以了。
因此现在的问题变为了求生产任意数量光盘可以得到的最小花费。考虑反悔贪心,我们从 \(1\sim n\) 依次考虑每一个 \(b_i\),每次有 \(3\) 种可能:
- 选取一个 \(j\in [1,i]\) 的未被匹配的 \(a_j\) 并让 \(a_j,b_i\) 进行配对。
- 选取一个 \(j\in [1,i]\) 的已经被匹配过的 \(b_j\) 并用 \(b_i\) 去替换掉 \(b_j\),从而让 \(b_j\) 失配。
- 什么都不做。
我们显然应该取这其中代价最小的。对于第一种情况,我们会选择满足条件的最小 \(a_j\)。对于第二种情况,我们会选择满足条件的最大 \(b_j\)。那么我们就可以维护两个反悔堆,一个用来维护未被匹配的 \(a_j\),一个用来维护已经被匹配的 \(b_j\)。如果当前两个堆顶的 \(a_j=-b_j\),那么我们优先选择 \(a_j\) 去和 \(b_i\) 配对。这样会使得当前斜率下生产的光盘数尽可能大。
于是 WQS 二分的 check
函数就搞定了。我们最后就可以直接通过 \(l_k\) 求得我们期望的答案 \(f_k\)。时间复杂度 \(\mathcal{O}(n\log n\log V)\)。
随机化篇
普通随机化
普通随机化所解决的问题往往都是没有那么复杂的。设所有可能的答案的可重集为 \(S\),最优解的可重集为 \(T\),\(T\subseteq S\),那么在完全随机条件下,我们从 \(S\) 中取出最优解的概率为 \(\frac{|T|}{|S|}\),反推回来,我们在 \(S\) 中任取元素时可以取到最优解的期望次数为 \(\frac{|S|}{|T|}\)。
在实现中,我们可以取一个参数 \(c≈ \frac{|S|}{|T|}\) 以表示我们随机选取的次数,通常情况下应确保选中最优解的概率不低于 \(95\%\)。因此若数据组数为 \(m\),那么 \(c\) 的最优取值为 \(\frac{|S|}{|T|}\times m\times k\),其中 \(k\in [1.1,+∞)\)。
当然,这里只是给一个示例,更多时候我们应该根据题目的极限数据去进行调参。
Graph Reconstruction
本题就相当于让我们在 \(G=(V,E)\) 的反图 \(G'\)上连 \(m\) 条边使得每个点的度数不超过 \(2\)。
不难发现当 \(n\) 很大的时候,\(G\) 的反图 \(G'\) 是相当稠密的,因为 \(G'\) 的边数为 \(\frac{n(n-1)}{2}-m\),且题目保证 \(m\leq n\)。我们考虑在包含 \(n\) 个点的完全图中任意找环,计算出这个环上不属于 \(E\) 的边数 \(u\),若 \(u\geq m\),则我们只需要输出这些不属于 \(E\) 的边中的任意 \(m\) 条。
如果我们执行多次上述的过程,那就可以在一定的时间内找到一组解。若进行了长时间的随机化后依然找不到,那就判定为无解。
为了证明这个随机化的正确性,我们可以计算一下概率:首先在完全图中找到的本质不同的环个数为 \(\frac{(n-1)!}{2}\),然后对于每一条给出的边 \((u,v)\),包括它的环的个数为 \((n-3)!\),最劣情况下假设每条边都会各自影响到 \((n-3)!\) 个不同的环,总计 \(m(n-3)!\) 个环,取 \(m\) 最大值 \(n\),那么我们取到不合法的环的概率为 \(\frac{2n(n-3)!}{(n-1)!}=\frac{2n}{(n-1)(n-2)}\)。由于一次随机的时间复杂度为 \(\mathcal{O}(n\log n)\),我们可以在 \(3.00\) 秒内执行 \(150\) 次随机,那么正确率就会大幅度提升,对于小数据我们可以执行更多次的随机,几乎可以排除错失正解的可能性。
[PA2013] Filary
取 \(m=2\),我们可以发现此时的答案至少为 \(\lceil \frac{n}{2}\rceil\),这也就说明我们随机在 \(n\) 个数中选出一个数 \(a_i\),它在最优解中的概率至少为 \(\frac{1}{2}\)。
考虑利用这一个性质去进行随机化,我们任取一个数 \(a_x\),设 \(S\) 为包含 \(a_x\) 的最优解元素集合,则显然 \(\forall y\in S\wedge y≠x\),\(m\) 必然整除 \(|a_x-a_y|\),因此最大的 \(m\) 为 \(\gcd_{y\in S,y≠x}|a_x-a_y|\)。
由此,我们可以枚举每一个质因子 \(p\),看有多少个 \(y\) 满足 \(p\) 整除 \(|a_x-a_y|\),设这些 \(y\) 的集合为 \(f(p)\)。我们找到所有质数中 \(|f(p)|\) 最大的一个 \(p\),那么显然 \(f(p)\) 就是上文我们提到的 \(S\),因此我们处理出它们的最大公约数即可。
为了优化时间,我们可以提前预处理出 \(l_i\) 表示 \(i\) 的所有质因子中最小的那一个,然后对于每个 \(v=|a_x-a_y|\),我们就先更新 \(l_{v}\) 这个质因子的贡献,然后除去 \(v\) 中的所有 \(l_v\) 因子,继续更新其它的质数。每次随机完之后的清空操作也应该这样操作,于是时间复杂度就会降低许多,原题数据可过。
Love-Hate
注意到“不少于 \(\lceil \frac{n}{2}\rceil\) 个人”的限制,这说明对于一个人 \(i\),他在最优解中的概率大致为 \(\frac{1}{2}\)。因此我们考虑随机选一个人 \(i\),钦定他必选,然后在剩余部分中选出尽可能多的元素。
由于 \(1\leq m\leq 60\),我们可以用 long long
对每个人储存一个二进制状态 \(sta_i\),若 \(sta_i\) 的第 \(j\) 个二进制位上是 \(1\),则表明第 \(i\) 个人喜欢第 \(j\) 中货币。设随机钦定 \(i\) 之后的最优解的货币状态为 \(S\),则显然有 \(S\subseteq sta_i\),因此我们可以枚举 \(sta_i\) 的所有子集 \(t\),然后在所有 \(n\) 个状态中查找有多少个 \(sta_j\) 满足 \(t\subseteq sta_j\)。
但是单次随机的 \(\mathcal{O}(3^pn)\) 时间复杂度是不能接受的。于是我们可以使用高维前缀和的手段去写一个 DP,这个方法是套路的。优化之后的时间复杂度就变为了 \(\mathcal{O}(2^pp)\)。
每次随机的正确率都是 \(\frac{1}{2}\),时限比较宽松,我们可以随机个 \(50\) 次,这样下来整个算法的错误率就趋近于 \(0\) 了,可以忽略不计。
Find a Gift
考虑已知某个石头的位置 \(x\) 之后我们该怎么找答案。由于题目是让我们求最靠前的不是石头的位置,那么我们就先比较 \(1,x\),如果交互得到的结果为 EQUAL
,则说明两者都是石头,否则答案就是 \(1\) 位置。此时为了更好地利用当前信息,我们可以让 \(A=\{1,x\},B=\{2,3\}\) 然后比较 \(A,B\),如果交互得到的结果为 EQUAL
,结合“石头质量严格大于礼物质量”的条件,我们可以推断出 \(2,3\) 也都是石头,否则答案就在 \(2,3\) 之中。接着,我们比较 \(A=\{1,2,3,x\}\) 和 \(B=\{4,5,6,7\}\),思路和上面的一样,以此类推。
不难发现整个过程我们询问的次数是 \(\mathcal{O}(\log n)\) 级别的,因为每次我们的两个集合的大小都在成倍增长。设当前集合的大小为 \(len\),则我们的 \(A\) 集合应当是 \(\{x\}\) 并上 \(\complement_{U}{\{x\}}\) 中最小的 \(len-1\) 个元素所组成的集合,\(B\) 则是 \(\complement_{U}A\) 中最小的 \(len\) 个元素。特殊地,如果 \(len>\frac{n}{2}\),则我们让 \(B\leftarrow \complement_{U}A\)。
当我们在一次比较中发现 \(A,B\) 的比较结果不再是 EQUAL
,那就说明 \(B\) 中必然存在一个礼物。此时我们只需要在 \(B\) 中进行二分,每次比较 \(A,B\) 各自的长度为 \(mid\) 的前缀,最后二分到的位置就是第一个礼物的位置。这里二分需要花费的询问次数同样为 \(\mathcal{O}(\log n)\)。
由于 \(k\geq 1\),所以上述过程一定可以找到解。
那么剩下的唯一问题就是如何确定一个石头的位置 \(x\)。观察到题目中 \(k\leq \frac{n}{2}\) 这个性质,说明在完全随机条件下,我们随机取到一个石头的概率是不低于 \(50\%\) 的。由于题目要求询问次数不超过 \(50\),且上述倍增与二分的过程所花费的最多询问次数为 \(2\log n\leq 20\),因此留给我们去随机寻找的次数上限为 \(50-20=30\)。假定我们随机 \(25\) 次,那么我们找不到石头的概率最大为 \((\frac{1}{2})^{25}=\frac{1}{33554432}\),考虑到最多会有 \(500\) 组数据,所以对于每个测试点我们的错误概率为 \(\frac{500}{33554432}≈0.0000149012\),CF 原题测试点数为 \(100\),那么无法得到满分的概率为 \(0.00149012\),足以通过。
color-coding
color-coding 中文翻译过后的意思是“随机染色”,即随机构造出一组映射 \(f\),将一个集合 \(S\) 内的所有元素 \(x\) 都映射到一种颜色上。
这样的随机染色思路最开始用在了 k-path 问题上:判断无向图 \(G=(V,E)\) 中是否存在一条长度为 \(k\) 的简单路径(不经过重复的点或边)。
这个问题其实是 NP-Hard 的,但是在 \(k\) 足够小的时候我们可以用一种随机化的手段去解决这个问题。我们给 \(G\) 中的每一个节点 \(i\) 都染上一个 \(1\sim k\) 中的颜色 \(c_i\),然后我们尝试找到这样一条路径 \(P\) 使得所有 \(k\) 种颜色都在 \(P\) 中出现恰好 \(1\) 次。这就是一个典型的状态压缩 DP 了,我们设 \(dp_{i,S}\) 表示当前点在 \(i\) 且经过的颜色集合为 \(S\) 的路径是否存在,\(0/1\) 分别表示不存在和存在,初始有 \(dp_{i,\{c_i\}}=1\),转移时对于每一条边 \((u,v)\) 若 \(c_u\notin S\) 则执行 \(dp_{u,S\cup\{c_u\}}\leftarrow dp_{u,S\cup\{c_u\}} \vee dp_{v,S}\),用 \(u\) 更新 \(v\) 的转移同理。用 \(U\) 表示 \(1\sim k\) 所有颜色的全集,我们只需要判断是否存在一个 \(dp_{i,U}\) 的值为 \(1\) 即可。时间复杂度 \(\mathcal{O}(2^km)\)。
若 \(G\) 中存在一条长度为 \(k\) 的简单路径,那么上述算法的正确率应为 \(\frac{k!}{k^k}\),也即我们找到答案的期望次数为 \(\frac{k^k}{k!}\),故我们可以反复执行 \(\frac{k^k}{k!}+T\) 次上述算法(\(T\) 为一较小常数),这样我们的正确性就会提升不少。时间复杂度就变为了 \(\mathcal{O}(\frac{k^k}{k!}2^km)\),这也就是为什么在 \(k\) 足够小的时候我们可以几乎解决这个问题。
这个随机化算法的重点思路就是进行随机染色 color-coding,相较于上一节中的普通随机化,它不再是拘泥于对一种元素的随机,而是进行了全局随机化,为所有元素都随机出一种映射方案,然后利用其余的算法巧妙地解决一些特殊问题。
一般来讲,color-coding 可以解决的题目都会有个显然的特点,就是存在一个值域在 \([1,5]\) 左右的变量 \(k\)。所以在遇到这种怪异的题目时,我们不仅要想状压和斯坦纳树,还要思考一下它是否可以利用随机化去解决。
[CSP-S 2022] 假期计划
这题正解是 meet in the middle,但其实也可以用 color-coding 巧妙地解决。
首先不难想到用 BFS 预处理出每两个点之间的距离 \(dist(i,j)\),因此对于当前景点 \(u\),若存在一个地方 \(v\) 使得 \(dist(u,v)\leq k+1\),则 \(v\) 可以成为 \(u\) 的下一个景点。我们用这个关系建边,即构造一个新图 \(G'=(V',E')\),其中每一条边 \((u,v)\) 表示原图 \(G\) 中 \(dist(u,v)\leq k+1\)。
那么现在的问题就是在 \(G'\) 找到一个经过 \(1\) 号节点的有向五元环使得环上每个点的点权之和最大。由于我们要找的环的节点个数很小,因此考虑 color-coding,对除了 \(1\) 以外的所有节点随机染上 \(1\sim 4\) 中的颜色,那么现在我们尝试找到一个点权之和最大的有向五元环,满足其包含 \(1\) 节点且 \(1\) 节点接下来走到的 \(4\) 个节点的颜色依次为 \(1,2,3,4\)。
这个问题可以记忆化搜索解决,设 \(c_i\) 为 \(i\) 节点的颜色,特殊地让 \(1\) 的颜色 \(c_1\) 为 \(0\),定义 DP 数组 \(dp_i\) 表示从 \(i\) 开始的大小为 \(4-c_i+1\) 的一条点权和最大的有向链,满足链上的点的颜色依次为 \(c_i,c_i+1,\dots,4\) 且链尾与 \(1\) 存在 \(E'\) 中的连边。转移:
最终的答案显然为 \(dp_1\)。由于 \(|E'|\) 是 \(\mathcal{O}(n^2)\) 级别的,因此我们 DP 的时间复杂度最劣为 \(\mathcal{O}(n^2)\)。
上述算法可以得到最优解的概率为 \(\frac{2}{4^4}=\frac{1}{128}\),我们理应执行 \(128+T\) 次上述算法。当然我们也可以用 clock()
函数卡时。理论来讲这样的 color-coding 算法的循环次数会达到 \(8\times 10^8\) 甚至更多,但是即使是构造出了 \(|E'|=2500^2-2500\) 的数据,我们发现它在开了 O2 优化的 \(2s\) 时限内竟然能执行 \(500\) 多次随机化,因此正确率可以得到极大保证。
Turtle and Three Sequences
先思考朴素的暴力算法,考虑状压 DP,设 \(dp_{i,S}\) 表示在前 \(i\) 个数中,选出的若干个数的 \(b_i\) 并集为 \(S\),且强制 \(i\) 必选的最优答案。转移显然为:
其中 \(S\) 需满足 \(b_i\in S\wedge |S|\leq m\)。最终答案即为所有 \(dp_{i,S}\) 的最大值。
但是 \(b_i\) 太大了,我们无法直接进行状压。注意到 \(m\) 的值很小,我们考虑 color-coding,对于 \(1\sim n\) 中的每一个 \(i\) 随机染色为 \(col_i\),保证 \(col_i\in [1,m]\),然后我们令 \(b_i\leftarrow col_{b_i}\),将原问题转换为选出一个子序列 \(\{p_1,p_2,\dots,p_m\}\) 使得 \(a_{p_1}\leq a_{p_2}\leq \dots \leq a_{p_m}\) 且 \(b_{p_i}\) 互不相同,由于 \(b_i\) 最多只有 \(5\) 种,因此这个问题可以根据上文的状压 DP 思路解决。
我们要取得正确答案 \(\{p'_1,p'_2,\dots,p'_m\}\) 就必须使得 \(b_{p'_i}\) 对应的颜色互不相同,概率显然为 \(\frac{m!}{m^m}\leq \frac{24}{625}\),则我们应该执行至少 $\lceil \frac{m^m}{m!}\rceil $ 次随机。但是上述算法的暴力时间复杂度为 \(\mathcal{O}(n^22^m)\),我们考虑用树状数组优化一下,对每个 \((i,S)\) 维护所有 \(a_j\in [1,i]\) 的 \(dp_{j,S}\),时间复杂度降为 \(\mathcal{O}(2^mn\log n)\)。我们只需要执行 \(500\) 次循环就能以极大的概率找到最优解。
[THUSCH2017] 巧克力
题目中给出了两个问题,一个是找到最小的合法连通块,一个是在保证合法连通块最小的情况求出最小的中位数。先思考前者怎么解决。不妨将整个网格图想象成一个无向图 \(G=(V,E)\),考虑一个和“无向图求斯坦纳树”类似的状态压缩 DP,设 \(dp_{i,S}\) 表示最小的连通块大小,使得这个连通块包含 \(i\) 号节点,且它内部节点的所有 \(c_i\) 的并集是 \(S\) 的超集,那么转移有两种可能:
第一种可以直接转移,第二种可以通过用最短路更新。设 \(c_i\) 的种类数为 \(p\),则时间复杂度为 \(\mathcal{O}(3^pnm+2^pnm\log nm)\)。由于 \(k\) 很小,我们可以用 color-coding 将 \(p\) 降为 \(k\) 的级别,即对于每个 \(i\) 随机一个颜色 \(col_i\in [1,k]\),然后令 \(c_{i,j}\leftarrow col_{c_{i,j}}\)。这样我们得到最优解的概率为 \(\frac{k!}{k^k}\),多随机几次就可以以极高概率获得最优解。
现在思考第二个问题——怎么最小化中位数?套路地,我们二分一个值 \(x\),令所有 \(a_{i,j}\leq x\) 的点的权值为 \(-1\),所有 \(a_{i,j}>x\) 的点的权职为 \(1\),我们希望在所有最小的连通块中找到一个连通块 \(A\) 使得 \(A\) 中元素的权值非正。这个问题很简单,我们只需要将第二种 DP 转移改为:
为了避免 \(\text{SPFA}\) 算法被卡,我们可以把 \(a_{i,j}\leq x\) 的权值设定为 \(99999\),将其余点的权值设定为 \(100001\),然后找到一个最小连通块使得其节点的权值之和不超过最小连通块大小的 \(100000\) 倍。
设我们每次随机的次数为 \(T\),这样的时间复杂度为 \(\mathcal{O}(T3^knm\log V+T2^knm\log nm\log V)\)。这个 \(V\) 比较大,我们可以把所有 \(a_{i,j}\) 离散化,这样时间复杂度就优化成了 \(\mathcal{O}(T3^knm\log nm+T2^knm\log^2 nm)\),将 \(T\) 设定为 \(200\),原题数据最慢的点跑了 \(3.26s\)。
模拟退火
模拟退火 Simulated Annealing(简称 SA)是随机化算法中十分巧妙的一个结合了物理知识的算法。这里引入一下百度百科中的解释:
模拟退火算法来源于固体退火原理,是一种基于概率的算法,将固体加温至充分高,再让其徐徐冷却,加温时,固体内部粒子随温升变为无序状,内能增大,而徐徐冷却时粒子渐趋有序,在每个温度都达到平衡态,最后在常温时达到基态,内能减为最小。
对于一个非单调且非单峰函数 \(f(x)\),我们尝试在定义域 \(R\) 中找到一个点 \(u\) 使得 \(f(u)\) 最大,由于 \(f(x)\) 是杂乱无序的,因此常规的二分法和三分法都无法解决。考虑模拟退火算法。我们在当前最优解 \(u\) 的附近随机找点 \(a\),如果 \(f(a)\) 优于我们的当前最优解 \(f(u)\),那么必然有 \(u\leftarrow a\) 的更新,否则,我们以一定的概率 \(p\) 去接受这个新解。
结合退火的原理,我们希望在最开始的时候 \(p\) 更大,这样就能呈现出一个无序状态,而到了后期我们就让 \(p\) 变小,这样就相当于一个粒子趋近于有序状态。设 \(T\) 为在退火过程中我们当前的温度,\(\Delta E\) 表示 \(f(a)-f(u)\),针对于 \(\Delta E\) 我们接受 \(a\) 这个解的概率 \(P(\Delta E)\) 为:
每次考虑完一个 \(a\) 时,我们就让 \(T\) 逐渐减小。这样便能契合退火的过程。
具体实现的话,我们可以调出三个参数 \(T_s,T_e,\delta\) 分别表示起始温度,终止温度,每次随机完一个数之后 \(T\) 的变化量(\(T\leftarrow T\times \delta\)),其中 \(\delta\) 也叫降温系数。我们初始时让 \(T\leftarrow T_s\),每次随机之后更新 \(T\) 为 \(T\times \delta\),如果 \(T\leq T_e\) 则停止退火过程。通常来讲,\(\delta\) 是一个非常趋近于 \(1\) 的小数,\(T_e\) 是一个非常趋近于 \(0\) 的小数。
另外,当我们以 \(e^{\frac{-\Delta E}{T}}\) 的概率保留当前解时,我们可以用 exp((-DeltaE)/T)>=(double)rand()/RAND_MAX
的代码来判断。
特别注意的是,我们的 \(T_s,T_e\) 应该是和值域 \(V\) 对应的,因为 \(\frac{-\Delta E}{T}\) 中 \(\Delta E\) 和 \(V\) 是同一级别的,那么我们的温度必然要与这个值域相关,这就确定了我们调参的方向。
为了尽可能避免调参具体数值对答案的影响,一个通用的方法是先将 \(T_s,\delta\) 尽可能取大一点,\(T_e\) 尽可能取小一点,这样不能保证时间限制,但是在一定程度上可以保证答案更优,然后我们将 \(T_s,T_e\) 逐渐靠拢,\(\delta\) 针对于一对确定的 \((T_s,T_e)\) 在 \([0.911,0.999]\) 选取。这样下来我们调参的时间也能少一点。
单次退火可能无法取得最优解,我们可以在 \(\text{TimeLimit}\) 内多执行几次 SA。在更难的题目当中,我们的模拟退火可能还需要伴随另一些随机技巧。下面通过例题介绍一下。
[TJOI2010] 分金币
先考虑普通随机化,我们每次从序列中任意选出 \(\lfloor \frac{n}{2}\rfloor\) 个数分为一组,剩下的数分为另外一组,然后两者做差,更新全局最小值。
但是这样做太低效了,考虑模拟退火,设当前最优答案为 \(m\),最优分组方法为 \((A,B)\),即将原序列分成 \(A,B\) 两组,我们随机交换 \(A,B\) 中的一个元素得到 \((A',B')\),然后计算新的解 \(v=|\sum_{x\in A'}x-\sum_{x\in B'} x|\),即 \(\Delta E=v-m\),此时若 \(\Delta E<0\) 则必然会用 \((A',B')\) 代替原来的解,否则我们就以 \(e^{\frac{-\Delta E}{T}}\) 的概率接受这一组解。
由于每次随机只会有两个数发生变化,因此我们是可以 \(\mathcal{O}(1)\) 维护两组数的差值的。
取每次退火的参数为 \(T_s=10^4,T_e=10^{-5},\delta=0.9651\),我们发现单次退火的正确率不够高,那么我们就增加退火的次数为 \(1200\),每次退火之前,我们的初始 \((A,B)\) 应当重新随机选取,而不是直接继承上一次 SA 结束后留下来的 \((A',B')\),这样就能让我们随机的对象更加多样化,也尽可能保证了答案的正确率。
原题数据可过,但是也存在一些高强度的数据可以 Hack 掉模拟退火做法,这里不再讨论解决这些 Hack 数据的方法。
[SCOI2008] 城堡
还是先从普通随机化开始考虑,每次任选基环树上的 \(k\) 个未被选择的点,然后求出 \(\max\{dist(c)\}\) 的值,这个 \(\max\{dist(c)\}\) 我们可以在预处理任意两点之间的最短路之后 \(\mathcal{O}(n^2)\) 求,可以直接用 \(\text{Dijkstra}\) 多源最短路 \(\mathcal{O}(n\log n)\) 解决,甚至可以用换根 DP 做到 \(\mathcal{O}(n)\) 求解,但是本题数据范围较小,我们可以使用 \(\mathcal{O}(n^2)\) 的朴素算法。
然后就和上一道题很类似,我们尝试从当前状态拓展出另一种新的解,即在当前选出的 \(k\) 个点中选择一个点移除掉,然后用另一个未被选择的点替补上去,按照这个方式我们直接模拟退火。调参为 \(T_s=10^4,T_e=10^{-11},\delta=0.9952\),每次 SA 的正确率可能还不够,我们就执行 \(150\) 次 SA。
注意到时限比较紧,我们的每次计算 \(\Delta E\) 时的 \(\mathcal{O}(n^2)\) 做法可以稍作改进,假设我们枚举的过程中已经取到的 \(\max\{dist(c)\}\) 为 \(M\),对于当前点 \(x\),如果已经枚举到的若干个点中存在一个与 \(x\) 的距离小于等于 \(M\) 的,那么我们就可以直接跳过对于 \(x\) 的统计。这样的话 \(\mathcal{O}(n^2)\) 在很大概率上都跑不满。
这种做法可以通过原数据,但是加强版的 Hack 过不了,这个可以通过调整随机种子解决。
[ABC157F] Yakiniku Optimization Problem
模拟退火也可以解决一些二维平面上的问题。
在这道题中,我们可以观察到数据范围比较小,且浮点数精度的限制要求也不算紧,那么我们就可以思考模拟退火。我们可以从随机一个点 \((s_x,s_y)\) 开始,每次用 \(\mathcal{O}(n\log n)\) 的时间复杂度计算出 \(c_i\times \sqrt{(X-x_i)^2+(Y-y_i)^2}\) 的 \(k\) 小值,然后用模拟退火的模板去对更新当前解。每次确定了当前解之后,我们就要在 \((X,Y)\) 的附近寻找一个新的点,考虑到最优解的坐标可能不是整数,我们就可以使用 uniform_real_distribution
去在特定范围内随机出一个浮点数,对横纵坐标都找一遍随机数,然后凑成一个新的坐标 \((X',Y')\),接着就按照模拟退火的流程走就行了。
为了防止出题人刻意卡掉模拟退火的解法,我们可以将整个平面直角坐标系旋转 \(\alpha\) 度,其中 \(\alpha\) 为一个 \([0,180]\) 的随机浮点数,此时每个点原来的坐标 \((x_i,y_i)\) 都会转换为 \((x_i\cos \alpha-y_i\sin \alpha,x_i\sin \alpha + y_i\cos \alpha)\)。我们还是按照上面的思路执行 SA,参数分别为 \(T_s=10^4,T_e=10^{-13},\delta =0.997\),任意调整随机种子执行大约 \(40\) 次 SA 就可以通过原题数据。
[JOISC2020] 伝説の団子職人
不难发现问题的中心在于白色团子,每个白色团子都最多有三种状态:从左上到右下可以连成一个合法串,从上到下可以连成一个合法串,从右上到左下可以连成一个合法串。分别设这三种状态为 \(0/1/2\)。
设二元组 \((i,j)\) 表示第 \(i\) 个白色团子的状态为 \(j\) 时它所在的合法串覆盖的范围,原题就转换为选出尽可能多的二元组,使得任意两个被选的二元组 \((i,s_1),(j,s_2)\) 满足 \((i,s_1)\cap (j,s_2)\not= \varnothing\)。如果我们对两个相交的二元组 \((i,s_1),(j,s_2)\) 连一条边,那么问题其实就转换为了求图中的最大独立集。
然而求最大独立集是 NP-Hard 问题,我们只能通过随机化的手段去取得尽可能优的解。由于本题是提交答案题,因此我们在本地运行时无需考虑任何时间限制,我们要做的就是让随机化尽可能快速地取得更好的解。
先考虑一个朴素的随机化贪心(这个在后面的章节会讲到),把所有点按照任意顺序排序,然后依次遍历每一个点,如果当前点 \(x\) 不存在已经被选择的相邻点,那么就强制选择 \(x\),否则跳过。这样不断随机下去,得到符合要求的解的耗时其实是比较长的。我们可以用模拟退火去优化这个过程,在初始时我们 random_shuffle
得出一个排列,每次退火操作前都任意交换这个排列中的两个点的顺序,得到 \(\Delta E\),根据 \(P(\Delta E)\) 对当前排列进行更新。
这道题的时间非常开放,我们可以根据自己的想法进行模拟退火,比如每次退火前都任意交换更多个点的顺序,或者退火过程中在一定概率下重新 random_shuffle
原排列。参数也不是特定的,不用刻意地去纠结最好的参数,主要在足够的时间里跑出来就行了。
对于每个输入数据,我们可以无限循环执行若干次 SA,直到最优解满足这个数据的要求。实现较好的话,最慢的测试点得到合法答案的时间不超过 \(3\) 分钟。
随机化贪心
随机化贪心原理
随机化贪心是大多数的人类智慧做法所用到的东西。当我们在做一道关于最优解的题目时,如果想到了一种伪贪心(通常是没有考虑到当前状态对后续状态的影响的一类贪心),我们其实用不着弃掉这个做法,相反,我们可以结合随机化,对问题的序列反复执行 random_shuffle
并进行贪心,从而获得近似最优解。所谓近似最优解,就是与严格最优解接近,但是无法完全保证与严格最优解相同的解。此外,有的题还可以用微扰法,每次任意交换序列中的两个元素,但这样的例子还是比较少的。
为了使我们的答案契合最优解的概率增大,我们可以采用 (double)clock()/CLOCKS_PER_SEC<=T
卡时,不过其中一定要注意 \(T\) 的调参,如果调整不当,是很有可能导致整道题不得分的。一般来讲应该 \(T\) 应该取到 \(\text{TimeLimit}-0.1\),如果单次伪贪心的时间复杂度较大或者常数较大,可以再适当减去一点。千万不要为了这 \(0.1\) 秒以内的时间而把 \(T\) 增大,因为这一点时间在 \(\text{TimeLimit}\) 中的占比算不了什么。
实际上,大多数随机化贪心的题目都有一个共同点,就是这个问题不对顺序作严格要求。如果一道题目强制作了顺序要求,那么我们每次的贪心都只会按照它固有的顺序去执行,因此我们的随机化无法影响到贪心的答案。当然这些还是要因题而论,如果一种贪心思路不受顺序限制,那么我们依旧可以去考虑随机化贪心。
最后,如果实测下来对于极限数据的通过概率比较小时,我们还可以利用模拟退火进行优化,一般来讲可能会多 \(10\) 分左右。因此如果一道题是作为拉分题的,那么随机化贪心加模拟退火的组合往往会带来巨大收益。
经典随机化贪心习题
Leaving the Bar
假设我们已经求出了 \(1\sim i-1\) 的向量按照某种组合加起来的坐标 \((u,v)\),考虑第 \(i\) 个向量 \((x,y)\),我们有 \((u-x,v-y),(u+x,v+y)\) 两种结果,那么我们就选择其中模最小的那一个向量,这样可以尽可能保证当前模在 \(1.5\times 10^6\) 范围内,但是可能会对后续选择造成负面影响。比如给出三个向量 \(a,b,c\),有 \(a=-b,|a|>0,|c|=1.5\times 10^6\) 且 \(a,b,c\) 共线,如果只考虑 \(a,b\) 向量,那么我们的坐标显然会转移到 \((0,0)\),但是此时再算上 \(c\) 的话我们的模就会达到 \(1.5\times 10^6\) 而不满足条件,实际上我们可以取 \(a+b±c\) 中模最小的那一个,这个最小的模显然不会超过范围。
虽然这是一个伪贪心,但是我们可以采取随机化贪心。在上面的那个例子中,如果我们按照 \(c,a,b\) 的顺序去考虑,那么贪心得到的答案其实是合法的。因此我们可以把所有向量执行 random_shuffle
操作,然后每次按照上述贪心思路求解,如果当前坐标的模小于 \(1.5\times 10^6\),那么直接输出当前解。由于题目保证有解,我们可以不用调时间参数,直接随机无限次,当找到正解时停止即可。CF 上可过。
[HAOI2006] 均分数据
可以发现其中 \(\bar{x}\) 是固定的,我们只需要在意分组的方式。
考虑一个贪心,我们从 \(1\sim n\) 遍历每一个 \(i\),然后把 \(a_i\) 放入当前组 \(S\) 中,如果 \(\sum_{v\in S}v > \bar{x}\),我们就比较 \(|\sum_{v\in S}v-\bar{x}|\) 和 \(|\sum_{v\in S}v-a_i-\bar{x}|\) 的值,如果前者更小,那么就保留 \(S\) 中的 \(a_i\),从下一个数开始就是新的一组;否则就把 \(a_i\) 从 \(S\) 中删除,让他单独成一组,在后续考虑 \(a_j\) 时就往 \(a_i\) 所在的组里放入 \(a_j\)。这个贪心存在一定正确率,但是并不能保证取得最优解。
由于 \(n\) 的规模较小,我们可以套路地进行随机化贪心,每次都打乱一遍原序列,把时间卡到 \(0.97s\),然后输出随机化过程中得到的最小 \(\sigma\) 即可。
[春季测试 2023] 密码锁
考虑一个伪贪心,对于第 \(i\) 个拨圈,假设我们已经确定了前 \(i-1\) 个拨圈的状态,我们找到它可以拨到的最优的状态,使得前 \(i\) 个拨圈组成的密码锁的松散度最小,这个过程可以 \(\mathcal{O}(k^2)\) 枚举。
但是这个伪贪心必然是错误的。由于本题的答案和最值有关,而一个序列的最值不受顺序影响,因此考虑随机化贪心。每次都随机打乱所有拨圈的顺序,然后依次执行伪贪心。
手造大样例可以发现,如果我们执行 \(185\) 次伪贪心,则能够以极大概率取到最优解。当然也可以使用 clock()
函数。
从思维难度和实现难度上都远远简单于正解的分类讨论和扫描线。为了逼近最优解,我们可以通过手造大样例的方式进行参数调试,一般来说,如果小规模的数据与暴力对拍无误,且大规模数据输出的答案近乎稳定,那么这个随机化贪心的正确性就是非常高的。
[HNOI2011] 任务调度
观察到 \(t_i=3\) 的 \(i\) 不超过 \(10\),因此我们可以直接 \(\mathcal{O}(2^{10})\) 枚举每个 \(t_i=3\) 的 \(i\) 的状态 \(1/2\),表示 \(t_i\leftarrow 1/2\)。那么当前我们就是对于一个固定的局面进行求解。
我们肯定是希望两个机器处理每个任务的时间尽可能连续,那初始时,我们先让机器 \(A\) 处理完所有 \(t_i=1\) 的任务,机器 \(B\) 处理完 \(t_i=2\) 的任务,接着交叉处理,让 \(A\) 处理 \(t_i=2\) 的任务,\(B\) 处理 \(t_i=1\) 的任务。
以机器 \(A\) 为例,我们记录 \(s_1\) 表示机器 \(A\) 将要处理第 \(j\) 个 \(t_i=2\) 的任务 \(p_j\) 时机器 \(A\) 的运行时间(包括最开始时处理 \(t_i=1\) 的任务的时间),\(s_2\) 表示机器 \(B\) 处理完前 \(j-1\) 个 \(t_i=2\) 的任务的运行时间.
现在考虑加入任务 \(p_j\),如果 \(s_1\geq s_2+b_{t_j}\),那么显然我们可以无缝衔接地把 \(t_j\) 安排到机器 \(A\),此时 \(s_1\leftarrow s_1+a_{t_j}\),否则,我们的机器 \(A\) 就要等 \(t_j\) 被机器 \(B\) 处理完,此时 \(s_1\leftarrow s_2+a_{t_j}\)。对于 \(s_2\),显然有 \(s_2\leftarrow s_2+b_{t_j}\)。我们只需要在不断加入任务的同时按照上述方法维护 \(s_1,s_2\) 即可。
对于机器 \(B\) 的总运行时间也是按照同样的方法进行处理,不再赘述。
设两台机器的运行时间分别为 \(T_1,T_2\),则我们当前状态的最少总时间为 \(\max\{T_1,T_2\}\)。
这个贪心看着很复杂,但其实还是不能保证正确性,比如这个数据:有 \(2\) 个 \(t_i=1\) 的任务,对应的 \((a_i,b_i)\) 分别是 \((1,100),(100,1)\),如果我们在机器 \(A\) 上先解决 \((100,1)\) 再解决 \((1,100)\),那么总时间就是 \(201\),如果先解决 \((1,100)\) 再解决 \((100,1)\),那么总时间就是 \(102\)。
我们发现错误原因是贪心时的顺序不优秀,因此我们对所有 \(t_i\) 相等的任务执行 \(2000\) 次 random_shuffle
,这样下来正确的概率就会提升,原题数据中最慢的点跑了 \(0.679s\)。此外,这题还可以用微扰法进行随机化贪心,即按照运行时间的多少进行排序,然后每次都随机交换两个元素。
[WC2018] 通道
这道题在我的「图论」树的进阶 中的“0x01 虚树” 一节中有讲到,其做法非常复杂且难以实现,这里就用另一种简洁的思路去解决它。
回忆一下树的直径的求法,我们先从任意一个点 \(x\) 开始进行 DFS,找到离 \(x\) 最远的一个点 \(y\),然后再从 \(y\) 开始 DFS,找到的最长距离即为这棵树的直径。
在这道题中,我们也可以尝试套用这样的思路进行贪心。从任意一个点 \(x\) 开始,我们分别在 \(T_1,T_2,T_3\) 三棵树中进行 DFS,然后找到一个点 \(y\) 使得 \(dis_1(x,y)+dis_2(x,y)+dis_3(x,y)\) 的值最大。接着我们就从这个新的 \(y\) 开始 DFS,找到使 \(dis_1(y,z)+dis_2(y,z)+dis_3(y,z)\) 最大的 \(z\),然后转移到 \(z\) 上面来,不过此时我们并不能证明此时 \(y,z\) 在三棵树上的距离之和就是最优解,因此我们还可以继续 DFS 下去。
注意到可能会出现这样一种情况:我们从 \(x\) 开始 DFS 找到了 \(y\),再从 \(y\) 开始 DFS 又重新找到了 \(x\),如果我们一直这样找下去,那么就会在 \(x,y\) 之间来回循环,有可能永远都找不到最优解。那么我们就考虑随机化,每次如果走到已经以前走过的点,那就随机一个新的点去 DFS。
但是这样还存在另一个问题,在上述算法中,我们一开始选择的起点 \(x\) 其实基本决定了后续搜索的走向,那么我们可以规定一个层数 \(d\),当这个起点已经向后找到了 \(d\) 个不同的点进行 DFS,那么就重新更新一个起点,按照同样的方式去循环。类似于迭代加深。
减小一下代码的常数可以卡时到 \(3.85s\),每个起点向后扩展的次数 \(d\) 可以定为 \(8\),这样就能以较高的正确率通过,实验下来最低的得分都还有 \(97\) 分,思维难度和得分的性价比几乎已经碾压正解了。