解答《編程之美》1.18問題1:給所有未標識方塊標註有地雷概率

  對於《編程之美》上沒有提供答案和提示的1.18和4.11兩節,本文將綜合網絡上已有的部分資料,深入挖掘解題思路,並對目前尚未找到滿意答案的1.18節問題2給出算法解答。閱讀本文需要了解古典概型(百度 /維基)和組合數(百度 / 維基)的含義,以及掃雷遊戲中的各種符號。

  《編程之美》上關於掃雷的概率有兩道題:1.18挖雷遊戲和4.11掃雷遊戲的概率。後者在網上已經有了令人滿意的解答,前者我還沒發現,相關內容也很少。經過近一天的研究,提出了一個自己的解法。這篇博文的寫作過程,同時也是我整理思路的過程。可能有的讀者還沒有看過這兩道題,或者看了之後記不大清楚了,先把兩個問題貼在下面:

1.18 挖雷遊戲

問題1:如果想給遊戲增加一個功能鍵,點擊就能查看剩餘所有未標識的方塊是否有地雷的概率。如何實現?

問題2:如果上一個問題太難了,可以先讓程序先標識所有有地雷的方塊。

 

4.11 掃雷遊戲的概率

      

 

在一個16*16的地雷陣中,有40個地雷。用戶點擊了兩下,出現如圖4-21的局面。分析圖4-22所示的這個局部。

問題1:當遊戲中有40個地雷沒有發現時,A、B、C三個方塊有地雷的概率(P(A),P(B),P(C))各是多少?

問題2:這個局面共有16*16=256個方塊,P(A)、P(B)、P(C)的相互大小關係和當前局面中總雷數有關係麼?比如從10個逐漸變化到240個,三者曲線如何變化、會不會相交?(建議用Matlab做解)

 

   1.18節的問題2比較好解,而且可以同時標記必然沒有地雷的方塊;但問題1就困難了,暫且放一邊;讀了4.11節之後,會發現解4.11節的方法是可以用來理解1.18節的問題的。本文的主要方法就是使用網絡上對4.11節提供的解法和1.18節問題2的輔助功能,來完成問題1的解答。

  先要明確地強調一點,1.18節的問題1、2與一些所謂的自動掃雷算法是不同的:對於確定爲無雷的方塊,並不做點擊動作;也即僅作概率分析,而不實際地翻開來確認並獲得更多信息。因此,有的自動掃雷算法可能每次並不是選擇無雷概率最大的方塊,包含了一些啓發式的嘗試。而這裏將會對當前狀態所有方塊是否有雷的概率進行分析。比如http://www.verydemo.com/demo_c173_i10167.html,我覺得這就是個似是而非的自動掃雷解法,要保持當前狀態來標註概率,又沒讓你挖開看看,你知道的太多了,不對,應該是你做的太過火了。

  爲了便於閱讀,我將一些定義(包括我自己的定義)做成了錨點,如果閱讀時忘記了前面的含義可以點擊鏈接來查看。

  這裏先引入“8鄰接”的定義。這個定義來源於圖形學,如下圖,像素P周圍的這8個像素就是P的8鄰接像素。對應於掃雷問題,同樣的可以對一個方塊定義它的8鄰接方塊。

分析:

  待標識概率的方塊可以根據是否與已經標識的方塊是8鄰接的分爲兩種,如果是,稱之爲“鄰接方塊”,反之則是“非鄰接方塊”。前者可以按照已標識的方塊提供的信息來輔助判斷,後者只能取平均概率。對應於原書的例圖,“鄰接方塊”是兩條黑線之間、既未挖開也未標記爲有地雷的方塊:

  對於這兩種方塊,其有地雷的概率計算方法是不一樣的,下面會逐一分析。

  爲了簡化原圖,同時解答1.18節問題1,先對必然有雷(即P(此方塊有雷)=1)和必然無雷(即P(此方塊有雷)=0)的方塊進行標識,並且把用旗子標識爲“有雷”的地區當作已知是有雷的方塊。步驟如下:

(1)(去掉插旗子的方塊)把這張圖中已經標識爲有雷的區域周圍8個鄰接區域計數減1,並把標識爲雷的區域的雷挖掉。同時,對於從1減爲0的方塊,還要把它8鄰接的所有未標識方塊標爲無雷。這個操作在解問題2之後可以用來進一步轉化,從而解決問題1。

等價轉化如下圖,其中紅色方塊是經過轉化的區域,紅色方塊且不含數字的標識此方塊已無可用信息。具體來說它是由三種方塊轉化而來:

原先標有數字的,此時它的8鄰接區域雷已排完;

原先是雷的,將其有雷概率標記爲1;

