題目一
竹筒有20根簽,10根白色,10根紅色。抽取10根顏色一致可獲得100元獎勵,抽取9根顏色一致可獲得50元獎勵,但是抽取紅色5根白色5根就損失50元,問這游戲是否值得參與?原因?
解:
這是典型的組合數求期望問題。設事件‘抽取10根顏色一致’為A,事件‘抽取9根顏色一致’為B,事件‘抽取紅色5根白色5根‘為C。
根據組合數公式
C(m.n) = m!/(n!*(m-n)!)
求得事件A的概率P(A) = C(10,10)*2/C(20,10) = 0.00001082508822446903 ;
事件B的概率P(B) = C(10,9)*C(10,1)*2/C(20,10) = 0.001082508822446903;
事件C的概率P(C) = C(10,5)*C(10,5)/C(20,10) = 0.34371820130334063;
期望 = P(A)*100 + P(B)*50+P(C)*(-50) = -17.130702115222242;
結論,期望低於0。游戲無數次后,最終會虧損17.13元,該游戲不值得玩。
下面我們使用程序實現該游戲,並且提供求期望的函數。
第一個思考,如何高效求出概率,
第二個思考,如何有效地模擬用戶抽簽的情況,
第三個思考,如何統計抽簽結果呢,
。。。。。
最后想想,萬一日后用戶需要修改游戲規則呢
完成整個模擬,需要實現四個模塊:
第一部分,制定游戲規則和獎勵規則
第二部分,計算期待
第三部分,獲取一次游戲結果
第四部分,根據獎勵規則,統計結果
打代碼總是很愉快地,啦啦啦~~~

