博弈论入门小结
Posted Hzoi_joker
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了博弈论入门小结相关的知识,希望对你有一定的参考价值。
感受到了被博弈论支配的恐惧……
入门的话个人按顺序推荐几篇论文和ppt:
《由感性认识到理性认识——透析一类搏弈游戏的解答过程》张一飞
《游戏策略》朱全民
《解析一类组合游戏》 王晓珂
《组合游戏概述—浅谈SG游戏的若干拓展及变形》 贾志豪
看完这四篇还是要有点时间的,然而博主很傻的倒着看完了,然后就成功地完成了入门到放弃,事倍功半……
到现在为止博弈论做了7道题,感觉只是大致学了一点皮毛,以后肯定回去再做一些题目,然而Hz书店没有和博弈论有关的书,差评。
只能说博弈论是一个很奇妙的东西,并没有什么板子,可以和多种知识点结合,所谓的套路也并不是那么靠谱,从某种意义上讲就是“比智商”。而且要学会发散思维,有时如果死扣一个小局面很有可能彻底困死,在这里把几道做过的认为比较有趣的题目及题解放出来分享一下。
Bzoj 1188 分裂游戏
一道论文题。在论文里有详细解释。(膜拜一发呵呵酵母菌,上来自己推出来的%%%)
对于这种只关注输赢而且先不能走的输的游戏我们一般都是按照“对称”思想。
在这道题里我们同样利用对称思想。如果先手者一个石子数为偶数的堆里的石子进行操作,后手者只要模仿一下可以发现局面实质还是不会变的。因此我们可以把所有石子堆按照个数奇偶简化问题,使所有堆的石子个数都不大于1。然后我们开始happy的推sg值。既然我们每次操作只能把石子放在右侧的堆里,每一个石子的后继局面就是从后面石子的sg值里面取出两个异或一下就可以代表后继局面了。然后,只要把所有有石子的堆的sg值异或一下结果就出来了(为了方便,我们在一开始倒序预处理)
1 #include <iostream> 2 #include <cstdio> 3 #include <cstdlib> 4 #include <cmath> 5 #include <cstring> 6 #define N 30 7 using namespace std; 8 int T,n,sg[N],a[N]; 9 bool mark[N*N]; 10 void init() 11 { 12 sg[0]=0; 13 for(int i=1;i<=N-4;i++) 14 { 15 for(int j=i-1;j>=0;j--) 16 mark[sg[i-1]^sg[j]]=1; 17 for(int j=0;j<=N*N-2;j++) 18 { 19 if(!mark[j]) 20 { 21 sg[i]=j; 22 break; 23 } 24 } 25 } 26 } 27 int main() 28 { 29 init(); 30 scanf("%d",&T); 31 while(T--) 32 { 33 scanf("%d",&n); 34 for(int i=1;i<=n;i++) scanf("%d",&a[i]); 35 int ans=0; 36 for(int i=1;i<=n;i++) 37 { 38 if(a[i]&1) 39 { 40 ans^=sg[n-i]; 41 } 42 } 43 int js=0; 44 bool bj=0; 45 for(int i=1;i<=n;i++) 46 { 47 if(!a[i])continue; 48 for(int j=i+1;j<=n;j++) 49 { 50 for(int k=j;k<=n;k++) 51 { 52 if((ans^sg[n-i]^sg[n-j]^sg[n-k])==0) 53 { 54 js++; 55 if(!bj) 56 { 57 printf("%d %d %d\\n",i-1,j-1,k-1); 58 bj=1; 59 } 60 } 61 } 62 } 63 } 64 if(!bj) printf("-1 -1 -1\\n"); 65 printf("%d\\n",js); 66 } 67 return 0; 68 }
Bzoj 2017 [Usaco2009 Nov] 硬币游戏
一道博弈DP题……
这道题基本一看就可以看出大致的转移方程。然而博主一开始多加了一个现在是谁先手然后就由于迷之转移和迷之最终答案扑街了……
我们可以倒着dp这道题。设f[i][j]为当前剩i个硬币,上一次取j个,接着往下取最多能得多少钱,则f[i][j]=max(f[i-k][k]+sum[i][k])(k<=j*2)这看似是n^3的dp我们可以通过借助之前的f[i][j-1]优化转移到n^2,在此就不多赘述了。
1 #include <iostream> 2 #include <cstdio> 3 #include <cstdlib> 4 #include <cmath> 5 #include <cstring> 6 #include <algorithm> 7 #define N 2005 8 using namespace std; 9 int n,a[N]; 10 int f[N][N]; 11 int sum[N]; 12 int main() 13 { 14 scanf("%d",&n); 15 for(int i=n;i>=1;i--) scanf("%d",&a[i]); 16 for(int i=1;i<=n;i++) sum[i]=sum[i-1]+a[i]; 17 for(int i=1;i<=n;i++) 18 { 19 for(int j=1;j<=n;j++) 20 { 21 f[i][j]=f[i][j-1]; 22 if(i>=2*j) f[i][j]=max(f[i][j],sum[i]-f[i-j*2][j*2]); 23 if(i>=2*j-1) f[i][j]=max(f[i][j],sum[i]-f[i-(j*2-1)][j*2-1]); 24 } 25 } 26 printf("%d",f[n][1]); 27 return 0; 28 }
之前写过专门的题解……
Bzoj 1982 Moving Pebbles
一道让人崩溃的题。友情提示:本道题最高级的算法是sort,如果你不想像本蒟蒻一样崩溃那最好再想一想……
先解释一下题意:这里有n堆石子,每次你选择一个还有石子的石子堆先拿走一个,然后拿出来任意个,再从任意个里面取出任意个,放到任意还有石子的堆中,其余扔掉。
仍然是按照惯例,“对称”思想。我们思考一下什么时候我们能够被不断“模仿”,最基础的就是按照石子数相同所有堆都可以两两配成一对。然后完全模仿对方的操作。那么除了这种方法还有别的必败状态吗?
让我们分类讨论一下。
首先,是单数个石子堆。我们将石子排序,第一小和第二小,第三小和第四小……最后晾出最大的石子堆。对最大的石子堆进行操作,显然肯定能够将其他石子都补成两两一致。使对方面临必败状态。
当石子堆数为双数且不满足上面所说状态时。我们将最大和最小,第二小和第三小,第四小和第五小……进行配对。显然仍然是可以通过对第一大的堆进行操作,仍然可以补成两两相同。
所以我们只要sort一下就好了……
1 #include <iostream> 2 #include <cstdio> 3 #include <cstdlib> 4 #include <cmath> 5 #include <cstring> 6 #include <algorithm> 7 #define N 100005 8 using namespace std; 9 int n,a[N]; 10 int main() 11 { 12 scanf("%d",&n); 13 for(int i=1;i<=n;i++) scanf("%d",&a[i]); 14 if(n&1) 15 { 16 printf("first player\\n"); 17 } 18 else 19 { 20 bool flag=1; 21 sort(a+1,a+n+1); 22 for(int i=1;i<=n;i+=2) 23 { 24 if(a[i]!=a[i+1]) 25 { 26 flag=0; 27 break; 28 } 29 } 30 if(!flag) printf("first player\\n"); 31 else printf("second player\\n"); 32 } 33 return 0; 34 }
Bzoj 2437 兔兔与蛋蛋
这道题是一道二分图博弈,然而爆搜能拿75分……考场上要是想不到二分图博弈爆搜还是很不错滴……
我们可以先证明出来我们白格永远不会回到之前到过的格子,因为在四联通的格子里环永远是偶数个格子。那么如果我们将每一个白格和他四周的黑格进行连边,就会发现他是一个经典的二分图博弈。我们只要每次判断一下就好了。如果你不知道二分图博弈的话推荐一发这个博客:BZOJ 1443 游戏(二分图博弈) 讲的还是挺不错的。
1 #include <iostream> 2 #include <cstdio> 3 #include <cstdlib> 4 #include <cstring> 5 #include <queue> 6 #include <iostream> 7 #define N 50 8 using namespace std; 9 int p,n,zz2,m,bh[N][N],ma[N][N]; 10 char bb[N]; 11 int zz,jg[1005],a[N*N]; 12 struct ro{ 13 int to,next; 14 }road[N*N*N]; 15 void build(int x,int y) 16 { 17 zz++; 18 road[zz].to=y; 19 road[zz].next=a[x]; 20 a[x]=zz; 21 } 22 int zy[10][2]; 23 void bui() 24 { 25 memset(a,0,sizeof(a)); 26 zz=0; 27 for(int i=1;i<=n;i++) 28 { 29 for(int j=1;j<=m;j++) 30 { 31 int tx,ty; 32 for(int k=1;k<=4;k++) 33 { 34 tx=i+zy[k][0],ty=j+zy[k][1]; 35 if(!tx||!ty||tx==n+1||ty==m+1) continue; 36 if(ma[tx][ty]!=ma[i][j]&&ma[tx][ty]&&ma[i][j]) 37 { 38 build(bh[i][j],bh[tx][ty]); 39 } 40 } 41 } 42 } 43 } 44 int b[N*N]; 45 bool fw[N*N],bj[N*N],cz[N*N]; 46 bool find(int x) 47 { 48 for(int i=a[x];i;i=road[i].next) 49 { 50 int y=road[i].to; 51 if(fw[y]||cz[y]) continue; 52 fw[y]=1; 53 if(!b[y]||find(b[y])) 54 { 55 b[y]=x; 56 bj[y]=1; 57 return 1; 58 } 59 } 60 return 0; 61 } 62 bool check(int x,int y) 63 { 64 int js=0; 65 memset(bj,0,sizeof(bj)); 66 memset(b,0,sizeof(b)); 67 for(int i=1;i<=n;i++) 68 { 69 for(int j=1;j<=m;j++) 70 { 71 if(ma[i][j]==2) 72 { 73 memset(fw,0,sizeof(fw)); 74 if(find(bh[i][j])) js++; 75 } 76 } 77 } 78 if(bj[bh[x][y]]) 79 { 80 int js2=0; 81 cz[bh[x][y]]=1; 82 memset(b,0,sizeof(b)); 83 for(int i=1;i<=n;i++) 84 { 85 for(int j=1;j<=m;j++) 86 { 87 if(ma[i][j]==2) 88 { 89 memset(fw,0,sizeof(fw)); 90 if(find(bh[i][j])) js2++; 91 } 92 } 93 } 94 cz[bh[x][y]]=0; 95 if(js==js2) return 1; 96 else return 0; 97 } 98 else return 1; 99 } 100 int main() 101 { 102 scanf("%d%d",&n,&m); 103 int sx,sy; 104 for(int i=1;i<=n;i++) 105 { 106 scanf("%s",bb+1); 107 for(int j=1;j<=m;j++) 108 { 109 if(bb[j]==\'.\') ma[i][j]=0,sx=i,sy=j; 110 else if(bb[j]==\'O\') ma[i][j]=1; 111 else ma[i][j]=2; 112 bh[i][j]=(i-1)*m+j; 113 } 114 } 115 116 zy[1][0]=1,zy[1][1]=0; 117 zy[2][0]=-1,zy[2][1]=0; 118 zy[3][0]=0,zy[3][1]=1; 119 zy[4][0]=0,zy[4][1]=-1; 120 scanf("%d",&p); 121 for(int i=1;i<=p;i++) 122 { 123 int x,y; 124 scanf("%d%d",&x,&y); 125 bui(); 126 if(!check(x,y)) 127 { 128 for(int j=1;j<=4;j++) 129 { 130 int tx=sx+zy[j][0],ty=sy+zy[j][1]; 131 if(!tx||!ty||tx==n+1||ty==m+1||(tx==x&&ty==y)) continue; 132 if(ma[tx][ty]==1) 133 { 134 if(check(tx,ty)) 135 { 136 zz2++; 137 jg[zz2]=i; 138 break; 139 } 140 } 141 } 142 } 143 swap(ma[sx][sy],ma[x][y]); 144 sx=x,sy=y; 145 scanf("%d%d",&x,&y); 146 swap(ma[x][y],ma[sx][sy]); 147 sx=x,sy=y; 148 } 149 printf("%d\\n",zz2); 150 for(int i=1;i<=zz2;i++) 151 { 152 printf("%d\\n",jg[i]); 153 } 154 return 0; 155 }
Bzoj 4035 数组游戏
这是一个幽默的故事。
博主一开始想到了当n/x相等时两个格子本质相同,然后就走向了“找对称”的不归路。这时候就体现出了思维的重要性了,事实证明博主并没有。我们可以回到一个很基础的函数:SG函数。
由于网上十分优秀的资料很多,关于SG函数我就不赘述了。
我们尝试着将问题转化:如果我们可以直接翻转黑格子,将失败条件改为全都是黑格子,问题是否会发生变化呢?很明显,如果翻完只有那一个白格子说明之前游戏早就结束了,肯定不对。如果不是,我们翻转黑格子一定会对自己有利,那么对方一定会给你再翻回来,然后两个傻蛋就这么死磕了……所以我们并不会去翻黑格子,问题并没有发生本质变化。但是我们就可以由于“只能翻白格子”的取消,我们可以对每一个点一视同仁的求sg函数。此时当我们以翻转一个棋子(包括顺带着翻他后面的)看做一个子游戏,那么他的后继状态就是2x、3x……所以sg[x]=mex(sg[2*x],sg[2*x]^sg[3*x],sg[2*x]^sg[3*x]^sg[4*x],……)现在就比较好理解了。
接下来我们的目的就是求SG函数。正如上面所言,所有n/x相等的数本质都是一样的。所以我们在这里可以使用除法分块。我们先枚举n/x的值,然后依次枚举题目描述里的k。直接说k或许并不是太准确,与其说枚举k,更好的说法是枚举每一个sg值相同的k的区间的起始k值。也就是说有很多k值的sg值相等(也正是这点,使我们的时间复杂度并不是O(n),话说谁能帮忙证一下到底是多少?)对于每一组数据直接异或上对应节点的sg值便是了。(代码里会有注释)
1 #include<cstdio> 2 #include<cstdlib> 3 #include<cstring> 4 #include<iostream> 5 #include<algorithm> 6 #include<cmath> 7 #define N 100000+10 8 using namespace std; 9 int n,m; 10 int ch(int x,int y) 11 { 12 return (x==y)?y+1:y/(y/(x+1)); 13 } 14 int c[2][N]; 15 int zz,jg[N]; 16 bool vis[N]; 17 void init() 18 { 19 for(int i=1;i<=n;i=ch(i,n))//枚举n/x的值 20 { 21 zz=0; 22 int now=0; 23 for(int j=2;j<=i;j=ch(j,i))//再次进行分块 ,注意从2开始 24 { 25 int x=i/j;//这里的x实际上就是题目里的k 26 int t=(x>m)?c[1][n/x]:c[0][x];//在这里体现出的j的分块的实际意义 27 zz++; 28 jg[zz]=now^t; 29 vis[jg[zz]]=1; 30 if((i/x-(i/(x+1)))&1) now^=t;//如果块内的k的个数是奇数个那么对于now需要异或上结果 31 } 32 now=1; 威佐夫博弈 入门小结