博弈算法总结

这几天开始学习博弈,发现这一块是个难啃的骨头。以下是我从网上收集的资料汇总:

       我国民间有个古老的游戏:就是有物品若干堆,(物品可以是火柴,围棋都可以)。两个人轮流从堆中取若干件,规定取光物体者为胜。这个就是我们今天要研究的组合游戏。

组合游戏定义:

       1、有且仅有两个玩家    2、游戏双方轮流操作    3、游戏操作状态是个有限的集合(比如:取石子游戏,石子是有限的,棋盘中的棋盘大小的有限的)  4、游戏必须在有限次内结束  5、当一方无法操作时,游戏结束。

现在我们来研究如何取胜:

(一)巴什博奕(Bash Game):有一堆n个物品,两人轮流从堆中取物品,每次取 x 个 ( 1 ≤ x ≤ m)。最后取光者为胜。

         如果 n = m + 1, 一次至多取 m 个,所以无论先取者,取了多少个,一定还剩余 x 个( 1 ≤ x ≤ m)。所以,后取者必胜。因此我们发现了取胜的秘诀:如果我们把 n 表示为

n = (m + 1)  * r + s 。(0 ≤ s  < m , r ≥ 0)。先取者 拿走 s 个, 后取者 拿走 k 个  (1 ≤ k ≤ m),那么先取者 再 拿走 m + 1 - k 个。结果还剩下 ( m + 1 ) * ( r - 1 ) 个。我们只要始终给对手留下 m + 1 的倍数,那么 先取者 肯定必胜。 现在 我们可以知道,如果 s = 0,那么后取者必胜。 否则 先取者 必胜。

看完这个可以用这个练练手:hdu 1846 Brave Game

 

(二)威佐夫博奕(Wythoff Game):有两堆各若干个物品,两个人轮流从某一堆或同时从两堆中取同样多的物品,规定每次至少取一个,多者不限,最后取光者得胜。

        这种情况下是颇为复杂的。我们用(a[k],b[k])(a[k] ≤ b[k] ,k=0,1,2,...,n)( a[k] 其中 k 为下标 )表示两堆物品的数量并称其为局势,如果甲面对(0,0),那么甲已经输了,这种局势我们称为奇异局势。前几个奇异局势是:(0,0)、(1,2)、(3,5)、(4,7)、(6,10)、(8,13)、(9,15)、(11,18)、(12,20)。

       可以看出,a[0] = b[0] = 0,a[k]是未在前面出现过的最小自然数,而 b[k] = a[k] + k。

奇异局势的性质:   

1、任何自然数都包含在一个且仅有一个奇异局势中。

          由于ak是未在前面出现过的最小自然数,所以有a[k] > a[k-1] ,而 b[k] = a[k] + k > a[k-1] + k > a[k-1] + k - 1 = b[k-1] > a[k-1] 。所以性质1成立。

2。任意操作都可将奇异局势变为非奇异局势。

          事实上,若只改变奇异局势(a[k],b[k])的某一个分量,那么另一个分量不可能在其他奇异局势中,所以必然是非奇异局势。如果使(a[k],b[k])的两个分量同时减少,则由于其差不变,且不可能是其他奇异局势的差,因此也是非奇异局势。

3。采用适当的方法,可以将非奇异局势变为奇异局势。

         假设面对的局势是(a , b),若 b = a,则同时从两堆中取走 a 个物体,就变为了奇异局势(0,0);如果 a = a[k] ,b > b[k] ,那么,取走b - b[k]个物体,即变为奇异局势;如果 a = a[k] , b < b[k] 则同时从两堆中拿走a - a[b-a] 个物体(如果不懂为什么减去a - a[b-a],详见文章最后的注释1)变为奇异局势( a[b-a], a[b-a] + b - a);如果a > a[k] ,b= a[k] + k 则从第一堆中拿走多余的数量a - a[k] 即可;如果a < a[k] ,b= a[k] + k,分两种情况,第一种,a=a[j] (j < k)从第二堆里面拿走 b - b[j] 即可;第二种,a=b[j] (j < k)从第二堆里面拿走 b - a[j] 即可。

          由上述性质可知,如果双方都采取正确操作,那么面对非奇异局势,先取者必胜。

          那么我们要如何判断一个局势是否为奇异局势?公式如下:

          a[k] = [k(1+√5)/2](a[k]这个方括号为下标运算符,[k(1+√5)/2]这个方括号为取整运算符),b[k] = a[k] + k 。奇妙的是其中出现了黄金分割数(1+√5)/2 = 1.618...因此,由a[k],b[k]组成的矩形近似为黄金矩形,由于2/(1+√5)=(√5-1)/2,可以先求出 j = [a(√5-1)/2],若 a = [ j(1+√5)/2],那么a = a[j],b[j] = a[j] + j,若不等于,那么a = a[j]+1,b = a[j] + j + 1,若都不是,那么就不是奇异局势。然后再按照上述法则进行,一定会遇到奇异局势。

