色哟哟视频在线观看-色哟哟视频在线-色哟哟欧美15最新在线-色哟哟免费在线观看-国产l精品国产亚洲区在线观看-国产l精品国产亚洲区久久

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

說透游戲中常用的兩種隨機算法

算法與數據結構 ? 來源:labuladong ? 作者:labuladong ? 2022-11-09 11:17 ? 次閱讀

沒事兒的時候我喜歡玩玩那些經典的 2D 網頁小游戲,我發現很多游戲都要涉及地圖的隨機生成,比如掃雷游戲中地雷的位置應該是隨機分布的:

e7b652ca-5fdc-11ed-8abf-dac502259ad0.jpg

再比如經典炸彈人游戲,障礙物的位置也是有一定隨機性的:

e7d12dac-5fdc-11ed-8abf-dac502259ad0.jpg

這些 2D 游戲相較現在的大型 3D 游戲雖然看起來有些簡陋,但依然用到很多有趣算法技巧,本文就來深入研究一下地圖的隨機生成算法。

2D 游戲的地圖肯定可以抽象成一個二維矩陣,就拿掃雷舉例吧,我們可以用下面這個類表示掃雷的棋盤:

classGame{
intm,n;
//大小為m*n的二維棋盤
//值為true的地方代表有雷,false代表沒有雷
boolean[][]board;
}

如果你想在棋盤中隨機生成k個地雷,也就是說你需要在board中生成k個不同的(x, y)坐標,且這里面x, y都是隨機生成的。

對于這個需求,首先一個優化就是對二維矩陣進行「降維打擊」,把二維數組轉化成一維數組

classGame{
intm,n;
//長度為m*n的一維棋盤
//值為true的地方代表有雷,false代表沒有雷
boolean[]board;

//將二維數組中的坐標(x,y)轉化為一維數組中的索引
intencode(intx,inty){
returnx*n+y;
}

//將一維數組中的索引轉化為二維數組中的坐標(x,y)
int[]decode(intindex){
returnnewint[]{index/n,index%n};
}
}

這樣,我們只要在[0, m * n)中選取一個隨機數,就相當于在二維數組中隨機選取了一個元素。

但問題是,我們現在需要隨機選出k不同的位置放地雷。你可能說,那在[0, m * n)中選出來k個隨機數不就行了?

是的,但實際操作起來有些麻煩,因為你很難保證隨機數不重復。如果出現重復的隨機數,你就得再隨機選一次,直到找到k個不同的隨機數。

如果k比較小m * n比較大,那出現重復隨機數的概率還比較低,但如果km * n的大小接近,那么出現重復隨機數的概率非常高,算法的效率就會大幅下降。

那么,我們有沒有更好的辦法能夠在線性的時間復雜度解決這個問題?其實是有的,而且有很多種解決方案。

洗牌算法

第一個解決方案,我們可以換個思路,避開「在數組中隨機選擇k個元素」這個問題,把問題轉化成「如何隨機打亂一個數組」

現在想隨機初始化k顆地雷的位置,你可以先把這k顆地雷放在board開頭,然后把board數組隨機打亂,這樣地雷不就隨機分布到board數組的各個地方了嗎?

洗牌算法,或者叫隨機亂置算法就是專門解決這個問題的,我們可以看下力扣第 384 題「打亂數組」:

e7eee2fc-5fdc-11ed-8abf-dac502259ad0.jpg

這個shuffle函數是算法的關鍵,直接看解法代碼吧:

