js隨機數生成器的擴展0.前言1.擴展+分區2.二進制法3. 總結

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也是可以做到。剛剛好就是最完美的,如果目標生成器是質數,就讓拒絕採樣次數儘量少,也就是儘量靠近目標。這種隨機數擴展, 套路就是超過的拒絕採樣,不足的利用加法和乘法使得剛剛好到目標範圍或者超過目標

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章