OSDN Git Service

fix format & feat some techniques
authorTianyi Qiu <gordanqiu@outlook.com>
Sun, 1 Nov 2020 15:05:34 +0000 (23:05 +0800)
committerTianyi Qiu <gordanqiu@outlook.com>
Sun, 1 Nov 2020 15:05:34 +0000 (23:05 +0800)
docs/misc/rand-technique.md
docs/misc/random.md

index 97994dd..6b1cbe3 100644 (file)
@@ -4,20 +4,27 @@ author: Ir1d, partychicken, ouuan, Marcythm, TianyiQ
 
 前置知识:[随机函数](../misc/random.md)和[概率初步](../math/expectation.md)
 
-本页面将对 OI/ICPC 中的随机化技巧做一个简单的分类,并对每个分类予以介绍。本文也将介绍一些在 OI/ICPC 中尚未广泛使用,但与 OI/ICPC 在风格等方面十分贴近的技巧,这些内容前将用 `*` 标注。
+本文将对 OI/ICPC 中的随机化相关技巧做一个简单的分类,并对每个分类予以介绍。本文也将介绍一些在 OI/ICPC 中尚未使用,但与 OI/ICPC 在风格等方面较为贴近的技巧,这些内容前将用 `*` 标注。
 
 这一分类并不代表广泛共识,或许也并不能囊括所有可能性,因此仅供参考。
 
+ **记号和约定** :
+
+ -  $\mathrm{Pr}[A]$ 表示事件 $A$ 发生的概率。
+ -  $\mathrm{E}[X]$ 表示随机变量 $X$ 的期望。
+
 ## 随机化用于非完美算法
 
 随机化被广泛应用于 OI 中各种骗分、偷懒的场景下。在这种场景下,随机化的作用通常包括:
-- 防止被出题人用针对性数据卡掉。例如在搜索时随机打乱邻居的顺序。
-- 保证算法过程中进行的“操作”具有(某种意义上的)均匀性。例如 [模拟退火](../misc/simulated-annealing.md) 算法。
+
+-  防止被出题人用针对性数据卡掉。例如在搜索时随机打乱邻居的顺序。
+-  保证算法过程中进行的“操作”具有(某种意义上的)均匀性。例如 [模拟退火](../misc/simulated-annealing.md) 算法。
 
 ### 例:[「TJOI2015」线性代数](https://loj.ac/problem/2100) 
 
 本题的标准算法是网络流,但这里我们采取这样的做法:
-- 每次随机一个位置,把这个位置取反,判断大小并更新答案。
+
+-  每次随机一个位置,把这个位置取反,判断大小并更新答案。
 
 ??? note "代码"
     ```cpp
@@ -82,27 +89,166 @@ author: Ir1d, partychicken, ouuan, Marcythm, TianyiQ
     void pop(int &now) { now = merge(nd[now].child[0], nd[now].child[1]); }
     ```
 
