博弈论学习总结

博弈论初步

必胜点和必败点的概念:
P点:必败点,换而言之,就是谁处于此位置,则在双方操作正确的情况下必败。
N点:必胜点,处于此情况下,双方操作均正确的情况下必胜。
必胜点和必败点的性质:  
1、所有终结点是 必败点 P 。(我们以此为基本前提进行推理,换句话说,我们以此为假设)    
2、从任何必胜点N 操作,至少有一种方式可以进入必败点 P。
3、无论如何操作,必败点P 都只能进入 必胜点 N。

我们研究必胜点和必败点的目的时间为题进行简化,有助于我们的分析。通常我们分析必胜点和必败点都是以终结点进行逆序分析。

【硬币游戏】

A与B玩游戏,给定k个数字a1…ak,有x枚硬币,A与B轮流取,每次所取的硬币数要在a1…ak中。A先取,取走最后一枚硬币的一方获胜,双方都采取最优策略,问谁获胜。

我们从终点开始逆序推,设win[x]数组表示轮到我取的时候剩下x枚硬币,我能赢与否。

那么win[0]=0;

代码如下:

void solve(){
	win[0]=0;
	for(int j=1;j<=X;j++){//枚举还剩几枚硬币 
		win[j]=0;
		for(int i=0;i<k;i++){//枚举一次取a[i]枚 
			win[j]|=(a[i]<=j&&!win[j-a[i]]);
			/*
			或运算,1|0=1 
			这里表示若可以取走(a[i]<=j)并且取走后对方一定输(win[j-a[i]]==0) 
			那么我就赢,只要有一种符合,那我就会赢 
			*/
		}
	}
	if(win[x])printf("A\n");
	else printf("B\n");
}

相关题:HDU - Good Luck in CET-4 Everybody!

【推理】

POJ2484 - A Funny Game(对称性)

硬币排成一个圈,A与B轮流从中取一枚或两枚,不过取两枚的时候,索取的必须是连续的,硬币取走留下空位,相隔空位的硬币视为不连续。A先取,取走最后一枚硬币的一方获胜。

n==1||n==2时,A胜。

其他情况,B胜。

B只要保证A取完,B取完使有硬币分成两个完全相同的状态,那么最后一个取完硬币的一定是B。

POJ1082 - Calendar Game

日期找规律,这个好难找啊qwq

给你一个日期(y-m-d),AB轮流往后数,A先,每次可以往后数1日或者往后数1月,问A能否先数到2001年11月4日。

我们发现的规律:不管往后数1日还是1月,m+d的奇偶性都会发生改变,11+4是奇数,所以开始日期是偶数一定可以赢

若开始日期是奇数,有可能会翻盘哦~

两个特殊日期:9.30->10.1,11.30->12.1,所以开始日期是这两个,我们也可以保证胜

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<vector>
#include<set>
using namespace std;

int main(){
	int t,y,m,d;
	scanf("%d",&t);
	while(t--){
		scanf("%d%d%d",&y,&m,&d);
		if((m+d)%2==0){printf("YES\n");continue;}
		if(d==30&&(m==9||m==11)){printf("YES\n");continue;}
		printf("NO\n");
	}
}

【动态规划】

POJ2068 - Nim

还是取石子,不同的是一共2*n个人,1、3、5…n-1是一拨,2、4、6…n是一拨,按1,2,3,4…这个顺序轮流取,每个人都取1~ai个,谁取完最后一个石子谁输,你先取,问能否胜

dp[i][j]:=轮到第i个人取时还剩j个石子的状态(1胜0败)

dp[i][0]=1,记忆化搜索求出dp[0][s]的值,注意:这里可能两拨人都轮一遍石子还没取完,那么我们从第1个人再开始取,需要求余。(最好从0开始,不用特判)

代码如下:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<vector>
#include<set>
#include<list>
using namespace std;
int a[25],dp[25][10000];
int n,s;
int solve(int i,int j){//该第i个人取了,此时剩下了j个石头 
        if(j==0)return dp[i][j]=1;
	if(dp[i][j]!=-1)return dp[i][j];
	dp[i][j]=0; 
	for(int k=1;k<=a[i]&&k<=j;k++){
		if(!solve((i+1)%(2*n),j-k))dp[i][j]=1;
	}
	return dp[i][j];
}

