雜談:經典演算法之隨機數生成
0. 引言
tkinter庫的那篇部落格(python筆記:視覺化介面寫作嘗試)真的是寫的我心力憔悴啊,其實東西並不難,就是多,然後一開始又沒有找到比較靠譜的官方文件,搞得我沒寫一個元件的應用就得去看原始碼,然後自己寫程式碼嘗試,搞得累的半死。
唉,所以這裡就休息一下,再寫上一篇小文章休息一下好了,也算是勞逸結合了。
所以,這裡,就讓我們來看一下另外一道經典的演算法題:隨機數生成問題好了。
1. 問題描述
隨機數生成這個經典演算法題我相信大部分人都知道,尤其刷過leetcode或者有過面試經歷的,無非就是給定一個隨機數生成器,然後取生成另一個範圍內的隨機數。
一個典型的例子就是使用rand7
生成rand10
。
因此,這裡,我們就以rand7
生成rand10
為例進行討論,考察一下有哪些實現思路,並對其進行一定的拓展延伸。
2. 解法一
1. 演算法思路
顯然的,如果用一個範圍更大的隨機數生成器去生成一個更小範圍的隨機數生成器是非常簡單的一件事,比如使用rand7()
來生成rand5()
,就可以使用下述方法:
def rand5():
while True:
seed = rand7()
if seed <= 5:
return seed
顯然,如此一來一個1到5的隨機數生成器就完成了,當然,效率上會略有損失,每一個隨機數的生成所需要的rand7()
的期望執行次數為1.4次,當時整體而言,這個值都不會高於2,因此,事實上大生成小的問題總是簡單的。
那麼,針對小生成大的問題,事實上也同樣可以嘗試將其拆解為大生成小的問題進行解決。
一種比較簡單的思路就是,由於
10
=
2
×
5
10 = 2 \times 5
10=2×5,因此,我們可以使用rand7()
構造兩個rand5()
生成器,然後合併成一個rand10()
2. 程式碼實現
給出python程式碼實現如下:
def rand2():
while True:
seed = rand7()
if seed != 7:
return seed % 2 + 1
def rand10():
return 5 * (rand2()-1) + rand5()
其中,rand5()
我們已經在上述內容中進行了介紹,這裡我們就不再多做說明了。
3. 演算法分析
可以看到,整體而言,每一次隨機數生成所需要呼叫的rand7()
的期望次數為
7
/
6
+
7
/
5
≃
2.57
7/6+7/5\simeq 2.57
7/6+7/5≃2.57。
但是上述演算法的限制也十分的明顯,需要目標範圍可以進行因式分解為兩個小數的乘積,否則就無法原模原樣地照抄上述的演算法,比如rand11()
,就無法採用分解的方式進行求解。
但是,這個問題也不是無解,上述相同的思路只要稍作調整,我們還是可以進行求解的。
3. 解法二
1. 演算法思路
可以看到,在上述演算法中,最為核心的地方在於將問題從一個小生成大的問題轉換為一個大生成小的問題。
而具體的實現方式上,上述思路採用的是大拆小的模型,將目標範圍通過因式分解的方式拆分為若干個概率相同且可以被當前隨機數生成覆蓋的子範圍,從而進行求解。
但是上述方法受限於拆分過程必須是拆分為等概率的幾個子範圍,即是說必須是因式分解可分的,但是如果目標範圍是一個質數或者因子中存在一個數大於當前的隨機數生成器,上述思路就會失效。
不過,我們可以將上述拆分的思路反著來,不是縮減目標範圍,而是將當前隨機數生成器進行等比例放大,使之可以覆蓋住目標範圍。
而放大的方式就是就是通過k進位制的方式,不斷地將其擴大k倍,那樣的話就可以等概率地覆蓋新的範圍內的所有值。
2. 程式碼實現
給出python程式碼實現如下:
def rand10():
while True:
seed = (rand7()-1) * 7 + rand7()
if seed <= 40:
return seed % 10 + 1
3. 演算法分析
同樣的,我們同樣分析可得,上述演算法的期望值 2 × ( 49 / 40 ) = 2.45 2\times(49/40)=2.45 2×(49/40)=2.45。
可以看到,通過這種方式,我們就可以將問題不受限制的擴充套件到任意情況當中。
4. 總結
綜上,我們給出了一道經典演算法題——隨機數生成問題的解答,並對其進行了一定的拓展,將其拓展到了任意兩個隨機數相互轉換的問題,具體而言,可以拆解為大生成小以及小生成大的問題。
其中,針對大生成小的我們沒有詳細的討論,因為事實上這還是比較明顯的。
而對小生成大的問題,其核心的處理思想事實上也都是將其轉換為大生成小的問題,我們具體給出了兩種常見的實現方法,分別是分解目標範圍以及擴充套件已有生成範圍的方式。