初学博弈论受益匪浅,将个人的博弈论解题总结一下。
1.威佐夫博弈论(证明真的没有的离散数学技巧是绝对这几个表情可以较好描述你的表情发展过程)
1)问题:首先有两堆石子,博弈双方每次可以取一堆石子中的任意个,不能不取,或者取两堆石子中的相同个。先取完者赢。
解题思路:说实话对这种题唯一的方法是打表,当然比赛时候打表不好搞,尽量将新的状态转到老的已确定的状态,然后再找规律。
(0,0)(1,2)(3,5)(4,7)(6,10)……
规律的确是挺神奇的ak =[k(1+√5)/2],bk= ak + k (k=0,1,2,...n 方括号表示取整函数),这个状态先手必败。
2)关于这个博弈论的定理与规律。
每行第一个数恰巧是前面每一行中没有出现过的最小正整数
在所有的必败态中,每个数字恰巧出现一次。。。。。。。。2
解题时会问你先手胜的话如何走第一步,根据.。。。。2可以去求,当然当(4,6)时这种方法不行,只能两边同时去取。(根据差值去取)。
当然证明我也可以阐述一下,毕竟学ACM的,如果这个都不掌握的话,太对不起威佐夫这个人了,他貌似毕生贡献都在这一个博弈上面了,如果我只用找规律解决的话,有愧于先人啊。(在本文最后说明,毕竟本文最重要的还是要解决ACM中的博弈类题目)。
2.Nim游戏
这类题目ACM中还是挺多的。
先来描述一下最基础的NIM游戏吧。
1)基础Nim
有若干堆石子,每堆石子的数量都是有限的,合法的移动是“选择一堆石子并拿走若干颗(不能不拿)”,如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动)。
结论:每个数量异或之后如果为0,则先手必输。
2)nim引申
将上面的每堆可取石子数限定为一个集合S;其他同上;
SG函数:
sg(x) = mex ( sg(y) |y是x的后继结点 )
其中mex(x)(x是一个自然是集合)函数是x关于自然数集合的补集中的最小值,比如x={0,1,2,4,6} 则mex(x)=3;
什么是后继结点?
所谓后继结点就是当前结点经过一个操作可以变成的状态。比如对于娶4石子游戏,假如每次可以取的数目是1,2,4,当前的石子数目也就是当前状态是5,那么5的后继结点就是{5-1, 5-2, 5-4}={4,3,1};
思考:hdu1848
每个堆点的可取数目是有限定的,这该怎么办呢?
sg{y}=mex{sg{x}|x是y的子节点}
那么sg{y}=sg{y-t}(t为斐波那契数列中的数)。
5 #include<iostream> 6 using namespace std; 7 8 #define MAX 1005 9 /* 10 计算从1-n范围内的SG值。 11 Array(存储可以走的步数,Array[0]表示可以有多少种走法) 12 Array[]需要从小到大排序 13 /*HDU1847博弈SG函数 14 1.可选步数为1-m的连续整数,直接取模即可,SG(x) = x % (m+1); 15 2.可选步数为任意步,SG(x) = x; 16 3.可选步数为一系列不连续的数,用GetSG(计算) 17 */ 18 int SG[MAX], hash[MAX]; 19 int Array[MAX]; 20 void GetSG(int Array[], int n = MAX-1) 21 { 22 int i, j; 23 memset(SG, 0, sizeof(SG)); 24 for(i = 0; i <= n; i++) 25 { 26 memset(hash, 0, sizeof(hash)); 27 for(j = 1; j <= Array[0]; j++) 28 { 29 if(i < Array[j]) 30 break; 31 hash[SG[i - Array[j]]] = 1; 32 } 33 for(j = 0; j <= n; j++) 34 { 35 if(hash[j] == 0) 36 { 37 SG[i] = j; 38 break; 39 } 40 } 41 } 42 } 43 int main() 44 { 45 int n, i, m, p, ans; 46 Array[1] = 1; 47 for(i = 2; ; i++) 48 { 49 Array[i] = Array[i-1] + Array[i-2]; 50 if(Array[i] > MAX) 51 break; 52 } 53 Array[0] = i - 1; 54 GetSG(Array); 55 while(cin>>m>>n>>p && m) 56 { 57 ans = SG[m]^SG[n]^SG[p]; 58 if(ans == 0) cout<<"Nacci"<<endl; 59 else cout<<"Fibo"<<endl; 60 } 61 return 0; 62 }取走-分割游戏
这种游戏允许取走某些东西,然后将原来的一个游戏分成若干个相同的游戏。
例1、Lasker’s Nim游戏:每一轮允许两种操作之一。(1)从一堆石子中取走任意多个(2)将一堆数量不少于2的石子分成都不为空的两堆。
分析:
很明显,g(0)=0,g(1)=1。状态2的后继有0,1和(1,1),它们的SG函数值分别是0,1和0,所以g(2)=2。状态3的后继有0,1,2和(1,2),它们的SG函数值分别是0,1,2和3,所以g(3)=4。状态4的后继有0,1,2,3,(1,3)和(2,2),它们的SG函数值分别是0,1,2,4,5和0,所以g(4)=3。在推一些,我们得到:
我们推测:对于所有的k>=0,有g(4k+1)=4k+1;g(4k+2)=4k+2;g(4k+3)=4k+4;g(4k+4)=4k+3。
请自行证明。
假设游戏初始时有3堆,分别有2、5和7颗石子。三堆的SG函数值分别是2、5和8,它们的Nim和等于15。所以要走到P状态,就要使得第三堆的SG值变成7,可以将第三堆分成按1和6分成两堆。
3.对称博弈
男人八题中的取石子问题。
走格子问题
4.K倍增长博弈
5.翻硬币
6.其他杂题
看做题经验了这些题目。
7.24点很多时候题目会要求你求24点,这其实也可以算是博弈论中的一种,只不过是对手只有自己一个人,这往往需要数学灵感。这类题目一般要枚举各种情况并且归纳几种相类似的情况。例如hdu 1427:
这题目需要dfs就够了。
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<queue>
#include<algorithm>
#include<map>
#include<iomanip>
#define INF 99999999
using namespace std;
const int MAX=5;
char s[3];
int number[MAX];
bool flag;
void check(char ch,int &num){
if(ch == 'A')num=1;
else if(ch == 'J')num=11;
else if(ch == 'Q')num=12;
else if(ch == 'K')num=13;
else for(int i=2;i<10;++i){
if(ch == i+'0'){num=i;break;}
}
if(ch == '1')num=10;
}
int f(int a,int op,int c){
if(op == 0)return a+c;
if(op == 1)return a-c;
if(op == 2)return a*c;
if(c && a%c == 0)return a/c;
return INF;
}
bool calculate(int i,int j,int k){
int temp1,temp2;
temp1=f(number[0],i,number[1]);
if(temp1 != INF)temp2=f(number[2],k,number[3]);
if(temp2 == INF)temp1=INF;
if(temp1 != INF)temp1=f(temp1,j,temp2);
if(temp1 == 24 || temp1 == -24)return true;
temp1=f(number[0],i,number[1]);
if(temp1 != INF)temp1=f(temp1,j,number[2]);
if(temp1 != INF)temp1=f(temp1,k,number[3]);
if(temp1 == 24 || temp1 == -24)return true;
return false;
}
void Perm(int k){
if(k == 3){
for(int i=0;i<4 && !flag;++i)for(int j=0;j<4 && !flag;++j)for(int k=0;k<4 && !flag;++k){
flag=calculate(i,j,k);
}
return;
}
Perm(k+1);
for(int i=k+1;i<4;++i){
if(flag)return;
if(number[i] == number[k])continue;
swap(number[k],number[i]);
Perm(k+1);
swap(number[k],number[i]);
}
}
int main(){
while(~scanf("%s",s)){
check(s[0],number[0]);
for(int i=1;i<=3;++i){
scanf("%s",s);
check(s[0],number[i]);
}
flag=false;
Perm(0);
if(flag)printf("Yes\n");
else printf("No\n");
}
return 0;
}
这是比较简单的24点,例如15年多校hdu5308,这个是需要将前面情况进行枚举以及归纳的当n大于等于15的时候就是可以分奇偶两种情况来讨论归纳。这题如果按照前面搜索来做的话不仅不好记录运算过程,还铁定超时间。
例如还有cf468A题和hdu5308做法有些类似。但比上面一题好写很多。
#include <cstdio>
#include <algorithm>
using namespace std;
int main()
{
int n;
scanf("%d",&n);
if (n<=3)
puts("NO");
else
{
puts("YES");
if (n%2==0)
{
for (int i=6;i<=n;i+=2)
printf("%d - %d = 1\n",i,i-1);
puts("4 * 3 = 12\n12 * 2 = 24\n24 * 1 = 24");
for (int i=1;i<=n/2-2;i++)
puts("24 * 1 = 24");
}
else
{
for (int i=7;i<=n;i+=2)
printf("%d - %d = 1\n",i,i-1);
puts("4 * 3 = 12\n5 + 1 = 6\n6 * 2 = 12\n12 + 12 = 24");
for (int i=1;i<=(n-5)/2;i++)
puts("24 * 1 = 24");
}
}
return 0;
}
附录:
1的证明:http://blog.sina.com.cn/s/blog_a661ecd501017out.html