0.前言
給你一個能生成隨機整數1-7的函式,就叫他生成器get7吧,用它來生成一個1-11的隨機整數,不能使用random,而且要等概率。
getx就是指一個能生成1到x的隨機數的函式
主角:get7(你們所有人都沒有random這個技能,全都disable了)
function get7() {
return ~~(Math.random()*7)+1 //規則:整篇文章,唯一能用random的地方
}
複製程式碼
1.擴充套件+分割槽
既然是擴充套件,那麼我給小範圍隨機數生成器擴充套件個幾倍,再擷取目標隨機數範圍不就得了。 先問一下,怎麼用get7能實現一個合格的get14?這樣子?
function get14(){
return get7() +get7()
}
複製程式碼
我們來測試一下:
var obj = Object.create(null)
for(var i = 0;i<1000000;i++){
var n = get14();
if(!obj[n]){
obj[n] = 1
}else{
obj[n] ++
}
}
console.log(obj)
複製程式碼
先不說最小值的問題,首先,他不是等概率的序列 那麼我們探討另一個問題,究竟get14能不能直接用get7加減乘除就表示出來?
喂,說get7() 乘以11/7的那個,你確定沒問題?
1.1 擴充套件
既然是小範圍隨機擴充套件到大範圍,那麼肯定離不開小範圍隨機數生成器get7的多次呼叫。當然我們最終目標很明確,目標隨機數生成器get11,它的每一個隨機數都會等概率對映到get7的擴充套件序列裡面:
然後我們很快就可以想到一個公式:
a*(getx - 1) + getx
複製程式碼
a是個整數,整個公式含義是,把getx擴充套件為a倍,並且實現等概率分佈。比如x是3,我們想擴充套件成2倍,a=2,就是2*(get3 - 1) + get3,範圍是1-7,但是,我們看看概率:
x\y 1 2 3
0 1 2 3
2 3 4 5
4 5 6 7 =》1-7的概率是1:1:2:1:2:1:1
複製程式碼
明顯這個公式還有前提
1.2 a取值範圍
我們再看a = 1:
x\y 1 2 3
0 1 2 3
1 2 3 4
2 3 4 5 =》1-5的概率是1:2:3:2:1
複製程式碼
好像矩陣每一行都是有交集
//如果a是3,ran3 - 1生成0-6 ,ran3 生成 1-3
x\y 1 2 3
0 1 2 3
3 4 5 6
6 7 8 9 =》1-9等概率
//如果a是4,ran3 - 1生成0-8 ,ran3 生成 1-3
x\y 1 2 3
0 1 2 3
4 5 6 7
8 9 10 11 =》數字都是等概率出現的,只是中間缺失了一些數,但不影響大局
複製程式碼
所以,只要保證所有的數等概率出現,先滿足對映表最大值大於等於自身的平方(3*3 = 9,即a至少要是3) 為什麼呢?因為不足本身,必然有交集,就像上面1和2兩個矩陣每一行都有交集,只要大於本身的大小,矩陣每一行就不會有交集,沒有交集,那它們就可以等概率 所以,對於7想擴充套件一個等概率序列,get14(get小於49都是沒用,不是等概率)顯然是沒用的,起碼要get49,此後所有的序列長度都是49。所以一個get14得通過get49得到,我們也可以從get49到get11了
1.3 從get49到get11
function get49(){
var n = 7*(get7()-1) + get7() //a*(getx - 1) + getx,a取7,不信自己列印看一下
return n
}
var obj = Object.create(null)
for(var i = 0;i<1000000;i++){
var n = get49();
if(!obj[n]){
obj[n] = 1
}else{
obj[n] ++
}
}
console.log(obj)
複製程式碼
既然我們看見全部元素等概率出現了,那我們只要把多餘的排除掉就行,即是遇到不是我們想要的範圍那些,重新生成一次。
function get11(){
var n = 7*(get7()-1) + get7() //a*(getx - 1) + getx,a取7,不信自己列印看一下
return n > 11?get11():n
}
複製程式碼
改改get49就行,對了,好像擴充套件陣列太多沒用的了,我們應該提高擴充套件陣列的利用率,並且減小遞迴次數
function get11(){
var n = 7*(get7()-1) + get7()
return n>44?get11():~~((n-1) / 4)+1
}
複製程式碼
2.二進位制法
對小隨機數函式進行二進位制劃分,一半表示1一半表示0,然後用二進位制表示大隨機數,再去除多餘的 get7到get11,8<11<16,我們取4位二進位制,也就是取4次get7 因為7是奇數,我們就去掉一個吧,那我們去掉1,當遇到1重新生成一次,剩下的劃分二等分
//獲取二進位制序列
function getBinary(){
var n = get7()
if(n>4){
return 1
}else if(n>1){
return 0
}else{
return getBinary()
}
}
//二進位制序列轉化回去
function get11_by_binary(){
var res = ''
for(var i = 0;i<4;i++){
res += getBinary()
}
res = parseInt(res,2)
return res>11?get11_by_binary():res
}
複製程式碼
當然,效能會差很多,因為太多遍歷了。通用版本也不難,可以自己封裝一個。
3. 總結
其實第一種方法叫做拒絕取樣。我們知道等概率生成某個範圍的隨機數,想通過這個函式生成一個更小範圍的隨機數,就應該這樣子:超過預期範圍,重新抽取,所以叫做拒絕取樣。 基本的操作:
//我們還是用get7獲取1到小於7的隨機數
function getn(n){//n是小於7的正整數
var num = get7()
return num > n?getn(n):num
}
//while形式
function getn(n){
var t
do{
t = get7()
}while(t>n)
return t
}
複製程式碼
那我們get14就可以很靈活獲得了,7*2得到14是吧,那就來:
function get14(){
var t
do{
t = get7()
}while(t>2)//我們就叫他get2吧
return get7() + 7 * (t -1) //上面的a*(getx - 1) + getx公式的變種,這個a等於7
}
複製程式碼
都get14了,那get11還會遠嗎,大於11就拒絕取樣咯。前面說先要get49?這只是一個循序漸進的過程,這樣子你可以深刻理解到這個過程要怎麼來,是不是感覺拒絕取樣很靈活?
公式推廣: 已知生成器getn能生成1-n的隨機數,那麼由getn拒絕取樣得到的新生成器geta和getb(a,b都不大於n),可以生成get(a*b):
get(a*b) = geta + a*(getb-1)//公式是對稱的,可以交換a和b
//上面的例子用公式解釋
get14() = get7() + 7 * (get2() -1) = get2() + 2*(get7() -1)
//其實get10也可以
get10() = get5() + 5 * (get2() -1) = get2() + 2*(get5() -1)
複製程式碼
get7能獲得get2,那get14就可以得到了還可以獲得get5,那get10也是可以做到。剛剛好就是最完美的,如果目標生成器是質數,就讓拒絕取樣次數儘量少,也就是儘量靠近目標。這種隨機數擴充套件, 套路就是超過的拒絕取樣,不足的利用加法和乘法使得剛剛好到目標範圍或者超過目標