int main(){
	while(scanf("%d",&n)==1&&n){
		scanf("%d",&s);
		memset(dp,-1,sizeof(dp));
		for(int i=0;i<2*n;i++){
			scanf("%d",&a[i]);
		}
		printf("%d\n",solve(0,s));
	} 
}

Nim博弈

有n堆物品,每堆有ai个石子,A和B轮流取走至少1个,A先取,取光者胜。问谁胜。

int x=0;
for(int i=0;i<n;i++)x^=a[i];
if(x!=0)printf("A\n");
else printf("B\n");

关于XOR运算的一些性质:

0^k=k;k^k=0;x^k^x=k;

若x!=0,a>0,b>0,(a-b)^x=0,那么a>x。(a-b=x)

异或性质的应用: POJ2975

代码如下:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<vector>
#include<set>
#include<list>
using namespace std;
int a[1005];
int main(){
	int n;
	while(scanf("%d",&n)!=EOF){
		if(n==0)break;
		int x=0;
		for(int i=1;i<=n;i++){
			scanf("%d",&a[i]);
			x^=a[i];
		}
		if(!x)printf("0\n");
		else{
			int tmp,ans=0;
		    for(int i=1;i<=n;i++){
		    	tmp=x^a[i];
		    	if(a[i]>tmp)ans++;
			} 
			printf("%d\n",ans);
		}
	}
}

SG函数和SG定理

SG函数

将组合游戏抽象为有向图,每个位置为有向图的一个节点,每种可行操作为有向图的一条路径,我们就在有向图的顶点上定义了SG函数。

首先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。对于任意状态 x , 定义 SG(x) = mex(S),其中 S 是 x 后继状态(比如当前石子x=5,规定能取{1,3,4},那么后继状态就是4,2,1)的SG函数值的集合。如 x 有三个后继状态分别为 SG(a),SG(b),SG(c),那么SG(x) = mex{SG(a),SG(b),SG(c)}。 这样 集合S 的终态必然是空集,所以SG函数的终态为 SG(x) = 0,当且仅当 x 为必败点P时。

【例子】取石子问题

有1堆n个的石子,每次只能取{ 1, 3, 4 }个石子,先取完石子者胜利,那么各个数的SG值为多少?

SG[0]=0,f[]={1,3,4},

x=1 时,可以取走1 - f{1}个石子,剩余{0}个,所以 SG[1] = mex{ SG[0] }= mex{0} = 1;

x=2 时,可以取走2 - f{1}个石子,剩余{1}个,所以 SG[2] = mex{ SG[1] }= mex{1} = 0;

x=3 时,可以取走3 - f{1,3}个石子,剩余{2,0}个,所以 SG[3] = mex{SG[2],SG[0]} = mex{0,0} =1;

x=4 时,可以取走4-  f{1,3,4}个石子,剩余{3,1,0}个,所以 SG[4] = mex{SG[3],SG[1],SG[0]} = mex{1,1,0} = 2;

x=5 时,可以取走5 - f{1,3,4}个石子,剩余{4,2,1}个,所以SG[5] = mex{SG[4],SG[2],SG[1]} =mex{2,0,1} = 3;

以此类推.....

SG函数性质

所有终结点 SG 值为 0(因为它的后继集合是空集) 。

SG 为 0 的顶点,它的所有后继 y 都满足 SG 不为 0,该点为P(必败点)。

对于一个 SG 不为 0 的顶点,必定存在一个后继满足SG 为 0 ,后继点为必败,该点为N(必胜点)。

满足组合游戏性质 所有 SG 为 0 定点对应 P 点,SG大于 0 顶点对应 N 点。

SG定理

设G1,G2…Gn是n个有向图游戏,

定义游戏G是G1,G2…Gn的和,也就是说总游戏G可以分成子游戏G1,G2…Gn。

Sprague-Grundy Theorem就是:

G的sg函数是所有子游戏的异或。

sg[G]=sg[G1]^sg[G2]^…^sg[Gn]。

【模板 - 硬币游戏2】

给定k个数字a1…ak,有n堆硬币,每堆有xi枚,A和B轮流取,A先,取出的枚数一定在a1…ak当中,取光者胜,问谁胜。

思路:把n堆硬币看成是n个子游戏,总游戏的sg值(也就是最后的结果)等于子游戏sg值的异或

代码如下:

sg[0]=0;
int max_x=*max_element(X,X+n);
for(int j=1;j<=max_x;j++){//max_x代表堆中最多的石子数,求它的sg值顺带其他堆的都求出来 
	int vis[N]={0};
	for(int i=0;i<K;i++){
		if(a[i]<=j)vis[sg[j-a[i]]]=1;//把后继状态的sg值标记为1 
	}
	for(int i=0;;i++){//找出最小的不属于集合的非负整数 
		if(!vis[i]){
			sg[j]=i;
			break; 
		} 
	} 
} 

【SG应用 - 一维数组】

POJ3537

给你1*n的网格,A和B轮流在网格内画X,谁先连到三个X谁胜,先手胜输出1,反之输出2.

思路:这个游戏一看就是可以分解成子游戏的,怎样分解是我们要考虑的。

当该A画X时,是X_X或是_XX_这样个状态,那么A肯定就胜了,但B又不傻,肯定会尽力阻止这两种情况发生的。

例如当A画了个X,   2  X  3    4 

若B在1、4位置上画X,那么会造成X_X这种情况,在2、3上画,会造成_XX_,那么A肯定胜了

所以B不能再1234上画,反之A也是

(即当对方画了一个X,我们不要在X的1234位置画,不然我们必败,除非无处可画)

那么长度为L的问题就分成了两个长度为posX-3和L-posX-2的问题。

疑惑:道理是明白,当时我就在疑惑怎么求sg[n],难道还像上题那样求出所有子问题的sg值后,再一起异或?答案是否定的啦,因为从不同的位置画X我们得到的后继状态是不同的,每个后继状态又对应两个子问题,一共能得到的不同的子问题灰常之多,这时候就要用到深搜了,每次对应长度n,我们有:在位置1…n画X这么多后继状态,对于每一个后继状态,分成2个子问题,(画X后分成posX-3和n-posX-2)。后继状态的sg值等于子问题sg值的异或,求出后用vis数组标记,再像模板上那样,把每个长度为n的问题的sg值求出,保存到sg数组里。(备忘录方法~)

代码如下:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<vector>
#include<set>
#include<list>
using namespace std;
int sg[2005];

int solve(int n){
	if(n<0)return 0;
	if(sg[n]!=-1)return sg[n];
	bool g[2001]={0};
	for(int i=1;i<=n;i++){
		int t=solve(i-3)^solve(n-i-2);
		g[t]=1;
	}
	for(int i=0;;i++){
		if(g[i]==0)return sg[n]=i;
	}
}

int main(){
	int n;
	memset(sg,-1,sizeof(sg));
    scanf("%d",&n);
	if(solve(n))printf("1\n");
	else printf("2\n");
}

【SG应用 - 二维数组】

POJ2311

这题和上题非常像啦:给一个w*h的格子的长方形纸张,两人轮流剪纸,(沿着格子的分界线,水平或竖直地剪),每次都选择切得的某一张纸再进行切割,首先切出1*1的获胜,问先手必胜还是必败。

思路:

切割纸张时一旦切割出了长或宽为1的纸那么必败,所以我们切割时控制在长宽至少为2,当然2*3,2*2,3*2这些状态都是必败态,再切一刀,长或宽就出现1了。

代码如下:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<vector>
#include<set>
#include<list>
using namespace std;
int sg[205][205];

int solve(int w,int h){
	if(sg[w][h]!=-1)return sg[w][h];
	if(w==2&&(h==2||h==3))return sg[w][h]=0;
	if(h==2&&w==3)return sg[w][h]=0;
	int vis[205]={0};
	for(int i=2;w-i>=2;i++){ 
		int t=solve(i,h)^solve(w-i,h);
		vis[t]=1;
	}
	for(int i=2;h-i>=2;i++){
		int t=solve(w,i)^solve(w,h-i);
		vis[t]=1;
	}
	for(int i=0;;i++){
		if(!vis[i])return sg[w][h]=i;
	}
}

int main(){
	int w,h;
	memset(sg,-1,sizeof(sg));
	while(scanf("%d%d",&w,&h)!=EOF){
		int res=solve(w,h);
		if(res)printf("WIN\n");
		else printf("LOSE\n"); 
	}
}

大概这就是sg的套路吧~

按照“不败”的思想把原问题分成若干后继状态,一个后继状态分成2个子问题,用dfs求子问题的sg值,异或得到后继状态的sg值,标记sg值,然后求出最小的不属于S的非负整数。(S是后继状态sg值的集合)。

学习链接~

SG函数定理

ACM博弈模板

各种博弈 

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值