[珠璣之櫝]隨機數函式取樣與概率
本節主要受到《程式設計珠璣》第12章隨機取樣問題的啟發,但不僅僅限於隨機取樣問題,進一步地,研究討論了一些在筆試面試中常見的和隨機函式以及概率相關的問題。
閱讀本文所需的知識:
1.對C語言中或其他語言中等價的rand()、srand()有所瞭解。本文不討論種子的設定和偽隨機數的問題;
2.中學或以上水平的概率基本概念。
目錄
利用隨機數函式生成隨機數
問題1(《程式設計珠璣》習題12.1後半段):
給定一個rand(),可以產生從0到RAND_MAX的隨機數,其中RAND_MAX很大(常見值:16位int能表示的最大整數32767),寫出利用rand()生成[a,b]中任意整數的函式,其中a>=0, b<=RAND_MAX,且b-a<<RAND_MAX.
分析:
這是在程式設計工作最常見的隨機函式的應用,在這裡做一個起點再合適不過。把隨機數區間的起點從0變為a,同時把一共RAND_MAX+1個數的區間縮小至只含有b-a+1個數的區間,寫為 a + rand()%(b-a+1),此時顯然最大值是a+(b-a)= b。
進一步地,這個b-a<<RAND_MAX的條件雖然看上去不起眼,其實很重要。
附加思考:
如果b-a和RAND_MAX很接近會發生什麼情況?讀者不妨先做思考,問題2的分析會做出解答。
這個rand(),其實相當於《程式設計珠璣》提到的bigrand()。
問題2(筆試題變形):
給定一個隨機數函式rand7(),它能以等概率生成1~7這7個整數。請根據rand7()寫出類似的rand5()。
分析:
如果直接像問題1中一樣,把1+rand7()%5作為rand5()會有什麼情況發生?這時確實能產生1~5的隨機數沒錯,可是各個數的概率相等嗎?
對於隨機數2,既有可能來自於1+1%5,也有可能來自於1+6%5,顯然其概率是2/7,而不是1/5,不滿足rand5()等概率產生各隨機數這一隱含要求。不同於問題1,問題1中一個很大的區間收縮成較小區間時,各個元素對映後的新元素概率雖然概率可能不完全一樣,但卻是近似相同的。
為了滿足等概率的要求,可以這麼做:
int rand5() { int res; do { res = rand7(); }等概率的rand5()while(t>=6); return res; }
雖然保證了1~5的概率都變成了1/5,但是有一個無法避免的缺點是,每當產生了6或者7都要拋棄,相當於這一次執行是“空轉”,浪費了時間。
如果對1/5這個概率不明白,可以有兩種理解:每次產生6或7就被拋棄,剩下數的概率相等,必然為1/5;或者用更嚴密的推理:產生1~5的隨機數,最終得到某一個的概率為:1/7+(1/7)*(2/7)+(1/7)*(2/7)2+...,無限項求和,結果是1/5。
問題3(筆試題原題)
給定一個隨機數函式rand7(),它能以等概率生成1~7這7個整數。請根據rand7()寫出類似的rand10()。
分析:
有了問題2的概率基礎,把rand7()變成rand10()僅僅需要一點點思考了。
int rand10() { int t1,t2,res; do { t1 = rand7(); } while(t1>=6); //t1以等概率成為1/5 do { t2 = rand7(); } while(t2==7); //t2以1/2概率成為奇數或偶數 res = t1+5*(t2%2); //res是1~10中的任意一個數的概率都是1/10 //注意到%和*具有相同的優先順序,這裡去掉括號結果是錯的 return res; } 等概率的rand10()等概率的rand10()
問題2和問題3是對《程式設計珠璣》上使用範圍不大的randint(i,j)生成其他範圍隨機數方法的解答。在掌握了問題2和問題3的解法後,你已經學會隨機數區間的收縮和擴張,類似的問題迎刃而解。
問題4(《程式設計珠璣》習題12.1前半段)
C庫函式rand()常返回15個隨機位,寫出bigrand(),能夠返回30個隨機位。
分析:
其實問題4和問題3有點像,但是不同之處在於,這次我們的視角是從位出發的,把rand()看做了將15個位每一個位以1/2概率設為0或1,從而生成0~RAND_MAX。從某種意義上來說,按這種理解方式來解這個問題更輕鬆一些,但是僅侷限於2的冪減1這樣的數值的區間,比如從0到11...11。在此基礎之上把兩部分的位拼接起來即可。
//《程式設計珠璣》的答案 //int bigrand() //{ return RAND_MAX *rand() +rand();} //最大值不是30個1,懷疑有錯 //我的答案,把先生成的部分左移15位作為高15位 long bigrand() { return ((long)RAND_MAX+1)*rand() +rand();}位拼接的bigrand()
擴充套件:拒絕取樣
這是一個簡單粗暴有效的方法,使用這個方法你可以不用考慮複雜的區間擴充套件和收縮的問題。
以rand7()產生rand10()為例,構造一個二維陣列並把它填成:
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 |
然後使用兩次rand7()分別生成行號和列號,如果對應元素是0,拋棄重來;否則即是結果。
其實這種方法依然用到了區間擴充套件:把7擴充套件成7*7,並把不符合的部分拋棄。從這裡可以看出這種方法其實還是有缺陷的:如果用rand7()生成rand50(),那這50個數可是填滿這個表格後還有填不下的。
利用隨機數函式產生隨機事件
用隨機事件表示隨機事件?經過上面的區間收縮的思考,看上去並不難。把這個問題具體化為:使用rand()表示以M/N的概率發生的隨機事件,M<=N,並可用作:if(事件A發生) ,其中P(A) = M/N,那麼表示為:
if(rand()%N< M) ...隨機事件
通過這種方式,我們就可以做出讓程式“以M/N的概率執行某個命令”這樣的設計了。
取樣問題:從n個元素中隨機選取m個
從概率角度出發
考慮整數0,1,2,...,n-1,可以用上節的方法以概率m/n選取0(推導方式略)。但是對於1,必須考慮之前0是否被選取而以(m-1)/(n-1)或m/(n-1)的概率選取1,後續就更加麻煩。好在迭代是計算機的長項,只需要把這個是否選擇當前數的隨機事件稍作修改即可,使之變成從r個其餘的數中選擇s個:
int gen(int m,int n) { int i, select = m,remaining = n; for(i=0;i<n;i++) { if(rand() % remaining <select) { printf("%d\n",i); select--; } remaining--; } return 0; }
其概率證明可見於Knuth的The Art of Computer Programming第2卷。進一步地可以優化為:
int genknuth(int m,int n) { int i; for(i=0;i<n;i++) if(rand()%(n -i) < m) { printf("%d\n",i); m--; } return 0; }
《程式設計珠璣》提示,這種演算法是“所有n的所有m元子集被選中的概率相等”,這個條件強於“所有元素被選中的概率相同”。下面是習題12.2中提到的“以等概率選擇搜有元素,但是有些m元子集被選中的概率比其他子集大”的演算法:直接選擇1個數,則這個m元集合為它本身即後續的一共m個數,可能包括迴繞。
對於這種方法,總要產生n次隨機數。進一步可以寫為for(i=0;i<n&&m>0;i++),但程式的平均執行時間是否變得更快需要權衡。對應地,習題12.7提供了一種稍微減少隨機數產生次數的遞迴函式:
int randselect(int m,int n) { int r; //assert(m<=n && m>=0); if(m>0) { r = rand()%n; if(r < m) { printf("%d\n",n-1); randselect(m-1,n-1); } else randselect(m,n-1); } return 0; }
從集合插入出發
由於集合元素不重複,如果按等概率選擇一個隨機數,不在集合中就把它插入,反之直接拋棄,直到集合元素個數達到m個,同樣可以滿足要求,並且用C++的STL很容易實現:
void gensets(int m,int n) { set<int> S; while(S.size() < m) S.insert(rand()%n); set<int>::iterator i; for(i = S.begin();i!=S.end();++i) cout<<*i<<"\n"; }
這個演算法的主要問題是,如果拋棄已存在的元素的次數過多,相當於多次產生隨機數並進行集合操作,效能將明顯下降。比如當n=100而m=99,取第99個元素時,演算法“閉著眼睛亂猜整數,直到偶然碰上正確的那個為止”(《程式設計珠璣(續)》,13.1節)。雖然這種情況會在“從一般到特殊”提供解決方案,但下面的Floyd演算法明顯規避了產生隨機數超過m次的問題。
習題12.9提供了一種基於STL集合的隨機數取樣方法,可以在最壞情況下也只產生m個隨機數:限定當前從中取值的區間的大小,每當產生重複的隨機數,就把這一次迭代時不會產生的第一個隨機數拿來替換。
int genfloyd(int m,int n){ set<int> S; set<int>::iterator i; for(int j = n-m; j<n;j++) { int t = rand()%(j+1); if(S.find(t) == S.end()) S.insert(t); else S.insert(j); } for(i=S.begin();i!=S.end();++i) cout<<*i<<"\n"; }Floyd的演算法
如果不必基於集合而實現,我自己寫了一個原理類似的演算法,思想是把“用過”的元素“扔”到下一次迭代的隨機數取樣區間之外:
int gen(int m,int n) { int *array; int i,j; array = (int *)malloc(sizeof(int) * n); for(i=0;i<n;i++) array[i] = i; while(m>=1) { j = rand()%n; printf("%d\n",array[j]); if(j<n-1) array[j] = array[n-1]; m--; n--; } return 0; }我的演算法
從“打亂順序”出發
這是個來源於實際的想法:將所有n個元素打亂,取出前m個。更快的做法是,打亂前m個即可。對應的C++程式碼如下:
int genshuf(int m,int n) { int i,j; int *x = new int[n]; for(i = 0;i<n;i++) x[i] = i; for(i = 0;i<m;i++) { j = randint(i,n-1); //randint產生i到n-1之間的隨機數 int t = x[i];x[i] = x[j];x[j] = t; } //sort(x,x+m); //sort是為了按序輸出 for(i=0;i<m;i++) cout<<x[i]<<"\n"; }
把sort註釋掉的這段程式碼,可以作為隨機不重複序列產生器。類似的還有Floyd的演算法P。(《程式設計珠璣(續)》,13.3節)
從一般到特殊
以上討論的幾種方式都不限定m和n的取值,只需m<=n即可。對於特殊的取值,有特殊的解決方案,以下是程式設計珠璣上的兩例:
1.n = 106而m=n-10,這時可以先生成10個元素,然後輸出其餘的元素。進行這種處理的額外程式碼可以提高演算法的平均速度。
2.n=231而m = 107,m<<n,這時可以先生成1.1*107個元素,排序後去掉重複的(由於n很大,m中出現重複元素的概率很低),得到 107個元素的樣本。
附:
(《程式設計珠璣(續)》習題13.5)Doug McIlory的求N個元素中取M種的第G種情況的組合的演算法,原書這個演算法我沒有理解,也沒有看到比較滿意的解釋,可能用到了某些我不熟悉的組合數性質。原書中沒有對其進行進一步解釋,並且似乎只是作者的題外話而已。如果看著有困難,這部分程式碼完全可以跳過。介紹這個演算法的原因是用它能在獲得隨機數G後,直接獲得第G種N個元素取M個的取法,相當於只產生了一次隨機數。
int comb(int n,int m,int g,int* array){ int d = 1,t; while(m>0) { t = combination(n-d,m-1); //combination(n,m)是計算n中取m個的組合數 if(g-t<0) { m--; array[m] = d; printf("%d\n",d); } else g-=t; d++; } return 0; }Doug McIlroy的演算法
取樣問題:從未知總數的元素中選擇一個
從事先未知總數為n的物件中隨機選擇一個的方法。有兩種常見的具體問題:
1.讀取一個未知行數的檔案,隨機輸出其中的的一行,同時最多隻能緩衝一行的內容(《程式設計珠璣(續)》習題15.3利用了這種形式);
2.對連結串列進行一趟遍歷,隨機輸出其中的一個結點的元素,只能使用一個臨時指標。
解法是,以概率1選擇第一個元素存入快取,以概率1/2用第二個元素替換掉快取,...,直至遍歷完所有元素,最後輸出快取的內容。可以分析出此時所有元素留在快取的概率均為1/n,比如1,是1*(1/2)*(2/3)*...*((n-1)/n)。
int random_select(void) { int i,num=1; for(i=1;i<n;i++) //i<n代表某種終止條件,n未知 if(rand()%i ==0) num = i; return num; }
對於這個問題的常見應用:馬爾科夫文字生成器裡選擇雜湊表中某項對應連結串列的任意一個結點。
概率問題選編
1.(習題12.4)對於集合插入問題,每次產生隨機數後都需要在集合中檢測該元素是否已經存在,這個測試次數和呼叫隨機數函式的次數相同。對於從n個元素中選擇m個元素的問題,平均需要測試多少次才能保證選擇了m個元素?
這裡先處理特殊的問題,即“贈券收集問題”:必須收集多少張棒球卡才能保證擁有所有的n種卡?
記Pi為從收集了i-1種到i種的概率,Pi=(n-i+1)/n
此時需要收集的卡片數目ni = 1* Pi + 2*(1-Pi)*Pi + 3*(1-Pi)2*Pi + ... + n*(1-Pi)n*Pi + ...,這個無窮級數可以求解為ni=1/Pi
那麼所需要總的卡片數目sum(ni) = n1+n2 +... +nn約為nlnn + γn + O(1)。相關維基百科
對於從n個元素中選擇m個,相應地sum(ni) = n1+n2 +... +nm,約為nlnm+γn。(根據調和數的推導)
2.(習題12.11)每個玩家有一張包含16個覆蓋點的紙牌,覆蓋點下面隱藏著1~16的隨機排列。玩家每次刮開一個點,如果出現3,則判玩家負;如果出現1或2,則判玩家勝。那麼,隨機選擇覆蓋點刮開,獲勝的概率是多少?
解答:
其實勝負只對應兩種排列:1和2都出現在3前則勝,否則負。前者的即*1*2*3*或*2*1*3*,後者為其餘四種排列。顯然獲勝的概率為2/3。
不要把問題複雜化,應該儘量簡化。4~16的數字都是沒有用的。
相關推薦
[珠璣之櫝]隨機數函式取樣與概率
本節主要受到《程式設計珠璣》第12章隨機取樣問題的啟發,但不僅僅限於隨機取樣問題,進一步地,研究討論了一些在筆試面試中常見的和隨機函式以及概率相關的問題。 閱讀本文所需的知識: 1.對C語言中或其他語言中等價的rand()、srand()有所瞭解。本文不討論種子的設定和偽隨機數的問題;
[珠璣之櫝]估算的應用與Little定律
估算的資料主要依賴於所能獲得的資料和常識,有時還包括實踐而不僅僅是理論。它常常作為一個大問題中的子問題,恰當地估算可以省去精確計算的時間和開銷。在計算機領域,所謂常識的內容很寬泛,比如硬碟的傳輸速度、CPU每秒能執行多少指令、各種資料結構的大小甚至每分鐘錄入的單詞數。有些資料是能夠從各種資料中查得的,但僅
“珠璣之櫝”系列簡介與索引
系列博文主要目的: 收集《程式設計珠璣》和《程式設計珠璣(續)》(以下簡稱《續》)上的演算法和思想,幷包括了一些自己的思考和對相關問題的引申,以備複習和查用。 內容提要: 主要是演算法收集,結合了《程式設計實踐》 (Practise of Programming)、《程式設計精粹:編寫高質量C語
[珠璣之櫝]二分思想與分治法、排序思想
#include <stdio.h> #include <assert.h> int BitCheck(int total,int n,int last) { FILE *input,*output0,*output1; char filename[10
[珠璣之櫝]字串和序列:左移、雜湊、最長重複子序列的字尾陣列解法、最大連續子序列
字串和陣列在儲存上是類似的,把它們歸為同一主題之下。本文主要介紹三大類問題和它們衍生的問題,以及相應演算法。 本文主要介紹和討論的問題和介紹的演算法(點選跳轉): 字串迴圈移位(左旋轉)問題 問題敘述: 將一個n元一維向量向左旋轉i個位置。例如,當n=8且i=3時,"abcde
[珠璣之櫝]位向量/點陣圖的定義和應用
位向量/點陣圖是一個很有用的資料結構,在充分利用小空間儲存大量資料方面非常具有優勢,Linux核心中很多地方都是用了點陣圖。同時,它不但基礎,而且用到了很多程式語言的知識,以及對細節的把握,常常作為面試題出現。這裡將要介紹它的實現、操作、應用。 與點陣圖(bitmap)比,我更傾向於用位向量(bit
[珠璣之櫝]淺談程式碼正確性:迴圈不變式、斷言、debug
這個主題和程式碼的實際寫作有關,而且內容和用法相互交織,以下只是對於其內容的一個劃分。《程式設計珠璣》上只用了兩個章節20頁左右的篇幅介紹,如果希望能獲得更多的例項和技巧,我比較推崇《程式設計實踐》 (Practise of Programming)、《程式設計精粹:編寫高質量C語言程式碼》(Writin
R語言︱分佈函式與概率密度+隨機數產生
1、常見概率分佈##正態分佈 pnorm(1.96) #P(x<=1.96)時的分佈概率 pnorm(1.96,0,1) #上同 pnorm(1
Python之路-Day07區域性變數與全域性變數,遞迴函式
區域性變數和全域性變數的含義 在子程式中定義的變數稱為區域性變數,在程式的一開始定義的變數稱為全域性變數. 全域性變數作用域是整個程式,區域性變數作用域是定義該變數的子程式. 當全域性變數於區域性變數同名時: 在定義區域性變數的子程式內,區域性變數起作用,在其它地方全域性變數起作用.
機器學習之---似然與概率
“概率”描述了給定模型引數後,描述結果的合理性,而不涉及任何觀察到的資料。 拋一枚均勻的硬幣,拋20次,問15次拋得正面的可能性有多大? 這裡的可能性就是”概率”,均勻的硬幣就是給定引數θ=0.5 ,“拋20次15次正面”是觀測值O。求概率P(H=15|θ=0.5)=
Python之路Python全域性變數與區域性變數、函式多層巢狀、函式遞迴 Python之路Python全域性變數與區域性變數、函式多層巢狀、函式遞迴
Python之路Python全域性變數與區域性變數、函式多層巢狀、函式遞迴 一、區域性變數與全域性變數 1、在子程式中定義的變數稱為區域性變數,在程式的一開始定義的變數稱為全域性變數。全域性變數作用域是整個程式,區域性變數作用域是定義該變數的子程式。 全域性變數
c++類與物件之預設成員函式
c++類與物件(二) 1.類的6個預設成員函式 一:建構函式 建構函式是一個特殊的成員函式,名字與類名相同,建立類型別物件時由編譯器自動呼叫,保證每個資料成員都有一個合適的初始值,並且在物件的生命週期內只調用一次。 建構函式是特殊的成員函式,其特徵如下:
進階之函式節流與函式防抖
原文標題:函式節流與函式防抖 原文地址:https://justclear.github.io/throttle-and-debounce/ 原文作者:justclear 什麼是函式節流與函式防抖 舉個栗子,我們知道目前的一種說法是當 1 秒內連續播放 24 張以上
tensorflow之損失函式:sparse_softmax_cross_entropy_with_logits 與 softmax_cross_entropy_with_logits的區別
原函式: tf.nn.sparse_softmax_cross_entropy_with_logits( _sentinel=None, labels=None, logit
ORACLE PL/SQL程式設計之六: 把過程與函式說透(窮追猛打,把根兒都拔起!)
本篇主要內容如下: 6.1 引言 6.2 建立函式 6.3 儲存過程 6.3.1 建立過程 6.3.2 呼叫儲存過程 6.3.3 AUTHID 6.3.4 PRAGMA AUTONOMOUS_TRANSACTION 6.3.5 開發儲存過程步驟 6.3.6
Kotlin學習之旅(D4)-函式與Lambda表示式
Kotlin學習之旅-第四天 今天的主題是:函式與Lambda表示式 前言 函式 Kotlin裡面的函式其實在之前的學習中已經見過了,通過 fun 關鍵字來標識 fun double(x: Int): Int { return 2 * x } 預
R語言學習筆記之set.seed()函式與table函式
set.seed(123)函式,此函式作用是為了,但你需要使用隨機數時,可保證你在執行或者除錯後,計算機所創造的隨機數保持不變。換句話說,如果使用隨機函式rnorm(10)之類的函式,每次執行後,結果都是不一樣的,如果再次之前使用set.seed()函式,則會保證測試資料保持
Linux高階程式設計基礎——程序間通訊之用sigqueue函式和sigaction函式實現訊號的安裝與傳送
程序間通訊之用sigqueue函式和sigaction函式實現訊號的安裝與傳送 程序A向程序B傳送SIGUSR1訊號; 程序B收到訊號後,列印字串“receive SIGUSR1”; 要求用sigqueue函式和sigaction函式實現以上功能; /這個實
菜鳥的C#學習之旅——C#方法過載與函式過載
目錄 過載 方法過載 函式過載 總結 過載 過載,簡單說,就是函式或者方法有相同的名稱,但是引數列表不相同的情形,這樣的同名不同引數的函式或者方法之間,互相稱之為過載函式或者方法。 過載的
【雜題】[LibreOJ 2541] 【PKUWC2018】獵人殺【生成函式】【概率與期望】
Description 獵人殺是一款風靡一時的遊戲“狼人殺”的民間版本,他的規則是這樣的: 一開始有 n個獵人,第 i 個獵人有仇恨度 wi。每個獵人只有一個固定的技能:死亡後必須開一槍,且被射中的人也會死亡。 然而向誰開槍也是有講究的,假設當前還活著的獵人有