博弈論

【註明】:引用了兩篇博客的內容。

https://blog.csdn.net/qq_36553623/article/details/67061459

https://blog.csdn.net/niushuai666/article/details/6638943

這是我第一次寫這樣總結性,綜合性的講解文章,大佬們如果發現我的錯誤,並且指出的話對我的幫助是非常大的。在這裏感謝大家啦:)

 

目錄

 

一.巴什博奕(Bash Game):

引入

理解

習題  hdu 1846  巴什博弈

二.威佐夫博奕(Wythoff Game):

引入

理解

習題  poj 1067  威佐夫博弈

三.尼姆博奕(Nimm Game):

引入

理解

習題  hdu 1850  尼姆博弈


 

一.巴什博奕(Bash Game):

引入

首先我們來玩一個比較古老的報數遊戲。A和B一起報數,每個人每次最少報一個,最多報4個。輪流報數,看誰先報到30.

如果不知道巴什博弈的可能會覺得這個是個有運氣成分的問題,但是如果知道的人一定知道怎樣一定可以贏。

比如A先報數的話,那麼B一定可以贏(這裏假定B知道怎麼正確的報數)

B可以這樣報數,每次報5-k(A)個數,其中k(A)是A報數的個數這樣的話沒一次

兩人報完數之後會變成5 10 15 20 25 30這樣是不是B一定會贏呢?是不是有一種被欺騙的感覺呢?好吧下面我們來看看這個原理。我們先看下一個一眼就能看出答案的例子 比如說我們報到5(4+1),每次報最多報4個,最少報1個.那麼是不是後者一定可以贏呢?答案是肯定的。好了到這巴什博弈的精髓基本就OK了。

那麼如果我們要報到n+1,每次最多報n個,最少報1個的話,後者一定能夠贏。

現在我們需要報數到n,而每次最多報數m個,最少報數1個.我們可以化成這樣

n = k*(1+m)+r(0 <= r <= m)這樣的話如果r不等於0那麼先手一定會贏,爲什麼呢?首先先手報r個,那麼剩下k倍(1+m)個數,那麼我們每次報數1+m-k(B)個數就一定能保證最後剩下1+m個,那麼就到了上面我們說的那個了,先手就一定會贏,如果r=0那麼後手一定會贏,道理一樣的。

到這巴什博弈也就介紹完了,知道這個道理之後我們也可以去騙小朋友了。-_-//

 

理解

進入遊戲的狀態就決定了輸贏。如果兩個人可以說的數是1~K,那麼如果進入遊戲時,你面對的總數n是(K+1)的倍數,那麼對方贏,若不是(K+1)的倍數,則你贏。(因爲你一定可以將n變爲(K+1)的倍數,而後對方說出一個數x,你說(K+1-x)即可)。

例如可以說的數是1~10,總數是n=30,你先說。(這時我們注意一個隱藏條件,對方說出一個數字,你一定可以說出另外一個數字,並讓二者和爲11,也就是上一段提到的(K+1))。你說8,n變爲22。這時無論對方說什麼,你都可以讓n變爲11,然後再變爲0。

 

習題  hdu 1846  巴什博弈

#include<iostream> 
#include<stdio.h>
#include<sstream>
#include<string>
#include<algorithm>
#include<vector>
#include<math.h>
#include<string.h>
using namespace std; 


int main() 
{
	int c, n, m;
	cin>>c;
	for(int i=0; i<c; i++) {
		cin>>n>>m;
		if(n%(m+1) == 0)
			cout<<"second"<<endl;
		else
			cout<<"first"<<endl;
	} 
    return 0;
}      

 

二.威佐夫博奕(Wythoff Game):

引入

這種博弈比前面一種要稍微複雜一點。我們來看下下面這個遊戲。

有兩堆火柴棍,每次可以從某一堆取至少1根火柴棍(無上限),或者從兩堆取相同的火柴棍數。最後取完的是勝利者。好了,如果你不知道這個博弈定理,對於小數目的火柴棍數,可能還能推出來,但是如果火柴棍數一多,就不行了。看了下面的這個介紹,你也會有一種被騙的感覺。

首先我們知道兩堆火柴是沒有差別的,也就是說第一堆有a根,第二堆有b根和第一堆有b根,第二堆有a根是一樣的結果。

我們用一個二維的狀態(a,b)來記錄當前剩下的火柴數,表示第一堆剩下a根火柴,第二堆剩下b根火柴。同樣我們假設兩個人的編號是A和B,且A先取。

那麼如果某個人遇到了這樣的狀態(0,0)那麼也就是說這個人輸了。這樣的狀態我們叫做奇異狀態,也可以叫做失敗態。

那麼接下來的幾個失敗態爲(1,2),(3,5),(4,7),(6,10),(8,13)……

我們用a[i]表示失敗態中的第一個,b[i]表示失敗態中的第二個.(i從0開始).