-## 与随机性有关的结论证明
+## 与随机性有关的证明技巧
+
+以下列举几个比较有用的技巧。
+
+自然,这寥寥几项不可能就是全部;如果你了解某种没有列出的技巧,那么欢迎补充。
+
+### 概率上界的分析
+
+随机算法的正确性或复杂度经常依赖于某些“坏事件”不发生或很少发生。例如,快速排序的复杂度依赖于“所选的 `pivot` 元素恰好是最小或最大元素”这一坏事件较少发生。本节介绍几个常用于分析“坏事件”发生概率的工具。
+
+#### 工具
+
+ **Union Bound** :记 $A_{1\cdots m}$ 为坏事件,则
+
+ $$
+ \mathrm{Pr}\Big[\bigcup\limits_{i=1}^m A_i \Big]\leq \sum\limits_{i=1}^m \mathrm{Pr}[A_i]
+ $$
+
+-  即:坏事件中至少一者发生的概率,不超过每一个的发生概率之和。
+-  证明:回到概率的定义,把事件看成单位事件的集合,发现这个结论是显然的。
+-  这一结论还可以稍作加强:
+    -  坏事件中至少一者发生的概率, **不小于** 每一个的发生概率之和,减掉每两个同时发生的概率之和。
+    -  坏事件中至少一者发生的概率, **不超过** 每一个的发生概率之和,减掉每两个同时发生的概率之和,加上每三个同时发生的概率之和。
+    -  ……
+    -  随着层数越来越多,交替出现的上界和下界也越来越紧。这一系列结论形式上类似容斥原理,证明过程也和容斥类似,这里略去。
+
+**自然常数的使用** : $\Big(1-\dfrac 1n\Big)^n\leq \dfrac 1e,\forall n\geq0$
+
+-  左式关于 $n\geq 0$ 单调递增且在 $+\infty$ 处的极限是 $\dfrac 1e$ ,因此有这个结论。
+-  这告诉我们,如果 $n$ 个独立的坏事件,每个的发生概率为 $1-\dfrac 1n$ ,则它们全部发生的概率至多为 $\dfrac 1e$ 。
+
+**(\*) Hoeffding** 不等式:若 $X_{1\cdots n}$  为互相独立的实随机变量且 $X_i\in [a_i,b_i]$ ,记随机变量 $X:=\sum\limits_{i=1}^n X_i$ ,则
+
+$$
+\mathrm{Pr}\Big[\big|X-\mathrm{E}[X]\big|\geq t\Big]\leq2\exp {-\dfrac {t^2}{\sum\limits_{i=1}^n (b_i-a_i)^2}}
+$$
+
+-  这一不等式限制了随机变量偏离其期望值的程度。从经验上讲,如果 $\mathrm{E}[X]$ 不太接近 $a_1+\cdots+a_n$ ,则该不等式给出的界往往相对比较紧;如果非常接近的话,给出的界则很松,此时更好的选择是使用 [Chernoff Bound](https://en.jinzhao.wiki/wiki/Chernoff_bound) 。
+
+#### 例子
+
+???+ note "例:抽奖问题"
+    一个箱子里有 $n$ 个球,其中恰有 $k$ 个球对应着大奖。你要进行若干次独立、等概率的随机抽取,每次抽完之后会把球放回箱子。请问抽多少次能保证以至少 $1-\epsilon$ 的概率,满足 **每一个** 奖球都被抽到至少一次?
+
+??? note "解答"
+    假如只有一个奖球 ,则抽取 $M:=-n\log\epsilon$ 次即可保证,因为 $M$ 次全不中的概率 $\Big(1-\dfrac 1n\Big)^{n\cdot (-\log\epsilon)}\leq e^{\log\epsilon}=\epsilon$ 。
+    
+    现在有 $k>1$ 个奖球,那么根据 Union Bound ,我们只需保证每个奖球被漏掉的概率都不超过 $\dfrac \epsilon k$ 即可。于是答案是 $-n\log\dfrac \epsilon k$ 。
+
+???+ note "例:随机选取一半元素"
+    给出一个算法,从 $n$ 个元素中等概率随机选取一个大小为 $\dfrac n2$ 的子集,保证 $n$ 是偶数。你能使用的唯一的随机源是一枚均匀硬币,同时请你尽可能减少抛硬币的次数。
+
+??? note "解法"
+    首先可以想到这样的算法:
+
+    - 通过抛 $n$ 次硬币,可以从所有子集中等概率随机选一个。
+    - 不断重复这一过程,直到选出的子集大小恰好为 $\dfrac n2$ 。
+        - 注意到大小为 $\dfrac n2$ 的子集至少占所有子集的 $\dfrac 1n$ ,因此重复次数的期望值 $\leq n$ 。
+
+    这一算法期望需要抛 $n^2$ 次硬币。
+
+    另一个算法:
+
+    - 我们可以通过抛期望 $2\lceil\log_2 n\rceil$ 次硬币来实现随机 $n$ 选 1 。
+        - 具体方法:随机生成 $\lceil\log_2 n\rceil$ 位的 2 进制数,如果大于等于 $n$ 则重新随机,否则选择对应编号(编号从 0 开始)的元素并结束过程。
+    - 然后我们从所有元素中选一个,再从剩下的元素中再选一个,以此类推,直到选出 $\dfrac n2$ 个元素为止。
+
+    这一算法期望需要抛 $n\lceil\log_2 n\rceil$ 次硬币。
 
-这一类应用可以细分为两种:
-1. **类型一**:利用某些技巧来帮助分析本就具有随机性的对象。
-2. **(\*)类型二**:在确定性的问题中人为引入随机性,从而帮助分析。
+    将两个算法缝合起来:
 
-其中,类型二也叫做「[概率方法](https://en.wikipedia.org/wiki/Probabilistic_method)」,是组合数学中的一类重要方法。
+    - 先用第一个算法随机得到一个子集。
+    - 如果该子集大小不到 $\dfrac n2$ ,则利用第二个算法将其补到 $\dfrac n2$ 。
+    - 如果该子集大小超过 $\dfrac n2$ ,则利用第二个算法将其削到 $\dfrac n2$ 。
 
-限于篇幅和笔者的垃圾水平,本文中只能举几个例子以对这两种方法略窥一斑。
+    尝试分析第二步所需的操作次数:
 
-### 例(类型一):随机图的连通性
+    - 记 01 随机变量 $X_i$ 表示 $i$ 是否被选入初始的子集,令 $X:=X_1+\cdots+X_n$ 表示子集大小,则第二步所需的操作次数等于 $\big|X-\mathrm{E}[X]\big|$ 。在 Hoeffding 不等式中取 $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+o(n)$ 。
 
-阅读这一例子前,建议先阅读 [概率 & 期望](../math/expectation.md) 以了解随机变量、独立性等概率论的基础概念。
+    ??? mdui-shadow-6 "闲得无聊想算精确的式子"
+        尝试用 Hoeffding 不等式求第二步中操作次数期望值的上界:
+        $$
+        E\Big[\big|X-E[X]\big|\Big]=\int\limits_0^\infty \mathrm{Pr}\Big[\big|X-\mathrm{E}[X]\big|\geq t\Big]\mathrm{d}t\leq2\int\limits_0^\infty \exp {-\dfrac {t^2}n}\mathrm{d}t=\sqrt{\pi n}
+        $$
+        
+        从而第二步所需抛硬币次数的期望值是 $\sqrt{\pi n}\cdot2\lceil\log_2 n\rceil$ 。
 
-求证:对于 $n \in \mathbf{N}^*; p,q\in (0,1)$ 且 $p\leq q$ ,求证随机图 $G_1(n,p)$ 的连通分量个数的期望值不超过 $G_2(n,q)$ 的连通分量个数的期望值。这里 $G(n,\alpha)$ 表示一张 $n$ 个结点的简单无向图 $G$ ,其中 $\frac {n(n-1)}2$ 条可能的边中的每一条都有 $\alpha$ 的概率出现,且这些概率互相独立。
+        综上,该算法期望需要抛 $n+2\sqrt{\pi n}\lceil\log_2 n\rceil$ 次硬币。
+
+### 「耦合」思想
+
+「耦合」思想常用于同时处理超过一个有随机性的对象,或者同时处理随机的对象和确定性的对象。
+
+#### 引子:随机图的连通性
+
+求证:对于 $n \in \mathbf{N}^*; p,q\in [0,1]$ 且 $q\leq p$ ,随机图 $G_1(n,p)$ 的连通分量个数的期望值不超过随机图 $G_2(n,q)$ 的连通分量个数的期望值。这里 $G(n,\alpha)$ 表示一张 $n$ 个结点的简单无向图 $G$ ,其中 $\dfrac {n(n-1)}2$ 条可能的边中的每一条都有 $\alpha$ 的概率出现,且这些概率互相独立。
 
 这个结论看起来再自然不过,但严格证明却并不那么容易。
 
-??? note "证明的大致思路"
-    我们假想这两张图分别使用了一个 01 随机数生成器来获知每条边存在与否,其中 $G_1$ 的生成器 $T_1$ 每次以 $p$ 的概率输出 1 ,$G_2$ 的生成器 $T_2$ 每次以 $q$ 的概率输出 1 。这样,要构造一张图,就只需把对应的生成器运行 $\frac {n(n-1)}2$ 遍即可。
-    现在我们把两个生成器合二为一。考虑随机数生成器 $T$ ,每次以 $q$ 的概率输出 0 ,以 $p-q$ 的概率输出 1 ,以 $1-p$ 的概率输出 2 。如果我们将这个 $T$ 运行 $\frac {n(n-1)}2$ 遍,就能同时构造出 $G_1$ 和 $G_2$ 。具体地说,如果输出是 0 ,则认为 $G_1$ 和 $G_2$ 中都没有当前考虑的边;如果输出是 1 ,则认为只有 $G_1$ 中有当前考虑的边;如果输出是 2 ,则认为 $G_1$ 和 $G_2$ 中都有当前考虑的边。
+??? note "大致思路"
+    我们假想这两张图分别使用了一个 01 随机数生成器来获知每条边存在与否,其中 $G_1$ 的生成器 $T_1$ 每次以 $p$ 的概率输出 1 ,$G_2$ 的生成器 $T_2$ 每次以 $q$ 的概率输出 1 。这样,要构造一张图,就只需把对应的生成器运行 $\dfrac {n(n-1)}2$ 遍即可。
+
+    现在我们把两个生成器合二为一。考虑随机数生成器 $T$ ,每次以 $q$ 的概率输出 0 ,以 $p-q$ 的概率输出 1 ,以 $1-p$ 的概率输出 2 。如果我们将这个 $T$ 运行 $\dfrac {n(n-1)}2$ 遍,就能同时构造出 $G_1$ 和 $G_2$ 。具体地说,如果输出是 0 ,则认为 $G_1$ 和 $G_2$ 中都没有当前考虑的边;如果输出是 1 ,则认为只有 $G_1$ 中有当前考虑的边;如果输出是 2 ,则认为 $G_1$ 和 $G_2$ 中都有当前考虑的边。
+
     容易验证,这样生成的 $G_1$ 和 $G_2$ 符合其定义,而且在每个实例中,$G_2$ 的边集都是 $G_1$ 边集的子集。因此在每个实例中,$G_2$ 的连通分量个数都不小于 $G_1$ 的连通分量个数;那么期望值自然也满足同样的大小关系。
 
-这一段证明中用到的技巧被称为“耦合”。本例中它体现为把两个本来独立的“随机数生成器”合二为一。
\ No newline at end of file
+这一段证明中用到的思想被称为“耦合”,可以从字面意思来理解这种思想。本例中它体现为把两个本来独立的随机过程合二为一。
+
+#### 应用: [NERC 2019 Problem G: Game Relics](https://codeforces.com/contest/1267/problem/G)
+
+观察:如果选择抽物品,就一定会一直抽直到获得新物品为止。
+
+-  理由:如果抽一次没有获得新物品,则新的局面和抽物品之前的局面一模一样,所以如果旧局面的最优行动是“抽一发”,则新局面的最优行动一定也是“再抽一发”。
+
+我们可以计算出 $f_k$ 表示:如果当前已经拥有 $k$ 个不同物品,则期望要花多少钱才能抽到新物品。根据刚才的观察,我们可以直接把 $f_k$ 当作一个固定的代价,即转化为“每次花 $f_k$ 块钱随机获得一个新物品”。
+
+??? note "期望代价的计算"
+    显然 $f_k=\dfrac x2 \cdot (R-1)+x$ ,其中 $R$ 表示要得到新物品期望的抽取次数。
+    
+    引理:如果一枚硬币有 $p$ 的概率掷出正面,则首次掷出正面所需的期望次数为 $\dfrac 1p$ 。
+
+    -  感性理解: $\dfrac 1p \cdot p = 1$ ,所以扔这么多次期望得到 1 次正面,看起来就比较对。
+    -  这种感性理解可以通过 [大数定律](https://en.wikipedia.org/wiki/Law_of_large_numbers) 严谨化,即考虑 $n\to \infty$ 次“不断抛硬币直到得到正面”的实验。推导细节略。
+    -  另一种可行的证法是,直接把期望的定义带进去暴算。推导细节略。
+
+    显然抽一次得到新物品的概率是 $\dfrac {n-k}n$ ,那么 $R=\dfrac n{n-k}$ 。
+
+结论:最优策略一定是先抽若干次,再买掉所有没抽到的物品。
+
+这个结论符合直觉,因为 $f_k$ 是关于 $k$ 递增的,早抽似乎确实比晚抽看起来好一点。
+
+??? note "证明"
+    先考虑证明一个特殊情况。将证:
+
+    -  随机过程 $A$ :先买物品 $x$ ,然后不断抽直到得到所有物品
+    -  ……一定不优于……
+    -  随机过程 $B$ :不断抽直到得到 $x$ 以外的所有物品,然后如果还没有 $x$ 则买下来
+    
+    考虑让随机过程 $A$ 和随机过程 $B$ 使用同一个随机数生成器。即, $A$ 的第一次抽取和 $B$ 的第一次抽取会抽到同一个元素,第二次、第三次……也是一样。
+    
+    显然,此时 $A$ 和 $B$ 抽取的次数必定相等。对于一个被 $A$ 抽到的物品 $y\neq x$ ,观察到:
+
+    -  $A$ 中抽到 $y$ 时已经持有的物品数,一定大于等于 $B$ 中抽到 $y$ 时已经持有的物品数。
+
+    因此 $B$ 的单次抽取代价不高于 $A$ 的单次抽取代价,进而抽取的总代价也不高于 $A$ 。
+
+    显然 $B$ 的购买代价同样不高于 $A$ 。综上, $B$ 一定不劣于 $A$ 。
+
+    然后可以通过数学归纳把这一结论推广到一般情况。具体地说,每次我们找到当前策略中的最后一次购买,然后根据上述结论,把这一次购买移到最后一定不劣。细节略。
+
+基于这个结论,我们再次等价地转化问题:把“选一个物品并支付对应价格购买”的操作,改成“随机选一个未拥有的物品并支付对应价格购买”。等价性的理由是,既然购买只是用来扫尾的,那选到哪个都无所谓。
+
+现在我们发现,“抽取”和“购买”,实质上已经变成了相同的操作,区别仅在于付出的价格不同。选择购买还是抽取,对于获得物品的顺序毫无影响,而且每种获得物品的顺序都是等可能的。
+
+观察:在某一时刻,我们应当选择买,当且仅当下一次抽取的代价(由已经抽到的物品数确定)大于剩余物品的平均价格(等于的话则任意)。
+
+-  可以证明,随着时间的推移,抽取代价的增速一定不低于剩余物品均价的增速。这说明从抽到买的“临界点”只有一个,进一步验证了先前结论。
+
+最后,我们枚举所有可能的局面(即已经拥有的元素集合),算出这种局面出现的概率(已有元素的排列方案数除以总方案数),乘上当前局面最优决策的代价(由拥有元素个数和剩余物品总价确定),再加起来即可。这个过程可以用背包式的DP优化,即可通过本题。
+
+**小结** :可以看到,耦合的技巧在本题中使用了两次。第一次是在证明过程中,令两个随机过程使用同一个随机源;第二次是把购买转化成随机购买(即引入随机源),从而使得购买和抽取这两种操作实质上“耦合”为同一种操作(即令抽取和购买操作共享一个随机源)。
\ No newline at end of file
index 26585a5..5425f45 100644 (file)
@@ -8,13 +8,14 @@
 
 现有的计算机的运算过程都是确定性的,因此,仅凭借算法来生成真正 **不可预测** 、 **不可重复** 的随机数列是不可能的。
 
-然而在绝大部分情况下,我们都不需要如此强的随机性,而只需要所生成的数列在统计学上具有随机数列的种种特征(比如均匀分布、互相独立等等)。这样的数列即称为 **伪随机数** 列。
+然而在绝大部分情况下,我们都不需要如此强的随机性,而只需要所生成的数列在统计学上具有随机数列的种种特征(比如均匀分布、互相独立等等)。这样的数列即称为 **伪随机数** 列。
 
 随机数与伪随机数在实际生活和算法中的应用举例:
-- 抽样调查时往往只需使用伪随机数。这是因为我们本就只关心统计特征。
-- 网络安全中往往要用到(比刚刚提到的伪随机数)更强的随机数。这是因为攻击者可能会利用可预测性做文章。
-- 在笔者了解范围内,OI/ICPC 中用到的所有随机算法,都只需要伪随机数。这是因为,这些算法往往是 通过引入随机数 来把概率引入复杂度分析,从而降低复杂度。这本质上依然只利用了随机数的统计特征。
-- 某些(未被引入 OI/ICPC 的)随机算法(例如 [Moser 算法](https://en.wikipedia.org/wiki/Algorithmic_Lov%C3%A1sz_local_lemma))用到了随机数的熵相关的性质,因此必须使用真正的随机数。
+
+-  抽样调查时往往只需使用伪随机数。这是因为我们本就只关心统计特征。
+-  网络安全中往往要用到(比刚刚提到的伪随机数)更强的随机数。这是因为攻击者可能会利用可预测性做文章。
+-  OI/ICPC 中用到的随机算法,基本都只需要伪随机数。这是因为,这些算法往往是 通过引入随机数 来把概率引入复杂度分析,从而降低复杂度。这本质上依然只利用了随机数的统计特征。
+-  某些(未被引入 OI/ICPC 的)随机算法(例如 [Moser 算法](https://en.wikipedia.org/wiki/Algorithmic_Lov%C3%A1sz_local_lemma))用到了随机数的熵相关的性质,因此必须使用真正的随机数。
 
 ## 实现
 
     在 `Windows` 系统下 `rand()` 返回值的取值范围为 $\left[0,2^{15}\right)$ ,当需要生成的数不小于 $2^{15}$ 时建议使用 `(rand() << 15 | rand())` 来生成更大的随机数。
 
 关于 `rand()` 和 `rand()%n` 的随机性:
-- C/C++ 标准并未关于 `rand()` 所生成随机数的任何方面的质量做任何规定。
-- GCC 编译器对 `rand()` 所采用的实现方式,保证了分布的均匀性等基本性质,但具有 低位周期长度短 等明显缺陷。(例如在笔者的机器上, `rand()%2` 所生成的序列的周期长约 2000000 )
-- 即使假设 `rand()` 是均匀随机的,`rand()%n` 也不能保证均匀性,因为 `[0,n)` 中的每个数在 `[0,RAND_MAX]` 中的出现次数可能不相同。
+
+-  C/C++ 标准并未关于 `rand()` 所生成随机数的任何方面的质量做任何规定。
+-  GCC 编译器对 `rand()` 所采用的实现方式,保证了分布的均匀性等基本性质,但具有 低位周期长度短 等明显缺陷。(例如在笔者的机器上, `rand()%2` 所生成的序列的周期长约 2000000 )
+-  即使假设 `rand()` 是均匀随机的,`rand()%n` 也不能保证均匀性,因为 `[0,n)` 中的每个数在 `[0,RAND_MAX]` 中的出现次数可能不相同。
 
 ### mt19937
 
@@ -75,9 +77,10 @@ int main() {
 内部使用的随机数生成器默认为 `rand()` 。当然也可以传入自定义的随机数生成器。
 
 关于 `random_shuffle` 的随机性:
-- C++ 标准中要求 `random_shuffle` 在所有可能的排列中 **等概率** 随机选取,但 GCC[^note1] 编译器 **并未** 严格执行。
-- GCC 中 `random_shuffle` 随机性上的缺陷的原因之一,是因为它使用了 `rand()%n` 这样的写法。如先前所述,这样生成的不是均匀随机的整数。
-- 原因之二,是因为 `rand()` 的值域有限。如果所传入的区间长度超过 `RAND_MAX` ,将存在某些排列 **不可能** 被产生[^ref1]。
+
+-  C++ 标准中要求 `random_shuffle` 在所有可能的排列中 **等概率** 随机选取,但 GCC[^note1] 编译器 **并未** 严格执行。
+-  GCC 中 `random_shuffle` 随机性上的缺陷的原因之一,是因为它使用了 `rand()%n` 这样的写法。如先前所述,这样生成的不是均匀随机的整数。
+-  原因之二,是因为 `rand()` 的值域有限。如果所传入的区间长度超过 `RAND_MAX` ,将存在某些排列 **不可能** 被产生[^ref1]。
 
 !!! warning
      `random_shuffle` 已于 C++14 标准中被弃用,于 C++17 标准中被移除。