看完这个可以做一做poj 1067 取石子游戏,练练手

#include <stdio.h>  
#include <stdlib.h>  
#include <math.h>  
  
int main(){  
    int a,b,k,a_k;  
    while(scanf("%d%d",&a,&b)!=EOF){  
         k = abs(a-b);  
         a = a < b? a : b;  
         a_k = floor(k*(1.0 + sqrt(5.0))/2);  
         printf("%d\n",a!=a_k);  
         //输出为0,说明该点为必败点,1为必胜点  
    }  
    return 0;  
}  

【文字描述上面代码】

          k = | a - b |,  a[k] =  [ k(1+√5)/2], 如果 a,b中的最小值, min{a,b} 与 a[k]相等,那么 他就是奇异局势。

 

(三)尼姆博奕(Nimm Game):有三堆各若干个物品,两个人轮流从某一堆取任意多的物品,规定每次至少取一个,多者不限,最后取光者得胜。

           这种情况最有意思,它与二进制有密切关系,我们用(a,b,c)表示某种局势,首先(0,0,0)显然是奇异局势,无论谁面对奇异局势,都必然失败。第二种奇异局势是(0,n,n),只要与对手拿走一样多的物品,最后都将导致(0,0,0)。仔细分析一下,(1,2,3)也是奇异局势,无论对手如何拿,接下来都可以变为(0,n,n)的情形。

计算机算法里面有一种叫做按位模2加,也叫做异或的运算,我们用符号(+)表示这种运算,先看(1,2,3)的按位模2加的结果:

1 =二进制01

2 =二进制10

3 =二进制11 (+)

———————

0 =二进制00 (注意不进位)

对于奇异局势(0,n,n)也一样,结果也是0。

任何奇异局势(a,b,c)都有a(+)b(+)c =0。

注意到异或运算的交换律和结合律,及a(+)a=0,:

a(+)b(+)(a(+)b)=(a(+)a)(+)(b(+)b)=0(+)0=0。

所以从一个非奇异局势向一个奇异局势转换的方式可以是:

1)使 a = c(+)b

2)使 b = a(+)c

3)使 c = a(+)b

 

那么对于(57 8 9 10)呢?

1.从5中取不了,因为7^8^9^10=12

2.从7中取不了,因为5^8^9^10=14

3.可以从8中取7个(5,7,1,9,10)

4.可以从9中去除9个(5,7, 8, 0,10)

5.可以从10中去除7个(5,7,8,9,3)

 

如何计算取法?

方法一:二重循环

for(i=0;i<n;i++)
{
	int m=0;
	for(j=0;j<n;j++)
	{
		if(j==i)
		   continue;
		m^a[j];
	}
	if(a[i]>m)
	num=a[i]-m; 
}

方法2:再异或(当对其中所有元素已经异或一次得到值m后,再对其中某一个值a[i]进行异或,得到的值是去除a[i]后剩下所有元素的异或值m1)

for(i=0;i<n;i++)//假设已经求出了所有值的异或结果 
{
	int k=m^a[i];
	if(k<a[i])
	{
		printf("%d",a[i]-k);//a[i]-k代表要拿走的值 
	}
}

 

同样给一个练手题:HDU 2176

 