那麼我們可以看到b[i] = a[i]+i;(i >= 0),a[i]是前面的失敗態中沒有出現過的最小的整數

下面我們可以得到三個基本的結論。

1.每個數僅包含在一個失敗態中

首先我們知道a[k]是不可能和前面的失敗態中的a[i],b[i]重複的(這點由a[i]的得到可以知道)

b[k] = a[k]+k > a[k-1]+k>a[k-1]+k-1+1>a[k-1]+(k-1) = b[k-1]>a[k-1]這樣我們知道每個數僅在一個失敗態中。

2.每個失敗態可以轉到非失敗態。

加入當前的失敗態爲(a,b),那麼如果我們只在一堆中取的話,肯定會變成非失敗態(這點由第一點可以保證),如果從兩堆同時取的話,由於每個失敗態的差是不一樣的,所以也不可能得到一個失敗態。也就是說一個失敗態不管你怎麼取,都會得到一個非失敗態。

   3.每個非失敗態都可以轉到一個失敗態

對於這個結論,首先我們要知到每個狀態(a,b)要麼a = a[i],要麼b = b[i].(每個數都出現在一個失敗態中),下面我們分兩種情況來討論

   I.a = a[i].如果b = a的話那麼一次取完就變成了(0,0).如果b > b[i]的話,那麼我們從第二堆中取走b-b[i]就變成了一個失敗態。如果b < b[i].那麼我們從兩堆中同時取走a-a[b-a[i]]這樣得到失敗態(a[b-a[i]],a[b-a[i]]+b-a[i])(a[i] = a)

   II.b = b[i].如果a > a[i]那麼我們從第一堆中取走a-a[i]根火柴.

              如果a < a[i].這裏又分兩種情況。第一是a = a[k](k < i)

那麼我們從第二堆取走b - b[k]就行了。

第二是a = b[k]這樣的話由於兩堆火柴是沒有區別的,所以我們把b變成a[k]就行了,也即是從第二堆火柴中取走b - a[k]就變成了失敗態

至於怎麼判斷一個狀態是否是失敗態.我們可以用下面的方法來判斷(本人暫時還不會證明)

  a[i] = [i*(1+√5)/2](這裏的中括號表示向下取整)   b[i] = a[i]+i;

  那麼這就是一個失敗態

 

理解

兩堆石子,兩種取法:①從任意一堆中取任意個;②從兩堆中同時取相同多個。

這個Wythoff博弈論和Bash博弈論其實有些像,就是存在兩種狀態:①必贏,②必輸。如果你從必贏狀態進入遊戲,下一步對手只能進入必輸狀態,然後輪到你的時候你又可以進入必贏狀態。(必贏狀態無論怎麼走都是進入必輸狀態,必輸狀態一定可以一步進入必贏狀態)。

而Wythoff的必輸狀態比較特別,是(0,0),(1,2),(3,5),(4,7),(6,10),(8,13)。。。。

我學習時遇到的疑問就是這些數字怎麼來的,設爲(a,b)。

                           a=[k*\frac{1+\sqrt{5}}{2}]  ([ ]表示向下取整),       b=a+k ,         (k=0,1,2,3....)

具體爲什麼可以見百度百科有證明過程

對於我們編代碼或者計算呢,背下來就ok了。所以我們只需判斷進入遊戲時給我們的兩個數字是否符合上面的公式,如果符合那麼我們輸了(後手贏),如果不符合則我們贏(先手贏)。

對於不理解怎麼走就不開心的同學呢,我們可以走走試試。假如初狀態是(4,6),我們發現差值爲2,則我們可以轉化爲差值同樣爲2的失敗態(3,5),因爲任意差值的失敗態我們都可以找到。假如初狀態是(3,6),我們發現差值爲3,爲啥不行了????別急我們還是可以轉化爲失敗態(3,5),因爲我們可以找到和a相比配的失敗態。相信我任何組合都逃不出這兩種情況。然後對手在(3,5)的情況下任意走,我們還是可以根據上面兩種情況找到下一個失敗態。如果想口算或者和其他同學玩的話可以用:a=k*1.6試試。數小的時候精度ok的。

 

習題  poj 1067  威佐夫博弈

#include<iostream> 
#include<stdio.h>
#include<sstream>
#include<string>
#include<algorithm>
#include<vector>
#include<math.h>
#include<string.h>
using namespace std; 


int main() 
{
	int a, b;
	while(cin>>a>>b) {
		if(a>b)
			swap(a,b);
		int k = b-a;
		int c = k*(1+sqrt(5))/2;
		if(c == a)
			cout<<"0"<<endl;
		else
			cout<<"1"<<endl;
	}
    return 0;
}      

 

三.尼姆博奕(Nimm Game):

引入