原先無雷的,將其有雷概率標記爲0。

注意:它是一種新的已標識方塊,而不是是否有雷未確定的未標識方塊。

(2)(標記必有雷和必無雷的方塊)對所有不爲0的區域,先計算周圍方塊必有雷的方塊。判斷規則:標爲“x”的方塊如果有x個8鄰接的未標識方塊,那麼必有雷。同時,由於這是由邏輯判斷得出的必有雷的區域,不是先前插上旗的,遊戲剩餘雷數計數器沒有變化,需要記錄推理出的雷數,並在剩餘雷數中減去。

(3)重複(1)(2)的轉化,直到標識完畢。在這個過程中,所有地雷概率爲0和1的鄰接方塊已經被處理且標記,問題2得到求解。上圖轉化爲:

並且,在這個過程中標記P(此塊有雷)=1的方塊一共10個,加上之前掃出的3個,那麼40個雷還剩下27個。

這裏再次強調,雖然有的方塊在處理中標記爲P(此塊有雷)=0,即必然無雷,但這只是邏輯推理,並不代表我們真的點開了這個方塊,它實際的數字是未知的,本身並不能提供周圍8鄰接的雷數信息,只能間接地提示它的8鄰接方塊的8鄰接中少了一顆雷而已。

(4)接下來是處理最後一部分,也即P(此方塊有雷)非0非1的部分。這裏借鑑了一個解答《編程之美》問題4.11思路,原內容來自任曉禕的博文,但是這個頁面我是打不開的,好在轉載的人多,比如這裏。另外還有一篇從一般情況入手的博文可以參考。

先簡單闡述一下其思想。(如果這部分沒看懂沒關係,看懂上面鏈接的兩篇就可以理解後面的思路):

  “子雷區”定義爲所有已知信息方塊的8鄰接中所有未挖開未標記方塊以及這些已知信息方塊的並集,例如以下幾個圖中的子雷區是黑框標出的部分,也是我在上面兩條黑線包圍的部分中未挖開未標記的部分。可見,“子雷區”和“鄰接方塊”是類似的,只不過"子雷區"描述的是這些方塊的總體外加一些已知信息,"鄰接方塊"描述的是個體。下面你會看到使用這個定義的方便之處。

思想:

  對於M大小的雷區,其中共有N個雷;已知信息和待判斷位置位於一個M'大小的子雷區,先分析子雷區一共可能有多少個雷;對於每一種雷數,分析其分佈的情況總數,在乘以餘下的雷在子雷區之外的分佈情況,其和就是所有的分佈情況。待判斷位置有雷的概率是它在這些情況中有雷的情況數/所有分佈情況(根據古典概型),寫成公式如下:

現在繼續處理(4)中獲得的子雷區。標記“?”的定義爲“結合點”,對它討論可以把一個複雜的子雷區分成多個簡單的子雷區。比較巧合的是,《編程之美》1.18這個圖,先假設再分情況討論時會發現有種情況是不成立的,另一種情況只有唯一的可能:

這裏舉一個和上文圖不同的例子說明子雷區分別求解的過程。先做出“結合點無雷”假設的假設後,把子雷區分爲這樣兩個不相交的子雷區

這時,要求解A處有雷的概率就稍微簡單了些。與單獨求解一個區域中點是否有雷的概率相比,這些子雷區之間的關係是:

所有子雷區的雷數+非子雷區的雷數+已確定是雷的雷數 = 總雷數。

  利用從上述兩篇博文中獲得的思路,分析如下(這部分的公式是在Word裏打的,HTML編輯器玩的不熟,直接截了個圖粘上來):

對於不在任何一個子雷區的其餘所有點,取任一個點X進行分析:

如果結合點分情況討論,那麼計算方法類似,可以表示爲:

這樣,對於原圖中所有點是否有雷的概率,都能給出了。

 

   這下所有點的概率都能求了,似乎1.18節問題1也能解決了。對(1)(2)(3)這三步是這樣的,維護好包含有用信息的方塊和鄰接方塊的數據結構,這麼標出來所有P=0和P=1的方塊是不難的,然而第 (4)步就犯難了:如何把子雷區劃分成多個子雷區結合點怎麼找?

  先回避這個問題,讓計算機揚長避短,直接在未劃分的子雷區搜索所有可能的地雷分佈情況數,並統計在特定位置有雷的情況數,概率也就能算出來了。這時的子雷區已經很小了,處理起來不會很慢,不過要注意不能忽略子雷區以外的非鄰接方塊中地雷的分佈情況。

  如果非要直面這個問題,讓計算機像人一樣找結合點、劃分子雷區、按結合點分情況計數當然也可以,但是編碼難度大,計算也未必快。可以注意到,與含有信息的已挖開方塊相連的鄰接方塊其實最多隻有一層,並且是一條直線。如果一個含有信息的已挖開方塊與兩層鄰接方塊相連,那麼相連的地方就是待討論的點,如下圖所示,紅色和藍色是兩條鄰接方塊的一層,紫色是鄰接點。接下來就是像人一樣分情況討論了,但由於實際劃分很複雜,而且這個所謂的討論其實還是讓計算機去進行可能情況的搜索,可能性能還不如上面的直接去搜索快一些。另外,我這裏給出的規律是自己總結的,不確定是否有疏漏,可靠性有限。

