随机化技巧
概述¶
本文将对 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 - 作出这一观察的动机是,要想将存在性结论应用于算法,前置步骤往往是把定性的结果加强为定量的结果。
推论:等概率随机选取
- 注意到这个结论严格强于先前给出的存在性结论。
推论:等概率独立随机地连续选取
- 理由:
而连续选完
算法伪代码
1 2 3 4 5 6 | 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」线性代数¶
本题的标准算法是网络流,但这里我们采取这样的乱搞做法:
- 每次随机一个位置,把这个位置取反,判断大小并更新答案。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | #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';
}
|
例:(*)随机堆¶
可并堆最常用的写法应该是左偏树了,通过维护树高让树左偏来保证合并的复杂度。然而维护树高有点麻烦,我们希望尽量避开。
那么可以考虑使用随机堆,即不按照树高来交换儿子,而是随机交换。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 优化,即可通过本题。
回顾:可以看到,耦合的技巧在本题中使用了两次。第一次是在证明过程中,令两个随机过程使用同一个随机源;第二次是把购买转化成随机购买(即引入随机源),从而使得购买和抽取这两种操作实质上“耦合”为同一种操作(即令抽取和购买操作共享一个随机源)。
参考资料¶
build本页面最近更新:,更新历史
edit发现错误?想一起完善? 在 GitHub 上编辑此页!
people本页面贡献者:Ir1d, partychicken, ouuan, Marcythm, TianyiQ
copyright本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用