随机化技巧
概述¶
本文将对 OI/ICPC 中的随机化相关技巧做一个简单的分类,并对每个分类予以介绍。本文也将介绍一些在 OI/ICPC 中很少使用,但与 OI/ICPC 在风格等方面较为贴近的方法,这些内容前将用 (*)
标注。
这一分类并不代表广泛共识,也必定不能囊括所有可能性,因此仅供参考。
记号和约定:
\mathrm{Pr}[A] A \mathrm{E}[X] X - 赋值号
:= Y:=1926 1926 Y
用随机集合覆盖目标元素¶
庞大的解空间中有一个(或多个)解是我们想要的。我们可以尝试进行多次撒网,只要有一次能够网住目标解就能成功。
例:三部图的判定¶
问题
给定一张
对每个点
在这些限制下,对于一对邻居
- 对于所有异于
C_u,C_v X u X v \{R,G,B\}\setminus\{X,C_v\}
于是我们可以对每个
这样做,单次的正确率是
回顾:本题中“解空间”就是集合
例:CodeChef SELEDGE¶
简要题意
给定一张点、边都有非负权值的无向图,找到一个大小
观察:如果选出的边中有三条边构成一条链,则删掉中间的那条一定不劣;如果选出的边中有若干条构成环,则删掉任何一条一定不劣。
推论:最优解选出的边集,一定构成若干个不相交的菊花图(即直径不超过 2 的树)。
推论:最优解选出的边集,一定构成一张二分图。
我们对每个点等概率独立随机地染上黑白两种颜色之一,并要求这一染色方案,恰好也是最优解所对应的二分图的黑白染色方案。
尝试计算最优解符合这一要求的概率:
- 考虑一张
n \dfrac 2{2^n}=2^{1-n} - 假设最优解中每个菊花的结点数分别为
a_1,\cdots,a_l (a_1-1)+\cdots+(a_l-1)\leq K K - 从而所有菊花都被染对颜色的概率是
2^{1-a_1}\cdots 2^{1-a_l}\geq 2^{-K}
在上述要求下,尝试建立费用流模型计算最优答案:
- 建立二分图,白点在左侧并与
S T - 对于白点
v S -A_v \infty - 对于黑点
v T -A_v \infty
- 对于白点
- 对于原图中的边
(u,v,B) u v u v B - 在该图中限制流量不超过
K
用 SPFA 费用流求解的话,复杂度是
- 首先,显然 SPFA 的运行次数
\leq K - 然后,在一次 SPFA 中,任何一个结点至多入队
O(K) - 任意时刻有流量的边不会超过
3K K - 对于任何一条长为
L \dfrac L2-2 - 综合以上两条,得到任意一条增广路的长度不超过
6K+4
- 任意时刻有流量的边不会超过
- 综上,复杂度是
O\big(K^2(n+m)\big)
和上一题类似,我们需要把整个过程重复
用随机元素命中目标集合¶
我们需要确定一个集合中的任意一个元素,为此我们随机选取元素,以期能够恰好命中这一集合。
例:Gym 101550I¶
简要题意
有一张图形如:两条平行的链,加上连接两链的两条平行边。给定这张图上的若干条简单路径(每条路径表示一次通话),请你选择尽量少的边放置窃听器,以使得每条给定的路径上都有至少一个窃听器。
整张图可以拆分为一个环加上四条从环伸出去的链。对于这四条链中的任何一条(记作
- 在拦截所有
C - 在上一条的前提下,使得
C - 作这一要求的目的是尽可能地拦截恰有一个端点在
C
- 作这一要求的目的是尽可能地拦截恰有一个端点在
接着考虑链与环相接处的共计 4 条边,我们暴力枚举这些边上有没有放窃听器。显然,如果想要拦截跨越链和环的通话,在这 4 条边上放窃听器一定是最优的。现在,我们可以把通话线路分为以下几种:
- 完全在链上的通话线路。这些线路一定已经被拦截,故可以忽略。
- 跨越链和环,且已经被拦截的通话线路。它们可以忽略。
- 跨越链和环,且未被拦截的通话线路。我们可以直接截掉它在链上的部分(因为链上的窃听器放置方案已经固定了),只保留环上的部分。
- 完全在环上的通话线路。
至此,问题转化成了环上的问题。
设最优解中在环上的边集
- 先在
e - 然后从
e O(1) - 从而以
O(|S|)
我们考虑随机选取环上的一条边
分析单次复杂度:
- 观察:记
S' e' |S'|\leq |S|+1 - 从而单次复杂度
O(|S'|)=O(|S|)
分析正确率:
- 显然单次正确率
\dfrac {|S|}n n - 所以需要重复
-\dfrac n{|S|}\log\epsilon 1-\epsilon
综上,该算法的复杂度
例:CSES 1685 New Flight Routes¶
简要题意
给定一张有向图,请你加最少的边使得该图强连通,需 输出方案。
先对原图进行强连通缩点。我们的目标显然是使每个汇点能到达每个源点。
不难证明,我们一定只会从汇点到源点连边,因为任何其他的连边,都能对应上一条不弱于它的、从汇点到源点的连边。
我们的一个核心操作是,取汇点
不难发现,上述操作能够达到目标 I 的充要条件是:
- 有了这个充要条件还难以直接得到算法,主要的原因是连边
t\to s (s',t')
注意到我们关于源汇点间的关系知之甚少(甚至连快速查询一对
观察:不满足目标 I 的
- 理由:对于每一对这样的
(s,t) s,t - 作出这一观察的动机是,要想将存在性结论应用于算法,前置步骤往往是把定性的结果加强为定量的结果。
推论:等概率随机选取
- 注意到这个结论严格强于先前给出的存在性结论。
推论:等概率独立随机地连续选取
- 理由:
而连续选完
算法伪代码
while(n>1 and m>1):
randomly choose k=min(n,m)/2 pairs (s,t)
add edge t->s for all these pairs
if new_n>n-k or new_m>m-k:
roll_back()
solve_trivial()
复杂度
回顾:我们需要确定任意一对能够实现目标 I 的二元组
用随机化获得随机数据的性质¶
如果一道题的数据随机生成,我们可能可以利用随机数据的性质解决它。而在有些情况下,即使数据并非随机生成,我们也可以通过随机化来给其赋予随机数据的某些特性,从而帮助解决问题。
例:随机增量法¶
随机生成的元素序列可能具有“前缀最优解变化次数期望下很小”等性质,而随机增量法就通过随机打乱输入的序列来获得这些性质。
详见 随机增量法。
例:TopCoder MagicMolecule 随机化解法¶
简要题意
给定一张
不难想到折半搜索。把点集均匀分成左右两半
- 注意到可以
O(1) f_{L,k} d L - 假设最优解中
d f_{L\setminus \{d\},k} - 假设最优解中
d f_{L\cap N(d),k}+\textit{value}(d) N(d) d - 别忘了还要用
f_{L,k+1} f_{L,k}
- 假设最优解中
这个解法会超时。尝试优化:
- 平分点集时均匀随机地划分。这样的话,最优解的点集
C_{res} |C_{res}\cap V_L|=|C_{res}\cap V_R| - 当然,
|C_{res}| - 实验发现,随机尝试约 20 次就能以很大概率有至少一次满足该性质。也就是说,如果我们的算法依赖于“
C_{res}
- 当然,
- 有了这一性质,我们就可以直接钦定左侧团
L C_R \geq \dfrac n3 f - 因为只需考虑大小
\geq \dfrac n3 L C_R 1.8\cdot 10^6
- 现在的瓶颈变成了求单侧的某一子集的权值和,因为这需要
O\big(2^{|V_L|}+2^{|V_R|}\big) - 解决方案:在
V_L,V_R
- 解决方案:在
- 这样即可通过本题。
回顾:一个随机的集合有着“在划分出的两半的数量差距不会太悬殊”这一性质,而我们通过随机划分获取了这个性质。
随机化用于哈希¶
例:UOJ #207 共价大爷游长沙¶
简要题意
维护一棵动态变化的树,和一个动态变化的结点二元组集合。你需要支持:
- 删边、加边。保证得到的还是一棵树。
- 加入/删除某个结点二元组。
- 给定一条边
e (s,t) e s,t
对图中的每条边
哈希的方式是,对每个
这样的话,任何一个固定的集合的哈希值一定服从
- 单个
H_{(a,b)} - 两个独立且服从
R R
从而该算法的正确率是有保障的。
至于如何维护这个哈希值,使用 LCT 即可。
例:CodeChef PANIC 及其错误率分析¶
本题的大致解法:
- 可以证明1
S(N) N O(K) - 用 BM 算法求出该递推式。
- 借助递推式,用凯莱哈密顿定理计算出
S(N)
这里仅关注第二部分,即如何求一个矩阵序列的递推式。所以我们只需考虑下述问题:
问题
给定一个矩阵序列,该序列在模
如果一系列矩阵服从一个递推式
解决方案:给矩阵的每一位
错误率分析:
- 假设上述做法求得了不同于
F F l F' - 因为矩阵序列不服从
F' (i,j) S_{i,j} N F'
- 假设
(i,j)
- 显然这仅当
x_{i,j}=0 P^{-1} - 如果有多个不服从的位置呢?
- 对每个这样的位置
(i,j) T_{i,j} R:=\{0,1,\cdots,P-1\} - 若干个互相独立的、服从
R R - 从而这种情况下的错误率也是
P^{-1}
- 对每个这样的位置
例:UOJ #552 同构判定鸭 及其错误率分析¶
简要题意
给定两张边权为小写字母的有向图
令
要判断是否存在长度 *
这里表示每一个结点,例如
接下来考虑具体的哈希方式。注意到常规的哈希方法——即把串 {"ab","cd"}
与集合 {"cb","ad"}
的哈希值是一样的,不论
上述做法的问题在于,一个串的哈希值是一个和式,从而其中的每一项可以拆出来并重组。为避免这一问题,我们考虑把哈希值改为一个连乘式。此外,乘法交换律会使得不同的位不可区分,为避免这一点我们要为不同的位赋予不同的权值。
对每一个二元组
(*)Schwartz-Zippel 引理
令
如果你不知道域是什么
你只需记得这两样东西都是域:
- 模质数的剩余系,以及其上的各种运算。
- 实数集,以及其上的各种运算。
推论:若
记
假如两个不同的字符串多重集的哈希值相同,则有两种可能:
P_0\equiv P_1\pmod {Q} P_0,P_1 Q P_0\not\equiv P_1\pmod {Q}, P_0(x_{*,*})\equiv P_1(x_{*,*})\pmod {Q} P_0,P_1 \{x_{*,*}\}
分析前者发生的概率:
- 观察:对于任意的
A\neq B; A,B\leq N Q\leq Q_{max}
- 这是因为:使
A\equiv B Q Q\big|(A-B) Q \omega(A-B)\leq \log_2 N Q_{max} \Theta\Big(\dfrac {Q_{max}}{\log Q_{max}}\Big) - 在上述观察中取
A,B A\neq B P_0,P_1 G_0,G_1 A,B\leq (m_1+m_2)^{L}
- 所以取
Q_{max}\approx 10^{12}
分析后者发生的概率:
- 在 Schwartz-Zippel 引理中:
- 取域
F Q - 取
f(x_{*,*})=P_0(x_{*,*})-P_1(x_{*,*}) L - 取
S=F
- 取域
- 得到:所求概率
\leq \dfrac LQ
注意到我们需要对每个
实践上我们不必随机选取模数,因为——比如说——用自己的生日做模数的话,实际上已经相当于随机数了。
例:(*)子矩阵不同元素个数¶
问题
给定
允许
引理:令
- 证明:考虑一个单位圆,其上分布着 相对位置 均匀随机的
k+1 0,X_1,X_2,\cdots,X_k \min\limits_i X_i k+1 \dfrac 1{k+1}
我们取
考虑采用某个哈希函数,将矩阵中每个元素都均匀、独立地随机映射到
于是我们得到了算法:
- 给矩阵中元素赋
[0,1] map
和随机数生成器实现,即每遇到一个新的未出现过的值就给它随机一个哈希值。 - 回答询问时设法求出子矩阵中哈希值的最小值
M \dfrac 1M-1
然而,这个算法并不能令人满意。它的输出值的期望是
也就是说,我们不能直接把
怎么算期望值?多次随机取平均。
我们用
实验发现取
最后,怎么求子矩阵最小值?用二维 S-T 表即可,预处理
随机化在算法中的其他应用¶
随机化的其他作用还包括:
- 防止被造数据者用针对性数据卡掉。例如在搜索时随机打乱邻居的顺序。
- 保证算法过程中进行的“操作”具有(某种意义上的)均匀性。例如 模拟退火 算法。
在这些场景下,随机化常常(但并不总是)与乱搞、骗分等做法挂钩。
例:「TJOI2015」线性代数¶
本题的标准算法是网络流,但这里我们采取这样的乱搞做法:
- 每次随机一个位置,把这个位置取反,判断大小并更新答案。
代码
#include <algorithm>
#include <cstdlib>
#include <iostream>
int n;
int a[510], b[510], c[510][510], d[510];
int p[510], q[510];
int maxans = 0;
void check() {
memset(d, 0, sizeof d);
int nowans = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) d[i] += a[j] * c[i][j];
for (int i = 1; i <= n; i++) nowans += (d[i] - b[i]) * a[i];
maxans = std::max(maxans, nowans);
}
int main() {
srand(19260817);
std::cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) std::cin >> c[i][j];
for (int i = 1; i <= n; i++) std::cin >> b[i];
for (int i = 1; i <= n; i++) a[i] = 1;
check();
for (int T = 1000; T; T--) {
int tmp = rand() % n + 1;
a[tmp] ^= 1;
check();
}
std::cout << maxans << '\n';
}
例:(*)随机堆¶
可并堆最常用的写法应该是左偏树了,通过维护树高让树左偏来保证合并的复杂度。然而维护树高有点麻烦,我们希望尽量避开。
那么可以考虑使用随机堆,即不按照树高来交换儿子,而是随机交换。
代码
struct Node {
int child[2];
long long val;
} nd[100010];
int root[100010];
int merge(int u, int v) {
if (!(u && v)) return u | v;
int x = rand() & 1, p = nd[u].val > nd[v].val ? u : v;
nd[p].child[x] = merge(nd[p].child[x], u + v - p);
return p;
}
void pop(int &now) { now = merge(nd[now].child[0], nd[now].child[1]); }
随机堆对堆的形态没有任何硬性或软性的要求,合并操作的期望复杂度对任何两个堆(作为 merge
函数的参数)都成立。下证。
期望复杂度的证明
将证,对于任意的堆
- 注意到在前述过程中合并堆
A,B O\big(h(A)+h(B)\big)
证明采用数学归纳。边界情况是
假设
证毕。
与随机性有关的证明技巧¶
以下列举几个比较有用的技巧。
自然,这寥寥几项不可能就是全部;如果你了解某种没有列出的技巧,那么欢迎补充。
概率上界的分析¶
随机算法的正确性或复杂度经常依赖于某些“坏事件”不发生或很少发生。例如,快速排序的复杂度依赖于“所选的 pivot
元素几乎是最小或最大元素”这一坏事件较少发生。
本节介绍几个常用于分析“坏事件”发生概率的工具。
工具¶
Union Bound:记
-
即:坏事件中至少一者发生的概率,不超过每一个的发生概率之和。
-
证明:回到概率的定义,把事件看成单位事件的集合,发现这个结论是显然的。
-
这一结论还可以稍作加强:
- 坏事件中至少一者发生的概率,不小于 每一个的发生概率之和,减掉每两个同时发生的概率之和。
- 坏事件中至少一者发生的概率,不超过 每一个的发生概率之和,减掉每两个同时发生的概率之和,加上每三个同时发生的概率之和。
- ……
- 随着层数越来越多,交替出现的上界和下界也越来越紧。这一系列结论形式上类似容斥原理,证明过程也和容斥类似,这里略去。
自然常数的使用:
-
左式关于
n\geq 1 +\infty \dfrac 1e -
这告诉我们,如果
n 1-\dfrac 1n \dfrac 1e
(*) Hoeffding 不等式:若
- 这一不等式限制了随机变量偏离其期望值的程度。从经验上讲,如果
\mathrm{E}[X] a_1+\cdots+a_n
例子¶
例:抽奖问题
一个箱子里有
与该问题类似的模型经常出现在随机算法的复杂度分析中。
解答
假如只有一个奖球,则抽取
现在有
例:(*)随机选取一半元素
给出一个算法,从
解法
首先可以想到这样的算法:
- 通过抛
n - 不断重复这一过程,直到选出的子集大小恰好为
\dfrac n2 - 注意到大小为
\dfrac n2 \dfrac 1n \leq n
- 注意到大小为
这一算法期望需要抛
另一个算法:
- 我们可以通过抛期望
2\lceil\log_2 n\rceil n - 具体方法:随机生成
\lceil\log_2 n\rceil n
- 具体方法:随机生成
- 然后我们从所有元素中选一个,再从剩下的元素中再选一个,以此类推,直到选出
\dfrac n2
这一算法期望需要抛
将两个算法缝合起来:
- 先用第一个算法随机得到一个子集。
- 如果该子集大小不到
\dfrac n2 \dfrac n2 - 如果该子集大小超过
\dfrac n2 \dfrac n2
尝试分析第二、第三步所需的操作次数(即添加/删除元素的次数):
- 记 01 随机变量
X_i i X:=X_1+\cdots+X_n \big|X-\mathrm{E}[X]\big| t=c\cdot\sqrt n c \mathrm{Pr}\Big[\big|X-\mathrm{E}[X]\big|\geq t\Big]\leq 2e^{-c^2} \Theta(\sqrt n)
至此我们已经说明:该算法可以以很大概率保证抛硬币次数在
- 其中
n \Theta(\sqrt n\log n) \Theta(\sqrt n)
计算期望复杂度
我们再从另一个角度分析,尝试计算该算法的期望抛硬币次数。
用 Hoeffding 不等式求第二、第三步中操作次数期望值的上界:
从而第二、第三步所需抛硬币次数的期望值是
综上,该算法期望需要抛
「耦合」思想¶
「耦合」思想常用于同时处理超过一个有随机性的对象,或者同时处理随机的对象和确定性的对象。
引子:随机图的连通性¶
问题
对于
这个结论看起来再自然不过,但严格证明却并不那么容易。
证明思路
我们假想这两张图分别使用了一个 01 随机数生成器来获知每条边存在与否,其中
现在我们把两个生成器合二为一。考虑随机数生成器
容易验证,这样生成的
这一段证明中用到的思想被称为“耦合”,可以从字面意思来理解这种思想。本例中它体现为把两个本来独立的随机过程合二为一。
应用:NERC 2019 Problem G: Game Relics¶
简要题意
有若干个物品,每个物品有一个价格
- 选择一个未拥有的物品
i c_i - 花
x \dfrac x2 x
问最优策略下的期望花费。
观察:如果选择抽物品,就一定会一直抽直到获得新物品为止。
- 理由:如果抽一次没有获得新物品,则新的局面和抽物品之前的局面一模一样,所以如果旧局面的最优行动是“抽一发”,则新局面的最优行动一定也是“再抽一发”。
我们可以计算出
期望代价的计算
显然
引理:如果一枚硬币有
- 感性理解:
\dfrac 1p \cdot p = 1 - 这种感性理解可以通过 大数定律 严谨化,即考虑
n\to \infty - 另一种可行的证法是,直接把期望的定义带进去暴算。推导细节略。
显然抽一次得到新物品的概率是
结论:最优策略一定是先抽若干次,再买掉所有没抽到的物品。
这个结论符合直觉,因为
证明
先考虑证明一个特殊情况。将证:
- 随机过程
A x - ……一定不优于……
- 随机过程
B x x
考虑让随机过程
显然,此时
A y B y
因此
显然
然后可以通过数学归纳把这一结论推广到一般情况。具体地说,每次我们找到当前策略中的最后一次购买,然后根据上述结论,把这一次购买移到最后一定不劣。细节略。
基于这个结论,我们再次等价地转化问题:把“选一个物品并支付对应价格购买”的操作,改成“随机选一个未拥有的物品并支付对应价格购买”。等价性的理由是,既然购买只是用来扫尾的,那选到哪个都无所谓。
现在我们发现,“抽取”和“购买”,实质上已经变成了相同的操作,区别仅在于付出的价格不同。选择购买还是抽取,对于获得物品的顺序毫无影响,而且每种获得物品的顺序都是等可能的。
观察:在某一时刻,我们应当选择买,当且仅当下一次抽取的代价(由已经抽到的物品数确定)大于剩余物品的平均价格(等于的话则任意)。
- 可以证明,随着时间的推移,抽取代价的增速一定不低于剩余物品均价的增速。这说明从抽到买的“临界点”只有一个,进一步验证了先前结论。
最后,我们枚举所有可能的局面(即已经拥有的元素集合),算出这种局面出现的概率(已有元素的排列方案数除以总方案数),乘上当前局面最优决策的代价(由拥有元素个数和剩余物品总价确定),再加起来即可。这个过程可以用背包式的 DP 优化,即可通过本题。
回顾:可以看到,耦合的技巧在本题中使用了两次。第一次是在证明过程中,令两个随机过程使用同一个随机源;第二次是把购买转化成随机购买(即引入随机源),从而使得购买和抽取这两种操作实质上“耦合”为同一种操作(即令抽取和购买操作共享一个随机源)。
参考资料¶
buildLast update and/or translate time of this article,Check the history
editFound smelly bugs? Translation outdated? Wanna contribute with us? Edit this Page on Github
peopleContributor of this article Ir1d, partychicken, ouuan, Marcythm, TianyiQ
translateTranslator of this article Visit the original article!
copyrightThe article is available under CC BY-SA 4.0 & SATA ; additional terms may apply.