從有限元素中隨機取未使用的元素 [tr-D6VS]

元素集合可以轉換為正整數集合,因此問題可以寫成:給定範圍 [0,N)[0, N),已使用之數字集合為 UU,最簡單的演算法就是排除 UU

[0,N)∖U[0, N) \setminus U

從中隨機取一個元素

問題 [local-0]

然而排除已使用元素是固定成本,在已使用元素會變多的情況下更是越來越大的成本。隨機抽一個,再查看有沒有撞上已經使用的位置反而更有效。但這樣做的界線到底在哪裡?

幾何分布 [local-1]

對於空間大小 NN 與已使用的 UU,隨機抽一個位址命中未使用元素的機率是

p=N−∣U∣Np = \frac{N - |U|}{N}

假設每次抽樣互相獨立,那「抽到第 kk 次才第一次成功」服從幾何分布

(1−p)k−1p(1-p)^{k-1} p

連續 kk 次都失敗(選到已使用的位置)的機率是 (1−p)k(1 - p)^k,期望次數是

1/p=NN−∣U∣1/p = \frac{N}{N - |U|}

所以我們可以看到 ∣U∣|U| 只佔 NN 一小部分時,期望次數幾乎是常數,∣U∣=N/2|U| = N/2 時也只是2次,跟排除法的 O(N)O(N) 差距明顯。要等到 N−∣U∣N - |U| 幾乎為 00 時,這才開始稱得上成本

計算重試上限 [local-2]

(1−p)k(1 - p)^k 隨 kk 遞減趨近 00,但對任何有限的 kk 都不等於 00,也就是不存在有限次數可以保證我們抽到未使用的數字。所以這裡要找的不是「幾次一定會中」,而是「願意接受多大的失敗機率」

給定可以接受的失敗機率 ε\varepsilontr-notes 取 10−910^{-9},我們想找出滿足 (1−p)k≤ε(1 - p)^k \le \varepsilon 的最小 kk

先對兩邊取 ln⁡\ln,ln⁡\ln 單調遞增所以不等號方向不變

ln⁡((1−p)k)≤ln⁡ε\ln((1 - p)^k) \le \ln \varepsilon

根據Logarithm power rule可以得出

kln⁡(1−p)≤ln⁡εk \ln(1 - p) \le \ln \varepsilon

p∈(0,1)p \in (0, 1) 時 ln⁡(1−p)<0\ln(1 - p) < 0,所以同除要翻轉不等號

k≥ln⁡εln⁡(1−p)=ln⁡(1/ε)ln⁡(1/(1−p))k \ge \frac{\ln \varepsilon}{\ln(1 - p)} = \frac{\ln(1/\varepsilon)}{\ln(1/(1 - p))}

第二個等號來自分子分母同乘 −1-1,讓兩邊都寫成正數。取ceiling就是可拿來做嘗試次數的最小 kk

但計算 ln⁡(1/(1−p))\ln(1/(1 - p)) 太麻煩了,所以實作上用 m/pm/p 當上限這裡 m=ln⁡(1/ε)m = \ln(1/\varepsilon),而 1/p1/p 就是上面的期望次數。這個做法是因為如果把分母展開成無限級數注意收斂條件

ln⁡(11−p)=∑n=1∞pnn=p+p22+p33+⋯\ln(\frac{1}{1 - p}) = \sum_{n = 1}^\infty \frac{p^n}{n} = p + \frac{p^2}{2} + \frac{p^3}{3} + \cdots

p∈(0,1)p \in (0, 1) 時每一項都是正的,因此可以看出 ln⁡(1/(1−p))≥p\ln(1/(1 - p)) \ge p被砍掉的高次項正好說明了高估多少

分母變大則整體變小,所以 m/ln⁡(1/(1−p))≤m/pm / \ln(1/(1 - p)) \le m/p,也就是 m/pm/p 大於等於 kk 總是成立。因此這是安全的高估,代價是最壞情況多重試幾次,但讓我們不必計算 ln⁡(1−p)\ln(1 - p)在 p→1p \to 1 時數值上也不穩,這是另一個不採用的理由

最後這個上限還要跟 NN 取 min。期望次數 1/p=N/(N−∣U∣)1/p = N/(N - |U|) 本身其實永遠不會超過 NN(最壞的 N−∣U∣=1N - |U| = 1 時剛好等於),但 m/pm/p 可能會超過:N−∣U∣<m≈20.7N - |U| < m \approx 20.7 時就有 m/p>Nm/p > N。既然這時候成本已經比掃過整個空間還高,應該放棄然後退回過濾再抽取的演算法

結論 [local-3]

我們分析了抽取的上限是怎麼推導出來的,並且計算超過上限就退回一開始的排除法。所以最壞情況是 O(N)O(N) 而不是不會停;另外空間真的滿了會直接報錯,這也是為什麼上面的分析不涉及 p=0p = 0

這個演算法用在 raco tr next 的 --random 功能上