一次遍歷,等概率隨機排列數組與帶權隨機選取問題


由於背單詞軟件中需實現測試單詞與答案選項的隨機排列和帶權值的概率抽取,程序中實現了以下三個算法:

1.等概率隨機排列數組(洗牌算法)

假設有一個數組,包含n個元素。現在要重新排列這些元素,要求每個元素被放到任何一個位置的概率都相等(即1/n),並且直接在數組上重排(in place),不要生成新的數組。用 O(n) 時間、O(1) 輔助空間。

算法是非常簡單了,當然在給出算法的同時,我們也要證明概率滿足題目要求。

先想想如果可以開辟另外一塊長度為n的輔助空間時該怎么處理,顯然只要對n個元素做n次(不放回的)隨機抽取就可以了。先從n個元素中任選一個,放入新空間的第一個位置,然后再從剩下的n-1個元素中任選一個,放入第二個位置,依此類推。

按照同樣的方法,但這次不開辟新的存儲空間。第一次被選中的元素就要放入這個數組的第一個位置,但這個位置原來已經有別的(也可能就是這個)元素了,這時候只要把原來的元素跟被選中的元素互換一下就可以了。很容易就避免了輔助空間。

來計算一下概率。如果某個元素被放入第i(1in)個位置,就必須是在前 i - 1 次選取中都沒有選到它,並且第 i 次選取是恰好選中它。其概率為:

可見任何元素出現在任何位置的概率都是相等的。

實現代碼

public static T[] Shuffle(IList<T> list) { int x = 0; T[] array = new T[list.Count]; list.CopyTo(array, 0); for (x = list.Count()-1; x >= 0; x--) { int y = random.Next(x); T temp = array[y]; array[y] = array[x]; array[x] = temp; } return array; }

2.單次遍歷,等概率隨機選取一組元素

假設我們有一堆數據(可能在一個鏈表里,也可能在文件里),數量未知。要求只遍歷一次這些數據,隨機選取其中的一個元素,任何一個元素被選到的概率相等。O(n)時間,O(1)輔助空間(n是數據總數,但事先不知道)。

如果元素總數為n,那么每個元素被選到的概率應該是1/n。然而n只有在遍歷結束的時候才能知道,在遍歷的過程中,n的值還不知道,可以利用乘法規則來逐漸湊出這個概率值。在《利用等概率Rand5產生等概率Rand3》中提到過,如果要通過有限步概率的加法和乘法運算,最終得到分子為1、分母為n的概率,那必須在某一次運算中引入一個n在分母上,而分母和分子上其他的因數則通過加法、乘法、約分等規則去除。

OK,問題解決了。結束之前再做個簡單的擴展,改成等概率隨機選取m個元素(可知每個元素被選中的概率都是m/n)。

實現代碼

public static T[] SelectItems(IEnumerable<T> source, int count) { List<T> list = new List<T>(); int x = 0; foreach (T item in source) { if (x < count) list.Add(item); else { int rand = random.Next(x); if (rand < count) list[rand] = item; } x++; } return list.ToArray(); }

3.單次遍歷,帶權隨機選取

還是同樣的問題:有一組數量未知的數據,每個元素有非負權重。要求只遍歷一次,隨機選取其中的一個元素,任何一個元素被選到的概率與其權重成正比。

算法很簡單:對於任意的i(1 <= i <= n),按照如下方法給第i個元素分配一個鍵值key(其中ri是一個0到1之間等概率分布的隨機數):

之后,如果要隨機選取一個元素,就去key最大的那個;如果要選取m個元素,就取key最大的m個。

真不知道是怎么想出來的這樣的方法,不過還是先來關注一下證明的過程。

m=1證明

對於m=1的證明過程會介紹得詳細些,主要是怕我自己過幾天就忘記了。概率達人可以直接秒殺之。

m=1時,第i個元素被選取到的概率,就等於它所對應的鍵值key(i)是最大值的概率,即:

把key(i)的計算公式代入,但要注意公式中的ri並不是一個固定的數值,而是隨機變量。不考慮計算機數值表示的精度,可以假設ri是一個在0到1之間的連續均勻概率分布,因此如果要計算key(i)是最大的概率,必須要對ri所有的可能值進行概率累加,也就是積分。於是上面的概率表達式就被寫成:

再看式子中的,它表示每一個j都要滿足后面的條件,而各個j之間相互獨立,因此可以寫成概率乘積,於是得到:

對於給定的j,,另外rj也是個均勻概率分布,將概率密度函數代入可以得到:

 

因此,上面的概率算式就變成(其中w就是之前提到的所有元素的權重之和):

 

m>=1證明

當m取任意值時,概率公式變得非常復雜,在前一篇文章中使用了第i個元素不被選到的概率來簡化表達式。現在的證明也從同樣的角度進行。

第i個元素不被選到的概率,顯然等於這n個元素中,至少存在m個元素的鍵值大於key(i),與之前的討論一樣,不妨設這m個元素的下標(按鍵值從大到小)依次為j1, j2, ..., jm,滿足。注意jk和tk的取值范圍,為了簡單起見,下面的式子中就不再重復了。

為了能夠進一步求解,必須把這個連等式拆開。這里要非常小心,各個jk並不是相互獨立的,比如當j1改變的時候,j2的取值范圍也會隨之變化,依此類推。拆開之后的式子如下:

 

看起來還是相當恐怖的,一層套一層。注意等式右邊已經沒有顯式地關於i的信息了,這些信息被隱含在jk和tk的取值范圍中,切記。對每個jk,把key(jk)的式子代進去,轉換成積分;同時把tk轉換為tk。這些在m=1的證明中都提到過了。新出現的是jk,這個顯然適用概率加法,因為jk取不同的值對應於不同的互斥方案。經過這些變換得到:

 

其中的積分式在之前已經見過了,其運算過程如下(注意tk的取值范圍):

 

最終,概率計算式子變成:

與之前的理論值完全一樣。

呼,可怕的推導過程。

實現代碼

public static T[] SelectWeightedItems(IEnumerable<T> ie, int count, Func<T, double> weightfunc) { SortedList<double, T> sd = new SortedList<double, T>(); foreach (T item in ie) { double weight = weightfunc(item); if (weight <= 0) continue; double key = Math.Pow(random.NextDouble(), (1.0 / weight)); if (sd.Count < count) sd.Add(key, item); else { sd.RemoveAt(0); sd.Add(key, item); } } return sd.Values.ToArray(); } }

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM