相信你們都玩過鬥地主,規則就再也不介紹了。python
直接上一張朋友圈看到的殘局圖:緩存
這道題我剛看到時,曾嘗試用手工來破解,每次都覺得找到了農民的必勝策略時,最後都發現其實農民跑不掉。因爲手工破解沒法窮盡全部可能性,因此這道題究竟農民有沒有妙手跑掉呢,只能經過代碼來幫助咱們運算了。函數
本文將簡要講述怎麼經過代碼來求解此類問題,在最後會公佈殘局的最後結果,並開源代碼以供你們吐槽。優化
代碼的核心思想是minimax。minimax能夠拆解爲兩部分,mini和max,分別是最小和最大的意思。spa
直觀的理解是什麼呢?就有點像A、B兩我的下棋。A如今能夠在N個點走棋,假設A在某個點走棋了,使得A的這一步的盤面評估分數最高;可是輪到B下的時候,就必定會朝着讓A最不利的方向走,使得A的下一步必然按照B設定的軌跡來,而無法達到A在第一步時估算到這一步的最高盤面評分。code
在牌局中是同樣的,若是農民的一手牌,讓地主不管如何應對都不能贏的話,那麼能夠說農民有必勝策略;不然,農民必輸。blog
咱們能夠用一個函數hand_out
來模擬一我的的出牌過程。在現實生活中,一我的想要出牌的話,必然須要知道本身手上的全部牌:me_pokers
,也須要知道上一手的出的牌:last_hand
。若是咱們要用這個函數來模擬兩我的的出牌,則還須要知道對手當前的全部牌:enemy_pokers
。遞歸
這個函數的返回值,是輪到我me_pokers
出牌時,是否可以必贏牌。若是能贏則返回真,不然返回假。token
def hand_out(me_pokers, enemy_pokers, last_hand)
假設輪到我出牌時,若是我手上的牌都出完了,那麼我將馬上知道我贏了;反之若是對手的牌都出完了,而我沒有,則我失敗了。get
由於如今輪到我出牌,因此我首先須要知道我如今能出的全部手牌組合。注意:這個組合中,包括過牌(即不出牌)的策略。
all_hands = get_all_hands(me_pokers)
如今咱們要對全部可能的手牌組合進行遍歷。
首先我須要知道,上一手對方出的牌是什麼。
若是對方上一手選擇過牌,或者沒有上一手牌,那麼我這一輪必須不能過牌,可是我能夠出任意的牌
若是對手上一手出了牌,則我必需要出一個比它更大的牌或者選擇這一輪直接過牌(不出牌)
關鍵點來了,在出完個人牌或選擇過牌後,咱們須要用一個遞歸調用來模擬對手下一步的行爲。若是對手的下一次出牌不能獲勝的話,則我這一次的出牌必勝;不然,對於個人每個出牌選擇,對手都能獲勝的話,則我必敗。
所有代碼以下:
以上核心邏輯理清楚後,構建破解器將變得十分簡單。
首先,咱們要用數字來表示牌的大小,這裏咱們用3表示3,11來表示J,12表示Q,依次類推……
其次,咱們須要求出一個手牌的全部出牌組合,這裏須要get_all_hands
函數,具體實現比較繁瑣可是很簡單,就不在此贅述。
而後,咱們還須要一個牌力判斷函數can_comb2_beat_comb1(comb1, comb2)
,這個函數用於比較兩組手牌的牌力,看是否comb2
能夠擊敗comb1
。惟一須要注意的一點,在鬥地主的規則中,除了炸彈外,其餘全部牌力均等,只有牌型同樣時才能去比較。
最後,咱們須要一個模擬出牌函數make_hand(pokers, hand)
,用於求出在手牌爲pokers
的狀況下打出一手牌hand
後,剩下的手牌,實現也很是簡單,只需簡單的移除掉那些打出的牌便可。
因爲一副牌的可能手牌巨大,致使遞歸的分支數巨大。因此時間開銷很是大,爲階乘級O(N!),根據斯特林公式,大約爲O(N^N)。
因爲可能會有不少重複的牌面出現,致使了不少重複的遞歸調用。因此加一個緩存能極大提高效率。
即對我方手牌和敵方手牌和上一輪手牌的描述(str(me_pokers)+str(enemy_pokers)+str(last_hand)
)爲鍵,將求出的結果存進緩存字典中。下一次遇到相同的局面時,便可直接從緩存字典中取出,而無需再次重複計算。時間複雜度優化爲指數級O(C^N)。
代碼運算出來的結果是,農民沒有必勝策略。換言之,只要地主會玩,農民不可能贏。階級固化已經如斯了麼……