具體的編碼就略過了,主要過程是簡化原圖+搜索算法。按照(1)~(4)步,完全可以寫出僞代碼框架。

 

另外預先設計了幾個FAQ,當然各位看官如果有什麼問題,不要吝惜留言啊!

 

Q:1.18節標題是“挖雷遊戲”,4.11節標題成了“掃雷遊戲”,居然不一樣!

A:其實這也是我想吐槽的地方:前後不一致,《編程之美》裏可是不少,從代碼的語言到這小小的標題。

 

Q:既然有的方塊是否有雷能直接判斷出來,爲什麼不挖開從而獲得更多信息?

A:這個問題我在正文裏強調了兩遍了,直接點擊這個鏈接跳轉過去再讀一遍吧。

 

Q:在4.11中,按3*5區域中有3個雷還是2個雷進行了分情況計數。爲什麼不用全概率公式(百度/維基)?

A:P(A)=P(A|B1)*P(B1) + P(A|B2)*P(B2) + ... + P(A|Bn)*P(Bn),A={A點有雷的概率},Bi={3*5區域中有i個雷的概率},

  嗯,看上去很清楚,P(A|Bi)是容易計算了。但是P(Bi)你怎麼獲得?

 

Q:爲什麼對一些定義提供了兩個鏈接?

A:事實上我覺得文中出現的幾個定義,百度百科比維基百科闡述的清楚一些。當然有的人不喜歡百度,爲了照顧他們的情緒我把維基百科中的定義也附上了。

 

Q:分析了大半天,最後還是用搜索的方法,真失望。

A:是啊,我也有點失望。但我已經盡力剪枝了,剪到步驟(4) 才權衡是更精細的剪枝開銷大還是從這裏開始搜索開銷大。畢竟結合點的情況太多了,沒有進行很簡單的抽象。這還是比一開始就搜索要好很多了吧?

 

 

Q:我需要的是源代碼……

A:其實我可以直截了當地告訴你,博主在windows編程方面非常之poor,以至於現階段是不可能給你寫出源代碼的。自己努力吧!當然,如果你搞定了,記得把源碼發給我一份^ ^

 

Q:任曉禕的博文打不開,那些轉載裏的圖片也看不了,更不會用MATLAB,可還是想看看4.11的三條曲線。

A:這個希望還是能滿足的。不過注意到由於地雷數N是離散的,實際上繪出來的是散點圖,如下:

我甚至把MATLAB代碼都給你了,直接粘到m文件裏就能重現這個圖像:

 n = 10:1:240;
 m = 256;
 %P(A)
 p1 = (2*n -4)./(3*m+7*n-56);
 plot(n,p1,'r')
 hold on
 %P(B)
 p2 = (n -2)./(10*m-7*n-126);
 plot(n,p2,'g')
 hold on
 %P(C)
 p3 = (20*m -17*n-246)./(50*m - 35*n - 570);
 plot(n,p3,'b')
 legend('P(A)','P(B)','P(C)')

 

不過要注意的是,在圖像中,P(B)看上去與P(A)和P(C)相交,但真的是這樣麼?別忘了這三條其實不是連續的曲線,而是離散的點的連線。

令P(A) = P(B),解得N1 = 2,N2 = 4196/21,都不是10至240之間的整數,所以P(A)和P(B)其實沒有公共點。

再看看P(B)和P(C),這裏解方程比較複雜,直接把圖像放大了看,交點似乎是221。帶入P(B)和P(C)的表達式,二者不相等,只是很近似。

結論是曲線看上去是相交的,但實際上並沒有公共點。怎麼樣,沒有被你的眼睛欺騙吧?

 

Q:博主,你的XX式子中的XX算錯了/掃雷圖中XX格分析錯了。

A:本文中所有式子和圖形標記我都進行了兩遍計算/檢查了兩遍,雖然仍有可能有遺漏的地方,不過也請您先驗證一下自己的論斷吧,如果確實有錯我會改正的。

 

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