(四)SG函数和SG定理。

       Sprague-Grundy定理(SG定理):

        游戏和的SG函数等于各个游戏SG函数的Nim和。这样就可以将每一个子游戏分而治之,从而简化了问题。而Bouton定理就是Sprague-Grundy定理在Nim游戏中的直接应用,因为单堆的Nim游戏 SG函数满足 SG(x) = x。

 

       SG函数:

        首先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。

        对于任意状态 x , 定义 SG(x) = mex(S),其中 S 是 x 后继状态的SG函数值的集合。如 x 有三个后继状态分别为 SG(a),SG(b),SG(c),那么SG(x) = mex{SG(a),SG(b),SG(c)}。 这样 集合S 的终态必然是空集,所以SG函数的终态为 SG(x) = 0,当且仅当 x 为必败点P时。

       SG函数适应题型:一堆石子,可取的石子数不连续;

【实例】取石子问题

有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;

以此类推.....

   x        0  1  2  3  4  5  6  7  8....

SG[x]    0  1  0  1  2  3  2  0  1....

由上述实例我们就可以得到SG函数值求解步骤,那么计算1~n的SG函数值步骤如下:

1、使用 数组f 将 可改变当前状态 的方式记录下来。

2、然后我们使用 另一个数组 将当前状态x 的后继状态标记。

3、最后模拟mex运算,也就是我们在标记值中 搜索 未被标记值 的最小值,将其赋值给SG(x)。

4、我们不断的重复 2 - 3 的步骤,就完成了 计算1~n 的函数值。

代码实现如下:

    

//SG[]:0~n的SG函数值
//S[]:为x后继状态的集合
int f[N]={1,3,4},SG[MAXN],S[MAXN];
void  getSG(int n){
    int i,j;
    memset(SG,0,sizeof(SG));
    //因为SG[0]始终等于0,所以i从1开始
    for(i = 1; i <= n; i++){
        //每一次都要将上一状态 的 后继集合 重置
        memset(S,0,sizeof(S));
        for(j = 0; f[j] <= i && j <= N; j++)
            S[SG[i-f[j]]] = 1;  //将后继状态的SG函数值进行标记
        for(j = 0;; j++) if(!S[j]){   //查询当前后继状态SG值中最小的非零值
            SG[i] = j;
            break;
        }
    }
}

现在我们来一个实战演练(题目链接):

       只要按照上面的思路,解决这个就是分分钟的问题。

代码如下:

 

#include <stdio.h>
#include <string.h>
#define MAXN 1000 + 10
#define N 20
int f[N],SG[MAXN],S[MAXN];
void getSG(int n){
    int i,j;
    memset(SG,0,sizeof(SG));
    for(i = 1; i <= n; i++){
        memset(S,0,sizeof(S));
        for(j = 0; f[j] <= i && j <= N; j++)
            S[SG[i-f[j]]] = 1;
        for(j = 0;;j++) if(!S[j]){
            SG[i] = j;
            break;
        }
    }
}
int main(){
    int n,m,k;
    f[0] = f[1] = 1;
    for(int i = 2; i <= 16; i++)
        f[i] = f[i-1] + f[i-2];
    getSG(1000);
    while(scanf("%d%d%d",&m,&n,&k),m||n||k){
        if(SG[n]^SG[m]^SG[k]) printf("Fibo\n");
        else printf("Nacci\n");
    }
    return 0;
}

 

大家是不是还没有过瘾,那我就在给大家附上一些组合博弈的题目:

POJ 2234 Matches Game
HOJ 4388 Stone Game II

POJ 2975 Nim
HOJ 1367 A Stone Game
POJ 2505 A multiplication game
ZJU 3057 beans game
POJ 1067 取石子游戏
POJ 2484 A Funny Game
POJ 2425 A Chess Game
POJ 2960 S-Nim
POJ 1704 Georgia and Bob
POJ 1740 A New Stone Game
POJ 2068 Nim
POJ 3480 John
POJ 2348 Euclid's Game
HOJ 2645 WNim
POJ 3710 Christmas Game 
POJ 3533 Light Switching Game

下面是杭电OJ部分博弈题解