指的是這樣的一個博弈遊戲,目前有任意堆石子,每堆石子個數也是任意的,雙方輪流從中取出石子,規則如下:
1)每一步應取走至少一枚石子;每一步只能從某一堆中取走部分或全部石子;
2)如果誰取到最後一枚石子就勝。
也就是尼姆博弈(Nimm Game)。
必敗局面:也叫奇異局勢。無論做出何出操作,最終結果都是輸的局面。必敗局面經過2次操作後,可以達到另一個必敗局面。
必勝局面:經過1次操作後可以達到必敗局面。
即當前局面不是必敗局面就是必勝局面,而必勝局面可以一步轉變成必敗局面。
最終狀態:
(1)最後剩下一堆石子;(必勝局面)
(2)剩下兩堆,每堆一個;(必敗局面)
(3)當石子剩下兩堆,其中一堆只剩下1顆,另一堆剩下多於n顆石子時,當前取的人只需將多於1顆的那一堆取出n-1顆,則局面變爲剛纔提到的必敗局面。(必勝局面)
判斷當前局勢是否爲必勝(必敗)局勢:
1)把所有堆的石子數目用二進制數表示出來,當全部這些數按位異或結果爲0時當前局面爲必敗局面,否則爲必勝局面;
2)在必勝局面下,因爲所有數按位異或的結果是大於零的,那麼通過一次取,將這個(大於其它所有數按位異或的結果的)數下降到其它所有數按位異或的結果,這時局面就變爲必敗局面了。
定理:一組自然數中必然存在一個數,它大於等於其它所有數按位異或的結果。
證明:原命題等價於,設a1^a2^... ^an=p,p≠0時,必存在k,使得ak^p<ak(當p=0時,對於任意的k,有ak^p=ak)。
設p的最高位是第q位,則至少存在一個k,使得ak的第q位也是1,而ak^p的第q位爲0,所以ak^p<ak

 

理解

我們可以從n堆石子中的任意一堆石子中取任意個石子,目標是取到最後一堆石子。這其實和前兩種博弈也是相同的道理。但是我的思路入口是“平衡”,什麼是“平衡”呢?就是在這個狀態我們一定可以同過歐數次操作取完石子堆,原因是無論對手如何打破平衡我們都可以再讓整個狀態回到平衡。

例如兩堆石子(10,10)。這就是個平衡態(先手輸,後手贏),對手無論怎麼做都會打破這個平衡態。而我們只需模仿對手的行爲恢復平衡態,或者直接勝利。因爲是n堆,所以我們要找n堆的平衡態就是利用按位異或的方法。

按位異或就是指相同得1,不同得0。把每個石子堆的石子數量按位異或得到的就是整個石子堆的不平衡點,我們只需要從某一堆中那走這麼多的石子即可。

例如(1,2,1),按位異或結果爲2。我們讓每堆石子再去異或[結果2],若得到的數比原堆數小,則我們讓這堆石子變爲得到數就可以了(因爲這樣再去異或結果一定爲0),變爲(1,0,1)。進入了平衡狀態。

又例如(1,2,3),按位異或結果爲0,是平衡狀態,變爲(0,2,3)則可通過變爲(0,2,2)回到平衡狀態[異或結果爲1],變爲(1,2,2)則可通過變爲(0,2,2)回到平衡狀態。等等。自己可以嘗試感受一下按位異或的作用。

所以我們的做題思路就很清晰:按位異或,結果爲0後手贏,不爲0先手贏。

 

習題  hdu 1850  尼姆博弈

#include<iostream> 
#include<stdio.h>
#include<sstream>
#include<string>
#include<algorithm>
#include<vector>
#include<math.h>
#include<string.h>
using namespace std; 


int main() 
{
	int m;
	while(1) {
		cin>>m;
		if(!m)
			break;
		int arr[105];
		int temp ;
		cin>>temp;
		arr[0] = temp;
		for(int i=1; i<m; i++) {
 			cin>>arr[i];
			temp ^= arr[i];
		} 
		if(temp == 0)
			cout<<0<<endl;
		else {
			int cnt=0;
			for(int i=0; i<m; i++) {
				if((temp^arr[i])<arr[i])
				cnt++;
			}
			cout<<cnt<<endl;
		}
	}
    return 0;
}      

 

四、P/N圖解博弈論

引入

http://qianmacao.blog.163.com/blog/static/203397180201222945044622/

 

習題  HDU 2147 kiki's game 

#include<iostream> 
#include<stdio.h>
#include<sstream>
#include<string>
#include<algorithm>
#include<vector>
#include<math.h>
#include<string.h>
using namespace std; 


int main() 
{
	int n, m;
	while(true) {
		cin>>n>>m;
		if(n==0 && m==0)
			break;
		if(n%2!=0 && m%2!=0)
			cout<<"What a pity!"<<endl;
		else
			cout<<"Wonderful!"<<endl;
	}
	
	 
	
    return 0;
} 

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