1 function Probability(){ 2 3 //--------------------1,制定規則 4 5 //抽獎箱中的包含的簽種類和數量 6 this.typeColor = { 7 'red': 10, 8 'white':10 9 }; 10 //抽取數量 11 this.extractNum = 10; 12 //可能性,和收益對應表 13 this.possibility = { 14 '10' : 100, 15 '9' : 50, 16 '5' : -50 17 }, 18 19 20 //--------------------2,計算期待 21 this.getExpectation = function(){ 22 23 //暫時省略這一部分代碼,下面我們一同探討這一部分代碼如何優化 24 return (-17.130702115222242).toFixed(2); 25 }; 26 //-------------------3,獲取一次游戲結果 27 this.playGame = function(){ 28 var _a = [] ;//空抽獎箱,存儲所有的球 29 var _r = []; //‘抽獎結果’ 30 for( a in this.typeColor){ 31 for(var i=0;i<this.typeColor[a];i++){ 32 _a.push(a); 33 } 34 }//將簽放入抽獎箱 35 36 37 //遍歷,開始抽獎, 38 for(var i=0;i<10;i++){ 39 //抽取特定一個球 40 var _i = Math.round(Math.random()*(_a.length-1)); 41 //將抽出的球,放入‘抽獎結果’中 42 _r.push(_a[_i]); 43 //移除抽獎箱中剛被抽取的球 44 _a.splice(_i,1); 45 } 46 return _r; 47 }; 48 49 //-------------------4,統計結果 50 this.getResult = function(arr){ 51 //統計各種球的結果 52 var _r = {}; 53 for( k in arr){ 54 _r[arr[k]]? _r[arr[k]]++:_r[arr[k]] = 1; 55 } 56 57 //統計收益 58 for(p in _r){ 59 for(k in this.possibility){ 60 if(_r[p] == k){ 61 _r.earnings = this.possibility[k];//價格 62 } 63 } 64 } 65 return _r; 66 } 67 }
下面我們一起思考求期望這一部分如何逐步優化:
嘗試一,只完成基本的題目要求
1 function probability1(){ 2 //通用C函數 3 //C(m.n) = m!/(n!*(m-n)!) 4 function C(a,b){ 5 var _n=1,_d=1;//分子,分母 6 for(var i=a;i>0;i--) 7 _n *=i; 8 for(var i=b;i>0;i--) 9 _d *=i; 10 for(var i=1,j=(a-b);j>0;i++,j--) 11 _d *= i; 12 return _n/_d; 13 } 14 return C(10,10)*2/C(20,10)*100+//抽取10根相同顏色的概率*100 15 C(10,9)*C(10,1)*2/C(20,10)*50+//抽取9根相同顏色的概率*50 16 C(10,5)*C(10,5)/C(20,10)*(-50);//抽取5根相同顏色的概率*(-50) 17 18 }
仔細觀察,好多組合數是被重復計算的,例如,C(10,10)和C(20,10)等等,可否用一個變量存儲已經計算過的組合數,下次求相同的組合數的時候可以從靜態變量中獲取而不需要重復計算。
嘗試二,在嘗試一的基礎上避免計算重復的C
1 function probability2(){ 2 //通用C函數 3 function C(a,b){ 4 var _n=1,_d=1; 5 if(!resOfC[a+','+b]){ 6 var _n=1,_d=1;//分子,分母 7 for(var i=a;i>0;i--) 8 _n *=i; 9 for(var i=b;i>0;i--) 10 _d *=i; 11 for(var i=1,j=(a-b);j>0;i++,j--) 12 _d *= i; 13 return _n/_d; 14 15 }else{ 16 return resOfC[a+','+b]; 17 } 18 } 19 20 var resOfC = {}; //一個新的臨時數組,存儲可能出現的結果 21 //如果之前計算過C(10,10),可以直接從 22 console.info(C(10,10)*2/C(20,10)); 23 console.info(C(10,9)*C(10,1)*2/C(20,10)); 24 console.info(C(10,5)*C(10,5)/C(20,10)); 25 return C(10,10)*2/C(20,10)*100+ 26 C(10,9)*C(10,1)*2/C(20,10)*50+ 27 C(10,5)*C(10,5)/C(20,10)*(-50); 28 29 }
然而,判斷組合數是否重復計算並不是重點。有數學公式C(m,n) = C(m,m-n),我們計算C(10,9)可以轉換為計算C(10,1),以提高計算效率。
嘗試三,在嘗試一的基礎上判斷a/2和b的大小,例如對於C(10,9),直接計算C(10,1)
1 function probability3(){ 2 //通用C函數 3 function C(a,b){ 4 var _n=1,_d=1;//分子,分母 5 6 if(b>a/2) 7 b = a-b; 8 9 for(var i=a;i>0;i--) 10 _n *=i; 11 for(var i=b;i>0;i--) 12 _d *=i; 13 for(var i=1,j=(a-b);j>0;i++,j--) 14 _d *= i; 15 return _n/_d; 16 } 17 18 var resOfC = {}; //一個新的臨時數組,存儲可能出現的結果 19 //如果之前計算過C(10,10),可以直接從 20 return C(10,10)*2/C(20,10)*100+ 21 C(10,9)*C(10,1)*2/C(20,10)*50+ 22 C(10,5)*C(10,5)/C(20,10)*(-50); 23 24 }
學過組合數的同學都知道,有數學公式C(n,k)=C(n-1,k)+C(n-1,k-1) ,
我們先從C(0,0)算起,然后C(1,0),C(1,1),C(2,0),C(2,1),C(2,2)......C(n,k),仔細觀察,其實求組合數的問題由交疊的子問題構成。
第一時間想到的是使用遞歸關系包含子問題和大問題具有的相同形式,但由於子問題具有交疊,用遞歸方法解決代價很大,我們可以考慮使用動態規划來求解。
對每個交疊的子問題只求解一次,並把結果存儲在記錄表中,最后得出原始問題的解。
嘗試四,利用遞歸和動態規划來求解組合數
存在C(n,k)=C(n-1,k)+C(n-1,k-1) ,建立一張表,我們對數據進行動態更新,即每一次迭代,我們都去根據公式計算出合適的值。
1 function probability3(){ 2 //C(a,b)=C(a-1,b-1)+C(a-1,b) 3 //通用C函數 4 function C(a,b){ 5 if(b==0){ 6 return 1; 7 } 8 var temp = [];// 9 for (var i = 0; i < a; i++) { 10 temp[i] = []; 11 temp[i][0] = 1;//每行首尾為1 12 temp[i][i+1] = 1;//每行末尾為1 13 14 for (var j=1;j<=i;j++) { 15 temp[i][j] =temp[i-1][j-1]+temp[i-1][j];////計算第i行第j列的值 16 } 17 } 18 return temp[a-1][b]; 19 } 20 return C(10,10)*2/C(20,10)*100+ 21 C(10,9)*C(10,1)*2/C(20,10)*50+ 22 C(10,5)*C(10,5)/C(20,10)*(-50); 23 }
源碼及游戲演示測試地址:http://lovermap.sinaapp.com/probability.html
效果圖:
題目二
眼前有兩塊100px*100px的兩塊div,分別距離我們10m和7m,但是由於視覺差,我們只看到最靠近我們的一塊板。用鼠標模擬我們的運動軌跡,鼠標右移類似於我們往右側行走。請模擬實現我們右移,所看到兩塊div形態變換。
一開始,我腦海中的構想:
假設100px=4cm,即半徑等於85cm=>2125px。由於時間的關系,就直接把草稿拍照放上來,雅蠛蝶,真的好復雜:
我們圍繞小圈圈,想看到后面的div。在我們接近圓邊東方這個點的過程中,前面的div將會不要斷縮小,而后面的div將會不斷放大。但是鼠標移動npx,而div會放大mpx,需要集合詳細的三角函數和反三角函數計算。好啦沒問題來了,怎么算D1和D2
於是我開始不斷惡補各種高中三角知識。。。。。
先補一下三角函數的基礎知識
再補充一下javascript關於三角函數的方法
Math.cos(x) X的余弦函數
Math.sin(x) X的正弦函數
Math.tan(x) X的正切函數
Math.acos(y) X的反余弦函數
Math.asin(y) X的反正弦函數
Math.atan(y) X的反正切函數
折騰大半個晚上了,為什么還算不出來,難道真的是我理解錯了 好咯好咯~~重新再想想
秉承“怎么簡單怎么算”的原則,我重新理解了一次題目。。。。
10米和7米這兩個數值如果轉換為像素將不利於計算,出來的效果也不太樂觀,我將它們分別改成了10cm和7cm了。
嗯嗯,應該是這樣子了
假設人行走a cm,我們可以通過a來表示D1和D2的長度,期間使用到相似三角形、三角形面積公式等原理,完全不需要三角函數和反三角函數。剩下的就不難了
1 var $ = function(id){ 2 return typeof id == 'string' ? document.getElementById(id) : id; 3 }; 4 //計算前面板初始寬度 5 $("box1").style.width = 28/Math.sqrt(65)*30+'px'; 6 (function(o){ 7 if(o){var o = $(o);}else{return false;}; 8 var d = document,x,y; 9 o.onselectstart = function(){return false;}; 10 o.onmousedown = function(e){ 11 e = e || window.event; 12 x = e.clientX-o.offsetLeft; 13 d.onmousemove = function(e){ 14 e = e || window.event; 15 var a = e.clientX - x; 16 17 //小圓點在虛線范圍內可拖拽 18 if(a>=0 && a<90){ 19 o.style.left = e.clientX - x + "px"; 20 var Div1 = 28/Math.sqrt(65+8*a); 21 var Div2 = (9/7)*a/Math.sqrt((9/49)*a*a+9); 22 $("box1").style.width = Div1*30+'px'; 23 $("box2").style.width = Div2*30+'px'; 24 } 25 26 }; 27 d.onmouseup = function(){ d.onmousemove = null; }; 28 }; 29 })('origin');
源碼及演示地址:http://lovermap.sinaapp.com/rotate.html
拖動小黑點->
拖動小黑點->
總結
第一點,面試前千萬不要熬夜,千萬不要熬夜,千萬不要熬夜!!!重要的事情要說三遍,面試答題的時候,腦袋滿滿的困意和餓意,將會造成悲慘的結局。
第二點,注意要把問題細化,着手解決問題前,應該根據題目所給的條件,將問題拆分成為若干小問題,然后針對性地根據逐一解決。
第三點,避免先入為主。一開始認為問題很難,不斷深究,最后很容易把自己逼得走投無路。當問題暫時無法解決的時候,回到最初的問題,重新分析,換一個角度,將會有新想法。
參考鏈接