HDU 1517 A Multiplication Game

#include<iostream>
using namespace std;
int main()
{
	double n;
	while(cin>>n)
	{
		while(n>18)
			n/=18;
		if(n<=9)
			cout<<"Stan wins."<<endl;
		else
			cout<<"Ollie wins."<<endl;
	}
	return 0;
}

①、如果输入是2~9,因为Stan是先手,所以Stan必胜。

②、如果输入是10~18(9*2),因为Ollie是后手,不管第一次Stan乘的是多少,Stan肯定在2~9之间,如果Stan乘以2,那么Ollie就乘以9,那么Ollie乘以大于1的数都能超过10~18中的任何一个数,Ollie必胜。

③、如果输入的是19~162(9*2*9),那么这个范围Stan必胜。

④、如果输入是163~324(2*9*2*9),这个是Ollie的必胜范围。

…………

可以发现必胜态是对称的。

如果“我方”首先给出了一个在N不断除18后的得到不足18的数M,“我方”就可以胜利,然而双方都很聪明,所以这样胜负就决定与N了,如果N不断除18后的得到不足18的数M,如果1<M<=9则先手胜利,即Stan wins.如果9<M<=18则后手胜利。

 

HDU 1846 Brave game

巴什博弈n%(m+1)==0先手必败。 

#include <iostream> 
#include<cstdio> 
#include<cstring> 
#include<cstdlib> 
#include<cmath> 
using namespace std; 
int main() 
{ 
 int n,a,b; 
 scanf("%d",&n); 
 while(n--) 
 { 
 scanf("%d%d",&a,&b); 
 if(a%(b+1)==0) 
 printf("second\n"); 
 else printf("first\n"); 
 } 
return 0; 
}



hdu1847 good luck in cet--4 everybody

只要留下两类都是2的指数幂就是必输状态然后找规律发现这两类的和为3的倍数。即

有下面的结论。 若总数为 3 的倍数,后手保证所拿数量与先手之和为3的倍数则必赢!!

若总数非3的倍数,先手拿除于3的余数,以后先手所拿数量与后手所拿之和为 3 的倍数,则先手必赢!!

#include <iostream> 
#include<cstdio> 
#include<cmath> 
using namespace std; 
int main() 
{ 
 int n; 
 while(scanf("%d",&n)!=EOF) 
 { 
 if(n%3==0) 
 printf("Cici\n"); 
 else printf("Kiki\n"); 
 } 
 return 0; 
}


HDU 1848 Fibonacci again and again

#include <iostream> 
#include<cstdio>
 #include<cmath> 
#include<cstring> 
using namespace std; 
#define N 1005 
int f[N]; 
int sg[N]; 
void fun() 
{ 
 int i; 
 f[0]=1; 
 f[1]=1; 
 f[2]=2; 
 for(i=3;;i++) 
 { 
 f[i]=f[i-1]+f[i-2]; 
 if(f[i]>1000) 
 break; 
 } 
} 
int dfs(int v) 
{ 
 int i; 
 if(sg[v]!=-1) 
 return sg[v]; 
 bool visit[N]={0}; 
 for(i=1;i<16;i++) 
 { 
 if(v>=f[i]) 
 { 
 int temp=dfs(v-f[i]); 
 visit[temp]=1; 
 } 
 } 
 for(i=0;visit[i];i++); 
 return sg[v]=i; 
} 
int main() 
{ 
 fun(); 
 int m,n,p; 
 while(scanf("%d%d%d",&m,&n,&p),m||n||p) 
 { 
 memset(sg,-1,sizeof(sg)); 
 int ans;  ans=dfs(m)^dfs(n)^dfs(p); 
 if(ans) 
 printf("Fibo\n"); 
 else printf("Nacci\n"); 
 } 
 return 0; 
}

HDU1849 Rabbit and Grass
裸的NIM博弈直接异或即可。 