classSolution{
privateint[]nums;
privateRandomrand=newRandom();

publicSolution(int[]nums){
this.nums=nums;
}

publicint[]reset(){
returnnums;
}

//洗牌算法
publicint[]shuffle(){
intn=nums.length;
int[]copy=Arrays.copyOf(nums,n);
for(inti=0;i//生成一個[i,n-1]區間內的隨機數
intr=i+rand.nextInt(n-i);
//交換nums[i]和nums[r]
swap(copy,i,r);
}
returncopy;
}

privatevoidswap(int[]nums,inti,intj){
inttemp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
}

洗牌算法的時間復雜度是 O(N),而且邏輯很簡單,關鍵在于讓你證明為什么這樣做是正確的。排序算法的結果是唯一可以很容易檢驗的,但隨機亂置算法不一樣,亂可以有很多種,你怎么能證明你的算法是「真的亂」呢?

分析洗牌算法正確性的準則:產生的結果必須有n!種可能。這個很好解釋,因為一個長度為n的數組的全排列就有n!種,也就是說打亂結果總共有n!種。算法必須能夠反映這個事實,才是正確的。

有了這個原則再看代碼應該就容易理解了:

對于nums[0],我們把它隨機換到了索引[0, n)上,共有n種可能性;

對于nums[1],我們把它隨機換到了索引[1, n)上,共有n - 1種可能性;

對于nums[2],我們把它隨機換到了索引[2, n)上,共有n - 2種可能性;

以此類推,該算法可以生成n!種可能的結果,所以這個算法是正確的,能夠保證隨機性。

水塘抽樣算法

學會了洗牌算法,掃雷游戲的地雷隨機初始化問題就解決了。不過別忘了,洗牌算法只是一個取巧方案,我們還是得面對「在若干元素中隨機選擇k個元素」這個終極問題。

要知道洗牌算法能夠生效的前提是你使用數組這種數據結構,如果讓你在一條鏈表中隨機選擇k個元素,肯定不能再用洗牌算法來蒙混過關了。

再比如,假設我們的掃雷游戲中棋盤的長和寬非常大,已經不能在內存中裝下一個大小為m * nboard數組了,我們只能維護一個大小為k的數組記錄地雷的位置:

classGame{
//棋盤的行數和列數(非常大)
intm,n;
//長度為k的數組,記錄k個地雷的一維索引
int[]mines;

//將二維數組中的坐標(x,y)轉化為一維數組中的索引
intencode(intx,inty){
returnx*n+y;
}

//將一維數組中的索引轉化為二維數組中的坐標(x,y)
int[]decode(intindex){
returnnewint[]{index/n,index%n};
}
}

這樣的話,我們必須想辦法在[0, m*n)中隨機選取k個不同的數字了。

這就是常見的隨機抽樣場景,常用的解法是水塘抽樣算法(Reservoir Sampling)。水塘抽樣算法是一種隨機概率算法,會者不難,難者不會。

我第一次見到這個算法問題是谷歌的一道算法題:給你一個未知長度的單鏈表,請你設計一個算法,只能遍歷一次,隨機地返回鏈表中的一個節點。

這里說的隨機是均勻隨機(uniform random),也就是說,如果有n個元素,每個元素被選中的概率都是1/n,不可以有統計意義上的偏差。

一般的想法就是,我先遍歷一遍鏈表,得到鏈表的總長度n,再生成一個[0,n-1)之間的隨機數為索引,然后找到索引對應的節點。但這不符合只能遍歷一次鏈表的要求。

這個問題的難點在于隨機選擇是「動態」的,比如說你現在你已經遍歷了 5 個元素,你已經隨機選取了其中的某個元素a作為結果,但是現在再給你一個新元素b,你應該留著a還是將b作為結果呢?以什么邏輯做出的選擇,才能保證你的選擇方法在概率上是公平的呢?

先說結論,當你遇到第i個元素時,應該有1/i的概率選擇該元素,1 - 1/i的概率保持原有的選擇。看代碼容易理解這個思路:

/*返回鏈表中一個隨機節點的值*/
intgetRandom(ListNodehead){
Randomr=newRandom();
inti=0,res=0;
ListNodep=head;
//while循環遍歷鏈表
while(p!=null){
i++;
//生成一個[0,i)之間的整數
//這個整數等于0的概率就是1/i
if(0==r.nextInt(i)){
res=p.val;
}
p=p.next;
}
returnres;
}

對于概率算法,代碼往往都是很淺顯的,但是這種問題的關鍵在于證明,你的算法為什么是對的?為什么每次以1/i的概率更新結果就可以保證結果是平均隨機的?

我們來證明一下,假設總共有n個元素,我們要的隨機性無非就是每個元素被選擇的概率都是1/n對吧,那么對于第i個元素,它被選擇的概率就是:

e80b8b14-5fdc-11ed-8abf-dac502259ad0.png

i個元素被選擇的概率是1/i,在第i+1次不被替換的概率是1 - 1/(i+1),在第i+2次不被替換的概率是1 - 1/(i+2),以此類推,相乘的結果是第i個元素最終被選中的概率,也就是1/n。因此,該算法的邏輯是正確的。

同理,如果要在單鏈表中隨機選擇k個數,只要在第i個元素處以k/i的概率選擇該元素,以1 - k/i的概率保持原有選擇即可。代碼如下:

/*返回鏈表中k個隨機節點的值*/
int[]getRandom(ListNodehead,intk){
Randomr=newRandom();
int[]res=newint[k];
ListNodep=head;

//前k個元素先默認選上
for(inti=0;inull;i++){
res[i]=p.val;
p=p.next;
}

inti=k;
//while循環遍歷鏈表
while(p!=null){
i++;
//生成一個[0,i)之間的整數
intj=r.nextInt(i);
//這個整數小于k的概率就是k/i
if(jreturnres;
}

對于數學證明,和上面區別不大:

e81eac12-5fdc-11ed-8abf-dac502259ad0.png

雖然每次更新選擇的概率增大了k倍,但是選到具體第i個元素的概率還是要乘1/k,也就回到了上一個推導。

類似的,回到掃雷游戲的隨機初始化問題,我們可以寫一個這樣的sample抽樣函數:

//在區間[lo,hi)中隨機抽取k個數字
int[]sample(intlo,inthi,intk){
Randomr=newRandom();
int[]res=newint[k];

//前k個元素先默認選上
for(inti=0;iinti=k;
//while循環遍歷數字區間
while(i//生成一個[0,i)之間的整數
intj=r.nextInt(i);
//這個整數小于k的概率就是k/i
if(j1;
}
}
returnres;
}

這個函數能夠在一定的區間內隨機選擇k個數字,確保抽樣結果是均勻隨機的且只需要 O(N) 的時間復雜度。

蒙特卡洛驗證法

上面講到的洗牌算法和水塘抽樣算法都屬于隨機概率算法,雖然從數學上推導上可以證明算法的思路是正確的,但如果你筆誤寫出 bug,就會導致概率上的不均等。更神奇的是,力扣的判題機制能夠檢測出這種概率錯誤。

那么最后我就來介紹一種方法檢測隨機算法的正確性:蒙特卡洛方法。我猜測力扣的判題系統也是利用這個方法來判斷隨機算法的正確性的。

記得高中有道數學題:往一個正方形里面隨機打點,這個正方形里緊貼著一個圓,告訴你打點的總數和落在圓里的點的數量,讓你計算圓周率。

e8484e00-5fdc-11ed-8abf-dac502259ad0.png

這其實就是利用了蒙特卡羅方法:當打的點足夠多的時候,點的數量就可以近似代表圖形的面積。結合面積公式,可以很容易通過正方形和圓中點的數量比值推出圓周率的。

當然,打的點越多,算出的圓周率越準確,充分體現了大力出奇跡的道理。

比如,我們可以這樣檢驗水塘抽樣算法sample函數的正確性:

publicstaticvoidmain(String[]args){
//在[12,22)中隨機選3個數
intlo=12,hi=22,k=3;
//記錄每個元素被選中的次數
int[]count=newint[hi-lo];
//重復10萬次
intN=1000000;
for(inti=0;iint[]res=sample(lo,hi,k);
for(intelem:res){
//對隨機選取的元素進行記錄
count[elem-lo]++;
}
}
System.out.println(Arrays.toString(count));
}

這段代碼的輸出如下:

[300821,299598,299792,299198,299510,300789,300022,300326,299362,300582]

當然你可以做更細致的檢查,不過粗略看看,各個元素被選中的次數大致是相同的,這個算法實現的應該沒啥問題。

對于洗牌算法中的shuffle函數也可以采取類似的驗證方法,我們可以跟蹤某一個元素x被打亂后的索引位置,如果x落在各個索引的次數基本相同,則說明算法正確,你可以自己嘗試實現,我就不貼代碼驗證了。

拓展延伸

到這里,常見的隨機算法就講完了,簡單總結下吧。

洗牌算法主要用于打亂數組,比如我們在快速排序詳解及運用中就用到了洗牌算法保證快速排序的效率。

水塘抽樣算法的運用更加廣泛,可以在序列中隨機選擇若干元素,且能保證每個元素被選中的概率均等。

對于這些隨機概率算法,我們可以用蒙特卡洛方法檢驗其正確性。

最后留幾個拓展題目:

1、本文開頭講到了將二維數組坐標(x, y)轉化成一維數組索引的技巧,那么你是否有辦法把三維坐標(x, y, z)轉化成一維數組的索引呢?

2、如何對帶有權重的樣本進行加權隨機抽???比如給你一個數組w,每個元素w[i]代表權重,請你寫一個算法,按照權重隨機抽取索引。比如w = [1,99],算法抽到索引 0 的概率是 1%,抽到索引 1 的概率是 99%,答案見我的這篇文章。

3、實現一個生成器類,構造函數傳入一個很長的數組,請你實現randomGet方法,每次調用隨機返回數組中的一個元素,多次調用不能重復返回相同索引的元素。要求不能對該數組進行任何形式的修改,且操作的時間復雜度是 O(1),答案見我的這篇文章

審核編輯 :李倩


聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 算法
    +關注

    關注

    23

    文章

    4613

    瀏覽量

    92946
  • 生成器
    +關注

    關注

    7

    文章

    315

    瀏覽量

    21025
  • 數組
    +關注

    關注

    1

    文章

    417

    瀏覽量

    25956

原文標題:說透游戲中常用的兩種隨機算法

文章出處:【微信號:TheAlgorithm,微信公眾號:算法與數據結構】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    蘋果正在打造專為iOS用戶設計的游戲中

     10月23日,據國外媒體報道,蘋果公司正致力于開發一款新型應用程序,意在將App Store與Game Center的功能融為一體,打造一個專為iOS用戶設計的游戲中心。
    的頭像 發表于 10-23 16:00 ?875次閱讀

    噪聲傳導的兩種模式

    噪聲傳導有兩種模式,一為差模傳導,一為共模傳導。
    的頭像 發表于 10-15 11:33 ?302次閱讀
    噪聲傳導的<b class='flag-5'>兩種</b>模式

    晶閘管的阻斷狀態有兩種是什么

    晶閘管(Thyristor)是一半導體器件,具有單向導電性,廣泛應用于電力電子領域。晶閘管的阻斷狀態有兩種:正向阻斷狀態和反向阻斷狀態。以下是對這兩種阻斷狀態的分析。 正向阻斷狀態 正向阻斷狀態
    的頭像 發表于 08-14 16:49 ?745次閱讀

    華為設備中常用的RIP命令及其應用

    RIP(Routing Information Protocol,路由信息協議)是一應用廣泛的距離矢量路由協議,尤其適用于中小型網絡。本文將詳細介紹在華為設備中常用的RIP命令及其應用,以幫助網絡管理員和工程師更好地理解和配置RIP協議。
    的頭像 發表于 08-12 18:10 ?763次閱讀

    PCBA加工中常見的兩種焊接方式詳解

    ,在PCBA行業中經常被使用。接下來深圳PCBA加工廠家為大家詳細介紹PCBA加工手工焊接的兩種方式,為您揭秘行業內的技術細節。 PCBA加工過程中常用焊接方式 第一方式是傳統手工焊接。這種方式主要依靠技術工人的手動操作進行焊
    的頭像 發表于 06-14 09:18 ?553次閱讀

    輕松搞懂傳和非傳的區別

    傳和非傳是數據通信中的兩種不同模式,各自有其適用場景和優勢。傳模式簡單、高效,適用于數據完整性要求高的場景;非傳模式則通過數據處理提
    的頭像 發表于 06-05 12:03 ?9759次閱讀
    輕松搞懂<b class='flag-5'>透</b>傳和非<b class='flag-5'>透</b>傳的區別

    充電樁為什么有直流與交流兩種接口?

    充電樁設計有直流(DC)和交流(AC)兩種接口,主要是為了適應不同類型的電動汽車(EV)充電需求以及電池的充電特性。
    的頭像 發表于 04-30 15:33 ?1631次閱讀

    Xbox應用新增“游戲中心”功能

    微軟 Xbox 體驗高級產品經理 Dylan Meade表示,“游戲中心”便于玩家追蹤游戲進展,發現游戲最新內容和擴展包,與友人聯機競技,接收開發商的最新新聞等。
    的頭像 發表于 02-27 14:02 ?689次閱讀

    gis中常用的空間分析方法

    GIS中常用的空間分析方法 GIS(地理信息系統)是一用于收集、存儲、處理、分析和展示地理數據的技術。空間分析是GIS的核心部分,它包括一系列方法和技術,用來研究地理空間數據之間的關系和模式。本文
    的頭像 發表于 02-25 13:44 ?5668次閱讀

    異或門兩種常見的實現方式

    兩種實現方式都能夠實現異或門的功能,具體的選擇取決于設計需求和邏輯門的可用性。實際構建異或門時,可以使用離散電子元件(如晶體管、二極管等)或整合電路芯片(如 TTL、CMOS 等)來實現。
    的頭像 發表于 02-04 17:30 ?1.2w次閱讀
    異或門<b class='flag-5'>兩種</b>常見的實現方式

    雙絞線接法的標準有哪兩種 雙絞線使用什么端接頭

    雙絞線是一常用的網絡傳輸介質,其接法標準有兩種:直通接法和交叉接法。雙絞線通常使用RJ45接頭。 第一雙絞線接法標準是直通接法,也稱為直通連接。該連接方式使用相同的引腳對線纜的
    的頭像 發表于 02-03 11:11 ?4634次閱讀

    半導體存儲器有哪些 半導體存儲器分為哪兩種

    半導體存儲器(Semiconductor Memory)是一電子元件,用于存儲和檢索數據。它由半導體材料制成,采用了半導體技術,是計算機和電子設備中最常用的存儲器。 半導體存儲器可以分為兩種
    的頭像 發表于 02-01 17:19 ?3102次閱讀

    想知道淮安摜蛋游戲中的RFID技術有什么作用嗎?

    在淮安,有一深受群眾喜愛的撲克游戲,名為摜蛋。它是一融合了升級、跑得快等多種玩法的撲克游戲,具有極高的趣味性和競技性。近年來,隨著科技的不斷發展,RFID技術也開始在淮安摜蛋
    的頭像 發表于 01-26 17:01 ?586次閱讀

    加速度傳感器常用的有哪兩種

    加速度傳感器常用的有兩種,一是基于壓電效應的壓電式加速度傳感器,另一是基于微機電系統(MEMS)技術的微型加速度傳感器。 壓電式加速度傳感器是利用壓電晶體的壓電效應來測量加速度的。
    的頭像 發表于 01-15 15:27 ?1037次閱讀

    分享兩種簡單的平衡電橋設備設計

    本文給出了兩種簡單的平衡電橋設備設計,借此即可對個電感進行高精度的比較。LED指示器或高阻抗電話耳機用作不平衡指示器。
    的頭像 發表于 01-05 09:31 ?898次閱讀
    分享<b class='flag-5'>兩種</b>簡單的平衡電橋設備設計
    主站蜘蛛池模板: 伊人电院网| 小sao货ji巴cao死你视频| 日日夜夜国产| 97免费视频观看| 九九热视频免费| 亚洲精品蜜夜内射| 国产精品一区二区三区四区五区| 日本午夜精品理论片A级APP发布| 51精品国产AV无码久久久密桃| 精品亚洲大全| 亚洲中文字幕在线精品| 国模丽丽啪啪一区二区| 性做久久久久久久久浪潮| 国产成人一区二区三中文| 视频成人app永久在线观看| 纯肉合集(高H)| 日产日韩亚洲欧美综合搜索| 边摸边吃奶边做激情叫床视| 青青草偷拍国产亚洲欧洲| www.久艹| 日日操夜夜摸| 国产美女一区二区| 亚洲精品6久久久久中文字幕| 国产亚洲精品在线视频| 亚洲精品视频在线观看视频| 国语自产一区视频| 伊人久久大香线蕉无码麻豆| 久久受www免费人成_看片中文| 777久久人妻少妇嫩草AV| 欧美性色生活片天天看99顶级| 白丝萝莉喷水| 无码成A毛片免费| 禁室培欲在线视频免费观看| 在线va无卡无码高清| 秘密教学26我们在做一次吧免费| 99久久久久国产精品免费| 色吧电影院| 国产精自产拍久久久久久蜜| 亚洲伊人久久大香线蕉综合图片| 两个奶头被吃得又翘又痛| 成人免费公开视频|