#include <iostream> 
#include<cstdio> 
using namespace std; 
int main() 
{ 
 int n,p; 
 while(scanf("%d",&n)!=EOF) 
 { 
 if(n==0)break; 
 int ans=0; 
 for(int i=0;i<n;i++) 
 { 
 scanf("%d",&p); 
 ans^=p; 
 } 
 if(ans==0) 
 printf("Grass Win!\n"); 
 else printf("Rabbit Win!\n"); 
 } 
 return 0; 
}


HDU 1850 Being a Good Boy in Spring Festival
简单NIM博弈如果异或不为0找出异或可以使其成为0的值 if(num[j]>(ans^num[j]))这
是关键。 

#include <iostream> 
#include<cstdio> 
#include<cmath> 
#include<cstring> 
using namespace std; 
int num[105]; 
int main() 
{ 
 int t; 
 while(scanf("%d",&t)&&t) 
 {  int ans=0; 
 for(int i=0;i<t;i++) 
 { 
 scanf("%d",&num[i]); 
 ans^=num[i]; 
 } 
 int cnt=0; 
 for(int j=0;j<t;j++) 
 { 
 if(num[j]>(ans^num[j])) 
 { 
 cnt++; 
 } 
} 
 printf("%d\n",cnt); 
 } 
return 0; 
}


HDU 1851 A Simple Game
简单NIM博弈 

#include <iostream> 
#include<cstdio> 
#include<cmath> 
#include<cstdlib> 
#include<cstring> 
using namespace std; 
int main() 
{ 
 int t,n,m,l;
 scanf("%d",&t); 
 while(t--) 
 { 
 scanf("%d",&n); 
 int ans=0; 
 while(n--) 
 { 
 scanf("%d%d",&m,&l); 
 ans^=(m%(l+1)); 
 } 
 if(ans==0) 
 printf("Yes\n"); 
 else printf("No\n"); 
 } 
 return 0;
 }


HDU1907 John
 NIM博弈全是1的时候特判数1的个数奇数输偶数赢了。 

#include <iostream> 
#include<cstdio> 
#include<cmath> 
#include<cstring> 
#include<cstdlib> 
using namespace std; 
int main() 
{ 
	int t,num; 
	scanf("%d",&t); 
	while(t--) 
	{ 
		int ans=0,flag=0; 
		int p[50]; 
		scanf("%d",&num); 
		for(int i=0; i<num; i++) 
		{ 
			scanf("%d",&p[i]); 
			if(p[i]!=1) 
				flag=1; 
			ans^=p[i]; 
		} 
		if(flag) 
		{ 
			if(ans==0) 
				printf("Brother\n"); 
			else printf("John\n"); 
		}else 
		{ 
			if(num%2!=0) 
				printf("Brother\n"); 
			else printf("John\n"); 
		} 
	} 
	return 0; 
}

 

HDU 2147 kiki's game

判断P和N的状态画出图标即可。利用SG函数的基本性质。P后面的都是NN后面的

一定存在一个P状态。 观察图表规律知道n和m都是奇数时候必败。 

#include <iostream> 
#include<cstdio> 
#include<cstring> 
#include<cmath> 
#include<cstring> 
using namespace std; 
int main() 
{ 
	int n,m; 
	while(scanf("%d%d",&n,&m)!=EOF) 
	{ 
		if(n==0&&m==0)break; 
		if(n&1&&m&1) 
			printf("What a pity!\n"); 
		else printf("Wonderful!\n"); 
	} 
	return 0; 
}


HDU2149 Public Sale

简单的巴什博弈。 

#include <iostream> 
#include<cstdio> 
#include<cmath> 
#include<cstdlib> 
#include<cstring> 
using namespace std; 
int main() 
{ 
	int m,n; 
	while(scanf("%d%d",&m,&n)!=EOF) 
	{ 
		if(m%(n+1)==0) 
		{ 
			printf("none\n"); 
			continue; 
		} 
		else if(n>m) 
		{ 
			for(int i=m;i<n;i++) 
				printf("%d ",i); 
			printf("%d\n",n); 
		} 
		else printf("%d\n",m%(n+1));
		//这里是开始的时候如果先出m%(n+1)个那么剩下的就是
		//必败态了。  
	} 
	return 0; 
}



 

 

转载于:https://my.oschina.net/lin546/blog/1538540

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值