【博弈论】博弈论题单题解
会不断更新的(咕咕咕)
题目难度大致满足非降性博弈论真是深坑啊,填不动了,还有Nim积、Every-SG游戏等等等等很多题型还不会,先去学别的了
涉及知识:
SG函数及SG定理:传送门
博弈论知识总结:传送门
文章目录
- 简单博弈(入门)
- 1.Brave Game HDU - 1846
- 2.Good Luck in CET-4 Everybody! HDU - 1847
- 3.Fibonacci again and again HDU - 1848
- 4.Rabbit and Grass HDU - 1849
- 5.Being a Good Boy in Spring Festival HDU - 1850
- 6.kiki's game HDU - 2147
- 7.S-Nim HDU - 1536
- 8.Northcott Game HDU - 1730
- 9.Doubloon Game HDU - 4203
- 博弈论+搜索
- 1.The Game of 31 HDU - 4155
- 2.A Multiplication Game HDU - 1517
- 3.Stone Game HDU - 1729
- 4.Chess HDU - 5724
- 5.A Chess Game HDU - 1524
- 6.A New Tetris Game HDU - 1760
- 7.Digital Deletions HDU - 1404
- 8.Alice and Bob HDU - 4111
- Mult-SG游戏
- 1.A Simple Nim HDU - 5795
- 2.Stone Game, Why are you always there? HDU - 2999
- 3.Paint Chain HDU - 3980
- 4.Bomb Game HDU - 2873
- 5.Lunch HDU - 6892
- Anti-SG游戏
- 1.John HDU - 1907
- 2.Be the Winner HDU - 2509
- 删边游戏
- 1.A tree game HDU - 3094
- 2.PP and QQ HDU - 3590
简单博弈(入门)
一些经典的博弈题目或其变形,适合刚入门的时候刷
1.Brave Game HDU - 1846
题目链接:hdu-1846
题目大意:
巴什博弈
一堆石子共n个,两人轮流取,可以取1~m个
代码:
#include <iostream>
#include <algorithm>
#include <string>
#include <cstdio>
#include <cstring>
using namespace std;int main(){int C;scanf("%d",&C);while(C--){int n,m;scanf("%d%d",&n,&m);if(n%(m+1)==0) printf("second\n");else printf("first\n");}return 0;
}
2.Good Luck in CET-4 Everybody! HDU - 1847
题目链接:hdu-1847
题目大意:
两个人轮流从n张牌中取牌,每次只能取{1,2,4……}等2的幂次张牌。最后把牌取完的人获胜
分析:
直接推SG函数,发现规律:只要n%3==0则先手必败
这个规律和巴什博弈很像
其实这道题和巴什博弈也很像,都是从一堆东西中取东西,只是对取牌数量的限制不一样而已
3张牌是一个必败态
对于任意的n,只要n%3≠0,我们就可以取1张或两张使其维持为3的倍数,这样一直到n=3,留给对手的就是必败的局面
也就是相当于m=2的巴什博弈,因为可以通过取1或2张牌得到必胜策略。举个例子:n=4的时候,我们可以根据一次取4张获胜,也可以按照必胜策略先取1张,留给对手3张牌的必败局面从而获胜。
因为2的幂次%3必不等于0,对于n%3==0的情况,不管怎么取牌对方都可以让局势重新回到n%3的情况。
SG打表代码:
#include <iostream>
#include <algorithm>
#include <string>
#include <cstdio>
#include <cstring>
using namespace std;int f[15]={1,2,4,8,16,32,64,128,256,512};
int SG[1005];
int vis[1005];void get_SG(){memset(SG,0,sizeof(SG));for(int i=1;i<=1000;i++){memset(vis,0,sizeof(vis));for(int j=0;j<10&&i-f[j]>=0;j++)vis[SG[i-f[j]]]=1;for(int j=0;j<=1000;j++)if(!vis[j]){SG[i]=j;break;}}
}int main(){get_SG();int n;while(~scanf("%d",&n)){if(SG[n]) printf("Kiki\n");else printf("Cici\n");}return 0;
}
3.Fibonacci again and again HDU - 1848
题目链接:hdu-1848
题目大意:
三堆石子,各有m、n、p个,两个人轮流选一堆取石子,先取完的人获胜,规定每次取石子的数量必须为斐波那契数。
也就是三堆的Nim游戏,并对取石子数量做出了限制
分析:
直接推SG函数
吐槽一下位运算符号优先级比 比较符号优先级低,所以一开始写if(SG[m]^SG[n]^SG[p]==0) printf("Nacci\n");
就WA了嘤嘤嘤
代码:
#include <iostream>
#include <algorithm>
#include <string>
#include <cstdio>
#include <cstring>
using namespace std;const int maxn=1e3+10;
int f[maxn];
int SG[maxn];
int vis[maxn];void get_SG(){f[0]=1;f[1]=1;for(int i=2;i<=1000;i++)f[i]=f[i-1]+f[i-2];memset(SG,0,sizeof(SG));for(int i=1;i<=1000;i++){memset(vis,0,sizeof(vis));for(int j=1;j<=1000&&i-f[j]>=0;j++)vis[SG[i-f[j]]]=1;for(int j=0;j<=1000;j++)if(!vis[j]){SG[i]=j;break;}}
}int main(){get_SG();int m,n,p;while(~scanf("%d%d%d",&m,&n,&p)){if(!m&&!n&&!p) break;if((SG[m]^SG[n]^SG[p])==0)printf("Nacci\n");elseprintf("Fibo\n");}return 0;
}
4.Rabbit and Grass HDU - 1849
题目链接:HDU-1849
题目大意:
在一个1*n的棋盘中,每个棋子的初始位置为 a i a_i ai,合法操作为令任意一颗棋子向左移动任意步,当棋子走到0位置时不能再移动,最后走棋的一方获胜
分析:
将棋子的位置看作石子的数量,每次可以选择一枚棋子向左移动,即任意选一堆石子取出任意个。Nim游戏的变形,n堆石子的数量相异或即可,为0则为先手必败态
代码:
#include <iostream>
#include <algorithm>
#include <string>
#include <cstdio>
#include <cstring>
using namespace std;int main(){int m;while(~scanf("%d",&m)&&m){int ans=0;for(int i=0;i<m;i++){int temp;scanf("%d",&temp);ans^=temp;}if(!ans) printf("Grass Win!\n");else printf("Rabbit Win!\n");}return 0;
}
5.Being a Good Boy in Spring Festival HDU - 1850
题目链接:hdu-1850
题目大意:
求Nim游戏,先手想要获胜第一步的走法有几种
分析:
先手面对的局面为 a 0 ⊕ a 1 ⊕ … … ⊕ a n = k a_0⊕a_1⊕……⊕a_n=k a0⊕a1⊕……⊕an=k
若k=0,则先手必败,走法为0
若k≠0,则必然起码有一个 a i a_i ai满足 a i > a i ⊕ k a_i>a_i⊕k ai>ai⊕k,让 a i a_i ai变为 a i ⊕ k a_i⊕k ai⊕k是先手将局面变为必败态的一种策略:因为等式两边异或上k,右边变为0(即必败局面)。但是因为这里的 a i a_i ai是石子数量,合法操作只能令其减少,所以要求 a i > a i ⊕ k a_i>a_i⊕k ai>ai⊕k。
枚举n堆石子,满足 a i > a i ⊕ k a_i>a_i⊕k ai>ai⊕k的即为合法操作
代码
#include <iostream>
#include <algorithm>
#include <string>
#include <cstdio>
#include <cstring>
using namespace std;int card[105];int main(){int M;while(~scanf("%d",&M)&&M){int k=0;int ans=0;for(int i=0;i<M;i++){scanf("%d",&card[i]);k^=card[i];}for(int i=0;i<M;i++){int temp=card[i]^k;if(temp<card[i])ans++;}printf("%d\n",ans);}return 0;
}
6.kiki’s game HDU - 2147
题目链接:HDU-2147
题目大意:
给定一个n*m的棋盘,棋子一开始在棋盘的右上角,两人轮流移动棋子,可以将其向左/下/左下移动一格,先将其移动棋盘左下角的人获胜
分析:
棋盘左下角为必败态,从左下角开始推即可找到规律,当n、m都为奇数时先手必败
校赛上做过一道差不多的题:
题目链接:NEFU OJ-2226
代码
#include <iostream>
#include <algorithm>
#include <string>
#include <cstdio>
#include <cstring>
using namespace std;int main(){int n,m;while(~scanf("%d%d",&n,&m)&&n&&m){if(n%2&&m%2) printf("What a pity!\n");else printf("Wonderful!\n");}return 0;
}
7.S-Nim HDU - 1536
题目链接:HDU-1536
题目大意:
n堆牌的nim游戏,同时对取牌数量做出限制,取牌数量只能为题目给出的数字
分析:
简单题,直接Sg函数打表即可。数据范围是 1 0 4 10^4 104,一开始tle了,后来发现tle的原因应该是vis数组的类型,如果设为bool型就不会超时(大概是因为memset函数)
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;const int maxn=10005;
int k;
int f[105];
int SG[maxn];
bool vis[105];void get_SG(){memset(SG,0,sizeof(SG));for(int i=1;i<=10000;i++){memset(vis,0,sizeof(vis));for(int j=0;j<k&&i-f[j]>=0;j++)vis[SG[i-f[j]]]=1;for(int j=0;j<=100;j++){if(!vis[j]){SG[i]=j;break;}}}
}int main(){while(~scanf("%d",&k)&&k){for(int i=0;i<k;i++)scanf("%d",&f[i]);sort(f,f+k);get_SG();int m;scanf("%d",&m);for(int i=0;i<m;i++){int l;scanf("%d",&l);int ret=0;for(int i=0;i<l;i++){int temp;scanf("%d",&temp);ret^=SG[temp];}if(!ret) printf("L");else printf("W");}printf("\n");}return 0;
}
8.Northcott Game HDU - 1730
题目链接:HDU-1730
题目大意:
游戏在一个n行m列(1 ≤ n ≤ 1000且2 ≤ m ≤ 100)的棋盘上进行,每行有一个黑子(黑方)和一个白子(白方)。执黑的一方先行,每次玩家可以移动己方的任何一枚棋子到同一行的任何一个空格上,当然这过程中不许越过该行的敌方棋子。双方轮流移动,直到某一方无法行动为止,移动最后一步的玩家获胜。
分析:
很巧妙的一道题,Nim游戏的变形。这道题双方移动不同的棋子,看似不符合ICG游戏的定义,但是其实可以换个角度考虑:
只考虑一行,当两个棋子贴在一起时(即相邻),先手不管如何移动,后手都可以令棋子始终保持相邻的状态,直到先手碰到棋盘的边界后无法再移动。
所以关键的因素在于两个棋子的间距。双方的移动本质上是对棋子间距的改变,将这个间距视为石子数,则游戏转化为了Nim游戏。
代码:
#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;int main(){int N,M;while(~scanf("%d%d",&N,&M)){int ret=0;for(int i=0;i<N;i++){int t,j;scanf("%d%d",&t,&j);ret^=(abs(t-j)-1);}if(!ret) printf("BAD LUCK!\n");else printf("I WIN!\n");}return 0;
}
9.Doubloon Game HDU - 4203
题目链接:HDU-4203
题目大意:
一堆S个石子,规定每次只能取K的幂次个石子,给出S和K,求先手想要获胜第一步最少要取几个石子(若必败输出0)
分析:
前面的第二题规定是2的幂次,这题是K的幂次:
打表找规律,可知当K为奇数时,SG值序列为:
0、1、0、1、0、1、0、1、0、1、0……
当K=偶数时,SG值序列为:
长度为K+1的循环节:0、1、0、1、0、……2
当SG值为2时,至少一次取出K个达到必败态
当SG值为1时,至少一次取出1个达到必败态
当SG值为0时,必败,输出0
代码:
#include <cstdio>
using namespace std;int main()
{int t;scanf("%d",&t);while(t--){int S,K;scanf("%d%d",&S,&K);if(K%2==1) printf("%d\n",S%2);else {if(S%(K+1)==K) printf("%d\n",K);else printf("%d\n",S%(K+1)%2);}}return 0;
}
博弈论+搜索
ACM博弈论中涉及的游戏都需要递归得到结果,但是诸如从石堆中取石子的游戏,可以将取石子抽象为石子数的减法,因此可以利用dp的思想轻易得到后继局面。但有些题需要用到递归函数来寻找后继局面或者说只需要知道一个局面的必胜/必败性从而不需要打表,可以直接递归搜索后继局面有无必败态,收录如下:
1.The Game of 31 HDU - 4155
题目链接:HDU-4155
题目大意:
点数1,2,3,4,5,6的牌各4张,双方轮流操作,每回合选择一张牌,将其点数加到sum上,令sum超过31的玩家输掉游戏
分析:
将大于31的数设为必胜态(若无论如何都会让点数大于31则输,即后继状态都为必胜态)
回溯搜索即可
#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <string>
using namespace std;int f[6]={4,4,4,4,4,4}; //操作集合bool dfs(int sum){if(sum>31) return 1; for(int i=0;i<6;i++){if(!f[i]) continue;f[i]--;int t=dfs(sum+i+1); f[i]++; //回溯if(!t) return 1;}return 0;
}int main(){string s;while(cin>>s){for(int i=0;i<6;i++) f[i]=4;int sum=0;for(int i=0;i<s.size();i++)f[s[i]-'1']--,sum+=(s[i]-'0');int t=dfs(sum);cout<<s<<' ';if((!t&&s.size()%2==0)||(t&&s.size()%2==1))cout<<'B'<<endl;elsecout<<'A'<<endl;}return 0;
}
2.A Multiplication Game HDU - 1517
题目链接:HDU-1517
题目大意:
初始时p=1,双方轮流选一个2~9的数x,令p=p*x,先令p>=n的玩家获胜。给出一些n,判断获胜方
分析:
和上一题很像,但是不能直接搜索,会爆。可以发现:
假设n=120,则可以一步令p>=n的数(即乘上9大于n即可)14~120都是必胜态,而一步后只能到达必胜态的数(即乘上最小的2还是大于14)7 ~ 13是必败态,递归求。直到n==1为止
代码:
#include <cstdio>
using namespace std;int flag=0; //记录函数调用次数
bool get_SG(long long N){long long k;if(flag%2==0){k=N/9;while(k*9<N) k++;}else{k=N/2;while(k*2<N) k++;}if(k==1) return flag%2;flag++;return get_SG(k);
}int main()
{long long N;while(~scanf("%lld",&N)){flag=0;if(!get_SG(N)) printf("Stan wins.\n");else printf("Ollie wins.\n");}return 0;
}
3.Stone Game HDU - 1729
题目链接:HDU-1729
题目大意:
n个盒子,每个盒子初始时有 c i c_i ci个石头,两人轮流操作,规定每次可以选择一个盒子向里面加入石头,加入石头的数量必须小于等于当前石头数的平方。比如一个盒子当前有3个石头,则可以加入1-9个石头。
先不能继续操作输掉游戏
分析:
显然地,每次只能操作一个盒子,所以将一个盒子视作一个分游戏然后求出SG值再异或即可。因为不同盒子的容量不一样,所以不同盒子并不是同一类游戏,因此SG打表没必要且会超时。
那么给出S和C(盒子的容量和初始石头数)怎么求出其SG值呢?
以S=1000,C=7为例。
①(1000,1000)无法再加入石头,为必败态
②(1000,999) ……(1000,X)只要满足 X 2 + X > = 1000 X^2+X>=1000 X2+X>=1000即为必胜态,其SG值为1000-X(因为大于X小于1000的数都是X的后继局面)
③当X减少至 X 2 + X < 1000 X^2+X<1000 X2+X<1000时,(1000,X)为必败态,因为其所有后继局面都是必胜态,易求得X=31,此时SG=0。
④当X=30时,其后继局面为31到 X 2 + X = 930 X^2+X=930 X2+X=930,SG=mex{ 0,70,71,……,968 }=1;
X=29,同理SG=2
……
直到X=6,SG=31-X=25;
可以看出1000-32的SG值不会影响31-6的SG值,因此当X<32时,可以将S视作31。
(不会严谨的证明,但是自己手推几组后就能确定这个规律)
⑤当X=5时,因为 5 ∗ 5 + 5 = 30 < 31 5*5+5=30<31 5∗5+5=30<31,所以SG=0,为必败态,又开始一轮循环
找出一般规律后直接递归求解:
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;long long get_SG(long long s,long long c){if(c*c+c>=s) return s-c;for(long long i=(long long )sqrt(s*1.0)+1;i>=0;i--)if(i*i+i<s)return get_SG(i,c);
}int main(){int N;int Case=1;while(~scanf("%d",&N)&&N){long long ans=0;for(int i=0;i<N;i++){long long si,ci;scanf("%lld%lld",&si,&ci);ans^=(get_SG(si,ci));}printf("Case %d:\n",Case++);if(ans==0) printf("No\n");else printf("Yes\n");}return 0;
}
4.Chess HDU - 5724
题目链接:HDU-5724
题目大意:
n行棋盘,每行有m个棋子,规定棋子每次只能向右移动一格。若右边已经有棋子了,就跳过这个位置向右移动到下一个空位。
双方轮流操作,先无法操作的人输掉游戏
分析:
显然一行棋盘视为一个分游戏,用0表示没有棋子,1表示有棋子,将m列的棋盘用m个二进制位表示,利用状压的思想用一个int型变量表示状态
使用二进制枚举暴力搜索,因为棋子只能向右移动,所以一个状态的后继状态用int表示值肯定更小,故从小到大二进制枚举即可解出棋盘所有状态的sg值
代码:
#include <cstdio>
#include <cstring>
#include <bitset>
#include <iostream>
using namespace std;const int maxn=1<<21;
int sg[maxn],vis[25];//每个棋子只有一种走法,所以最多20种后继状态void get_SG(){for(int i=0;i<(1<<20);i++){memset(vis,0,sizeof(vis));for(int j=20;j>=0;j--)if(i&(1<<j)){//第j位上有棋子for(int k=j-1;k>=0;k--){if(!(i&(1<<k))){int temp=i^(1<<j)^(1<<k);vis[sg[temp]]=1;break;}}}for(int j=0;;j++)if(!vis[j]){sg[i]=j;break;}}
}int main()
{get_SG();int T;scanf("%d",&T);while(T--){int N,ans=0;scanf("%d",&N);for(int i=0;i<N;i++){int m;scanf("%d",&m);int value=0;for(int j=0;j<m;j++){int temp;scanf("%d",&temp);value|=(1<<(20-temp));}ans^=sg[value];}if(!ans) printf("NO\n");else printf("YES\n");}return 0;
}
5.A Chess Game HDU - 1524
题目链接:HDU-1524
题目大意:
有向无环图上有n个棋子,双方轮流操作,可以将一颗棋子移动到与其有(有向)边相连的下一个点,当棋子移动到出度为0的点后就无法再移动,最后一个移动棋子的人获胜
分析:
一个棋子的移动视作一个分游戏,因为每一次操作只能移动一个棋子。故计算出每个分游戏的SG值后异或即可。
对于每个分游戏,其必胜/必败性质是由其所在的初始位置决定的,出度为0的点为必败点,利用递推即可计算出SG值
点的数量不到1000,故使用了邻接矩阵作为图的存储结构(事后想想可能邻接表更加合适)
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;const int maxn=1e3+5;int N;
int G[maxn][maxn];
int SG[maxn];void init(){memset(G,0,sizeof(G));for(int i=0;i<=1000;i++)SG[i]=-1;
}int dfs(int u){int buc[maxn],vis[maxn];memset(buc,0,sizeof(buc));int cnt=0;for(int i=0;i<N;i++)if(G[u][i]){ //如果有边vis[cnt++]=i;}if(cnt==0) return 0;else {for(int i=0;i<cnt;i++){if(SG[vis[i]]==-1)SG[vis[i]]=dfs(vis[i]);buc[SG[vis[i]]]++;}for(int i=0;i<=1000;i++)if(!buc[i])return i;}
}int main(){while(~scanf("%d",&N)){init();for(int i=0;i<N;i++){int m;scanf("%d",&m);for(int j=0;j<m;j++){int to;scanf("%d",&to);G[i][to]=1; //表明有边}}for(int i=0;i<N;i++){if(SG[i]==-1)SG[i]=dfs(i);}int m;while(~scanf("%d",&m)&&m){int ans=0;for(int i=0;i<m;i++){int temp;scanf("%d",&temp);ans=ans^SG[temp];}if(ans==0) printf("LOSE\n");else printf("WIN\n");}}return 0;
}
6.A New Tetris Game HDU - 1760
题目链接:HDU-1760
题目大意:
给出一个N* M的棋盘,有些位置不能放棋子。两人轮流操作向棋盘中方2*2的矩形,先无法放矩形的人输掉游戏
分析:
博弈论与回溯搜索相结合
若棋盘没有可以放矩形的地方了,则为必败态。
枚举每一种放法,只要有一种放法放完后后继状态为必败态则为必胜态,若没有则为必败态。
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;int N,M;
int chessboard[55][55]; //0可用 1 不 可用int dfs(){for(int i=2;i<=N;i++)for(int j=2;j<=M;j++){ //枚举每个点作为2*2矩阵的右下角if(!chessboard[i][j]&&!chessboard[i-1][j]&&!chessboard[i-1][j-1]&&!chessboard[i][j-1]){chessboard[i][j]=1,chessboard[i-1][j]=1,chessboard[i-1][j-1]=1,chessboard[i][j-1]=1;int t=dfs();chessboard[i][j]=0,chessboard[i-1][j]=0,chessboard[i-1][j-1]=0,chessboard[i][j-1]=0;if(!t) return 1; //只要找到一个必败态,则该局面为必胜态}}return 0; //如果所有后继局面都没有必败态,则该局面为必败态
}int main(){while(~scanf("%d%d",&N,&M)){for(int i=1;i<=N;i++)for(int j=1;j<=M;j++){char temp;scanf(" %c",&temp);chessboard[i][j]=temp-'0';}if(dfs()) printf("Yes\n");else printf("No\n");}return 0;
}
7.Digital Deletions HDU - 1404
题目链接:HDU-1404
这道题利用筛法的思想优化了一下,感觉很有意思
题目大意:
给出一个小于1e6的数,两个人轮流进行操作:
①选择其中任意一个数位上的数x,将其减小为x-1、x-2……0
②若一个数位上的数为0,可以将0及0右边的数都抹掉
先把所有数字
注:前导0允许存在
比如10789可以依据操作①变为10787或00789……,也可以由操作②变为1
分析:
terminal position是所有数字都被抹除的局面;
含前导0的数(包括0)可以一步移动到terminal position,所以是必胜态。
1只能变为0,为必败态。
这道题数字比较大,因此按照之前常规的求SG方法(即求出一个数所有后继状态的SG值)应该会超时
利用筛法的思想,类似素数筛。先将一个局面映射为数(因为含前导0的必然为必胜态不需要考虑),将必败态类比为素数。先将所有数的SG值初始化为0(必败态),从1(1为必败态)开始考虑,对所有可以变为1的数(即后继局面含必败态的)进行标记为必胜态。可以发现题目给出的两种操作都是会导致值减小的,因此当遍历到一个数,其SG值为0表明未被比它更小的数标记过,也就是它的所有后继局面中没有必败态,故该局面为必败态。
但是这样求出来的SG值不是真正的SG值,而只能表明一个状态是必胜/必败的。因为不是真正的SG值,所以不能使用SG定理,不能用于组合游戏中。但这道题只是单个游戏所以可以这么做。
代码:
#include <iostream>
#include <algorithm>
#include <string>
#include <cstdio>
#include <cstring>
using namespace std;const int maxn=1e6+10;
int SG[maxn];int s_to_int(string s){ //设计字符串到数的变化int ret=0;int t=1;for(int i=s.size()-1;i>=0;i--){ret+=t*(s[i]-'0');t*=10;}return ret;
}int len(int x){//求一个数的位数int ret=0;while(x) x/=10,ret++;return ret;
}int get_num(int x,int pos){ //数x某一位置上的数for(int j=1;j<pos;j++)x/=10;return x%10;
}void get_SG(){memset(SG,0,sizeof(SG));for(int i=1;i<=999999;i++)if(!SG[i]){//第一种变化方式是每个数位上一直加到9int add=1;for(int j=1;j<=len(i);j++){int start=get_num(i,j);for(int k=start+1;k<=9;k++)SG[i+(k-start)*add]=1;//标记为必胜点add*=10;}//第二种变化方式是乘10,后面的数位随便填数字if(len(i)<6){int temp=i*10;SG[temp]=1;temp*=10;int t=10;while(len(temp)<=6){for(int j=0;j<t;j++){SG[temp+j]=1;}temp*=10,t*=10;}}}
}int main(){ios::sync_with_stdio(false);get_SG();string s;while(cin>>s){if(s[0]=='0'){cout<<"Yes"<<endl;continue;}int num=s_to_int(s);if(SG[num]==0) cout<<"No"<<endl;else cout<<"Yes"<<endl;}return 0;
}
8.Alice and Bob HDU - 4111
题目链接:HDU-4111
题目大意:
这题把我做吐了,细节实在太多了。
游戏规则很简单,有N堆石子,双方轮流操作,每回合有两种走法:①从一堆石子中取1个石子出来 ②合并两堆石子
若石子数变为0则自动消失,先无法操作的人输掉游戏
分析:
先从一堆石子开始分析,因为没有合并操作,所以双方只能一个一个拿,很容易得到当石子数为奇数时先手必胜,否则后手必胜
考虑合并的意义:
当有N堆石子时,如果双方不采取合并操作,那么和一堆石子的情况一样,石子总数为奇数先手必胜,否则后手必胜
合并操作的意义在于,可以不取石子。即若采取了合并操作,则场上石子总数不变,但是将由对手来下一步操作。
举个例子:
场上有两堆石子,一堆有2个,一堆有4个。则若双方一颗一颗地取最后将是先手失败,因为石子总数是偶数。但是先手可以合并这两堆石子,形成一堆6个石子,这样一来,石子数仍然是偶数,但是由对手接过了这个败态!
也就是说合并操作相当于一次改变局势的机会。
推广到N堆石子:
若有奇数堆石子,则有偶数次合并操作。若石子总数为偶数个,则先手必败,偶数个石子是必败态,而合并操作总数也是偶数个,所以无法通过合并操作改变局势
因此可以发现获胜与否和合并操作及石子总数的奇偶性有关。枚举一下即可找到规律:合并操作次数+石子总数为偶数则先手败,否则先手胜
考虑特殊情况:
若一堆石子为1,则减掉这堆石子的同时会令合并操作次数减少1。之前的推论都是建立在一次操作要不减少合并次数要不减少石子数的基础上的,如果存在石子数为1的石子堆则上述结论不成立。
但是考虑到这道题最多只有50堆石子,因此其实含1的特例是比较少的,可以直接记忆化搜索得到这种特殊情况下的解:
SG[a][b],a表示1的个数,b表示非1堆合并次数加石子数的值
考虑场上有A堆石子数为1的石子,以及B堆其它的
则后继状态有:
①从其中一个石子数为1的石子堆取走一个,后继状态为SG[a-1][b]
②合并两个石子数为1的石子堆,则多了一个石子数为2的堆,后继状态为SG[a-2][b+3]或SG[a-2][b+2](取决于场上是否有非1堆)
③将一个石子数为1的石子堆和另一个非1堆合并,后继状态为SG[a-1][b+1]
④合并两个非1堆或者从一个非1堆中取出1个石子,后继状态都是SG[a][b-1]
这里有个坑点,当b的值变为1时说明只剩一个石子了,将其转化为SG[a+1][0]
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <cstdlib>
using namespace std;int SG[55][50005];int dfs(int a,int b){if(SG[a][b]!=-1) return SG[a][b];if(b==1) return SG[a][b]=dfs(a+1,0);if(a==0) return SG[a][b]=b%2;if(a>=1&&!dfs(a-1,b)) return SG[a][b]=1;if(a>=1&&b&&!dfs(a-1,b+1)) return SG[a][b]=1;if(a>=2&&b&&!dfs(a-2,b+3)) return SG[a][b]=1;if(a>=2&&!b&&!dfs(a-2,b+2)) return SG[a][b]=1;if(b>=2&&!dfs(a,b-1)) return SG[a][b]=1;return SG[a][b]=0;
}int main()
{memset(SG,-1,sizeof(SG));int T;int Case=1;scanf("%d",&T);while(T--){int N;scanf("%d",&N);int sum=0;int flag=0;for(int i=0;i<N;i++){int temp;scanf("%d",&temp);if(temp==1) flag++;else sum+=temp;}if(N-flag) sum+=(N-flag-1);if(dfs(flag,sum)) printf("Case #%d: Alice\n",Case++);else printf("Case #%d: Bob\n",Case++);}return 0;
}
Mult-SG游戏
也可以称为SG游戏的嵌套,即一个游戏的后继局面是多个游戏
1.A Simple Nim HDU - 5795
题目链接:HDU-5795
题目大意:
Nim游戏变形:N堆石子,双方轮流操作,每回合可以从一堆石子中取任意颗石子或者将石子分成三堆(每堆至少一颗石子)
分析:
一开始我以为任意一个数划分为三份后再异或值肯定小于该数本身,就直接交了发Nim,后来发现只要二进制表示法有3个或以上的1就可以划分为3份后令异或值等于本身。。。
比如7的二进制表示为111,则其SG值为8而不是7
打表找规律:
int SG[200];
int vis[200];void get_SG(){for(int i=0;i<100;i++){memset(vis,0,sizeof(vis));for(int j=0;j<i;j++)vis[SG[j]]=1;for(int j=1;j<=i-2;j++)for(int k=1;k<=i-j-1;k++)vis[SG[j]^SG[k]^SG[i-j-k]]=1;for(int j=0;;j++)if(!vis[j]){SG[i]=j;break;}}}
AC代码:
#include <cstdio>
#include <cstring>
using namespace std;int main()
{int t;scanf("%d",&t);while(t--){int N;long long ret=0;scanf("%d",&N);for(int i=0;i<N;i++){long long temp;scanf("%lld",&temp);if(temp%8==0) temp--;else if(temp%8==7) temp++;ret^=temp;}if(!ret) printf("Second player wins.\n");else printf("First player wins.\n");}return 0;
}
2.Stone Game, Why are you always there? HDU - 2999
题目链接:HDU-2999
题目大意:
这题目描述也太难懂了。。。
给定一个数集,规定每次只能取石子的数量只能是这个数集中的数
给一排石头,编号1~N,双方玩家轮流操作取石头,注:3号石头若被取走,则2号和4号石头不再被视为相邻。取走最后一个石头的玩家获胜
分析:
一堆石头的后继局面可能形成两堆石头,SG嵌套即可
代码:
#include <cstdio>
#include <cstring>
using namespace std;int N;
int f[105];
int SG[1005];
bool vis[5005];void get_SG(){memset(SG,0,sizeof(SG));for(int i=0;i<=1000;i++){memset(vis,0,sizeof(vis));for(int j=0;j<N&&i-f[j]>=0;j++){for(int k=1;k<=i-f[j]+1;k++) //枚举取石子的起始位置kvis[SG[k-1]^SG[i-f[j]-k+1]]=1;}for(int j=0;;j++)if(!vis[j]){SG[i]=j;break;}}
}int main()
{while(~scanf("%d",&N)){for(int i=0;i<N;i++)scanf("%d",&f[i]);get_SG();int M;scanf("%d",&M);for(int i=0;i<M;i++){int K;scanf("%d",&K);if(!SG[K]) printf("2\n");else printf("1\n");}}return 0;
}
3.Paint Chain HDU - 3980
题目链接:HDU-3980
题目大意:
一串珠子围成一圈,双方轮流从中取出连续的m颗珠子,先无法操作的输掉游戏
分析:
第一次操作完后圆圈少了一段变成了线,然后就是典型的Mult-SG游戏了,后继状态变成两段珠子
代码:
#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;int SG[1005];
bool vis[1005];void get_SG(int M){memset(SG,0,sizeof(SG));for(int i=0;i<=1000;i++){memset(vis,0,sizeof(vis));for(int j=0;j<i-M+1;j++)vis[SG[j]^SG[i-M-j]]=1;for(int j=0;j<=1000;j++){if(!vis[j]){SG[i]=j;break;}}}
}int main()
{int T;scanf("%d",&T);int Case=1;while(T--){int N,M;scanf("%d%d",&N,&M);get_SG(M);printf("Case #%d: ",Case++);if(N>=M&&!SG[N-M]) printf("aekdycoin\n");else printf("abcdxyzk\n");}return 0;
}
4.Bomb Game HDU - 2873
题目链接:HDU-2873
题目大意:
N*M的棋盘上有n个炸弹,两人轮流操作,选择一枚炸弹并引爆:若一个炸弹位于(p,q)位置,爆炸后会分成两个炸弹且分别位于(u,q)和(p,v)且u<p、v<q。也就是说炸弹会分裂成两个,一个向上走,一个向左走。
(1,1)点的炸弹以及两个处于同一位置的炸弹会自动引爆
若一个玩家没有炸弹可以引爆了则输掉游戏
分析:
这是一道组合游戏题,每个炸弹的移动视作一个分游戏
(两个位于同一位置的炸弹会自动引爆,所以貌似分游戏之间会互相影响:但是可以这么考虑,对于两个处于同一位置的炸弹,若它们可以共存,先手移动第一个炸弹之后后手可以复刻先手的操作从而令炸弹消失,因此两个位于同一位置的炸弹和没有炸弹是等价的)。
对于,一个炸弹,其先手胜/败只取决于其位置,所以推出每个位置的SG函数值再异或即得答案:
①(1,1)点是必败点,SG=0
②其它边界上的点(1,x)和(x,1)的SG值为x-1(相当于取单堆石子的游戏)
③(2,2)点的后继局面只有一个,也就是分裂成位于(1,2)和(2,1)的两个炸弹,而此时相当于两个分游戏,因此(2,2)点的SG值即为(1,2)点和(2,1)点的SG值异或的结果。
④对于任意非边界上的点(p,q),其SG值为对所有后继局面的SG值进行mex运算的结果。
因此从边界开始递推即可得到所有点的SG值
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;int N,M;
int SG[55][55];
int vis[3000];void get_SG(){SG[1][1]=0;for(int i=2;i<=50;i++)SG[i][1]=i-1,SG[1][i]=i-1;for(int i=2;i<=50;i++)for(int j=2;j<=50;j++){memset(vis,0,sizeof(vis));for(int k=1;k<i;k++)for(int l=1;l<j;l++){vis[SG[k][j]^SG[i][l]]++;}for(int k=0;k<3000;k++)if(!vis[k]){SG[i][j]=k;break;}}
}int main(){get_SG();while(~scanf("%d%d",&N,&M)&&N&&M){int ans=0;for(int i=1;i<=N;i++)for(int j=1;j<=M;j++){char temp;scanf(" %c",&temp);if(temp=='#') ans^=SG[i][j];}if(ans) printf("John\n");else printf("Jack\n");}return 0;
}
5.Lunch HDU - 6892
题目链接:HDU-6892
题目大意:
n块巧克力,每个巧克力长度为 l i l_i li,两人轮流操作,选择一块巧克力,将其分成k份(要求 l i l_i li%k==0),如果一个人只能选择长度为1的巧克力了(即其它长度的巧克力都已经被分了)则其输掉游戏
分析:
CCPC网络赛的一题,当初看也看不懂到现在一眼就能知道思路,我也算是成长了一点吧
一块巧克力被分解后形成了k个巧克力,显然是Mult-SG游戏,但是数据太大了,打表一发。
可以发现规律,一个数的SG值就是其质因子数,特殊的是因子2不管有几个都只会被计算一次。
预处理 1 0 5 10^5 105以内的素数后,分解素因子得到一个数的SG值,将n个数的SG值异或后即得答案。
疑惑点:开long long 会tle?知识盲区
打表代码:
#include <cstdio>
#include <cstring>
using namespace std;int SG[1005];
int vis[1005];void get_SG(){memset(SG,0,sizeof(SG));for(int i=1;i<=1000;i++){memset(vis,0,sizeof(vis));for(int k=2;k<=1000;k++){//枚举分成k份if(k>i) break;if(i%k!=0) continue;int temp=0;for(int t=0;t<i/k;t++)temp^=SG[k];vis[temp]=1;}for(int j=0;j<=1000;j++){if(!vis[j]){SG[i]=j;break;}}}
}int main()
{get_SG();for(int i=1;i<=200;i++)printf("SG[%d]=%d\n",i,SG[i]);return 0;
}
AC代码:
#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;const int maxn=1e5+10;
int Prime[maxn];
bool Isprime[maxn];
int cnt=0;void pre_(){for(int i=2;i<=100000;i++) Isprime[i]=1;for(int i=2;i<=100000;i++){if(Isprime[i]) Prime[cnt++]=i;for(int j=0;j<cnt&&Prime[j]*i<=100000;j++){Isprime[Prime[j]*i]=0;if(i%Prime[j]==0)break;}}
}int main()
{pre_();int t;scanf("%d",&t);while(t--){int n;int ret=0;scanf("%d",&n);for(int i=0;i<n;i++){int temp;int value=0;scanf("%d",&temp);for(int j=0;j<cnt&&Prime[j]*Prime[j]<=temp;j++){if(temp%Prime[j]==0){value++,temp/=Prime[j];while(temp%Prime[j]==0){temp/=Prime[j];if(j) value++;}}}if(temp>1) value++;ret^=value;}if(!ret) printf("L\n");else printf("W\n");}return 0;
}
Anti-SG游戏
反Nim游戏
SJ定理:先手必胜当且仅当:1)总游戏SG!=0且至少一堆一个分游戏SG>1; 2)总游戏SG=0且所有游戏SG都等于1
二者满足任意一条先手必胜
1.John HDU - 1907
题目链接:HDU-1907
题目大意:
一个盒子中有n种颜色的糖果,两人轮流吃糖果,一次可以选择任意一种颜色的糖果吃掉若干个,吃掉最后一个糖果的玩家输掉游戏
分析:
反Nim游戏模板,将一种颜色的糖果看作一堆石子即可
代码:
#include <cstdio>
#include <cstring>
using namespace std;int main()
{int T;scanf("%d",&T);while(T--){int N,flag=0;int ret=0;scanf("%d",&N);for(int i=0;i<N;i++){int temp;scanf("%d",&temp);ret^=temp;if(temp>1) flag=1;}if((ret==0&&!flag)||(ret!=0&&flag))printf("John\n");else printf("Brother\n");}return 0;
}
2.Be the Winner HDU - 2509
题目链接:HDU-2509
题目大意:
n堆苹果,每堆苹果有 m i m_i mi个,这 m i m_i mi个苹果排成一列,两个玩家轮流操作,选取其中的一堆苹果取走连续一段的苹果(就是说可以将苹果变成两堆),取走最后一个苹果的输掉游戏
分析:
Mult-SG和Anti-SG的结合,这道题也加深了我对反Nim博弈的理解。令我疑惑的是网上都把这题当成了反Nim的模板题。。。
题目保证每堆苹果的个数不超过100,那么直接打表,可以得到单堆苹果做游戏的SG值。巧合的是SG[i]=i。
分析一下,发现因为可以取任意个数的苹果,所以i个苹果的后继状态包括1~i-1个苹果。至于形成两堆的情况:任意的数n,将其划分为a与b(a+b=n),可以证明a⊕b≤n,而因为这两堆是被取走了起码一个后形成的,所以它们的异或值必然小于n。
因此这道题虽然仍然可以套个反Nim的模板就过掉,但是很多人估计都没有考虑到这一点。
代码:
#include <cstdio>
#include <cstring>
using namespace std;int SG[105];
bool vis[105];void get_SG(){memset(SG,0,sizeof(SG));for(int i=0;i<=100;i++){memset(vis,0,sizeof(vis));int flag=0;for(int j=1;j<=i;j++){ //枚举拿苹果的数量for(int k=1;k<=i-j+1;k++)vis[SG[k-1]^SG[i-j-k+1]]=1;}for(int j=0;j<=100;j++)if(!vis[j]){SG[i]=j;break;}}
}int main()
{get_SG();int N;while(~scanf("%d",&N)){int ret=0;int flag=0;for(int i=0;i<N;i++){int temp;scanf("%d",&temp);ret^=SG[temp];if(SG[temp]>1) flag=1;}if((!flag&&!ret)||(flag&&ret))printf("Yes\n");else printf("No\n");}return 0;
}
删边游戏
删边游戏——披着图论皮的Nim游戏
1.A tree game HDU - 3094
题目链接:HDU-3094
题目大意:
树上删边游戏:双方轮流操作,每个回合可以选择一条边,删除该边,然后删除所有与根节点不再相连的边,先无法操作的玩家输掉游戏
分析:
定理:叶子节点SG值为0,其他节点sg值为所有子节点的sg值加1后异或的值
链式前向星存图,dfs遍历求sg值
代码:
#include <cstdio>
#include <cstring>
using namespace std;const int maxn=1e5+10;typedef struct{int next;int to;
}edge;edge e[maxn];
int cnt=1;
int head[maxn];
int vis[maxn];void add(int x,int y){e[cnt].to=y;e[cnt].next=head[x];head[x]=cnt++;
}int dfs(int u){vis[u]=1;int ret=0;for(int i=head[u];i;i=e[i].next){int v=e[i].to;if(vis[v])continue;ret^=(dfs(v)+1);}return ret;
}void init(){cnt=1;memset(vis,0,sizeof(vis));memset(head,0,sizeof(head));
}int main()
{int T;scanf("%d",&T);while(T--){init();int N;scanf("%d",&N);for(int i=0;i<N-1;i++){int u,v;scanf("%d%d",&u,&v);add(u,v);add(v,u);}if(!dfs(1)) printf("Bob\n");else printf("Alice\n");}return 0;
}
2.PP and QQ HDU - 3590
题目链接:HDU-3590
题目大意:
多棵树同时进行删边游戏,且先无法操作的人获胜
分析:
删边游戏+反Nim游戏,将每个树视为一个独立的分游戏,按照反Nim的规则判断即可
代码:
#include <cstdio>
#include <cstring>
using namespace std;const int maxn=1e5+10;typedef struct{int next;int to;
}edge;edge e[maxn];
int cnt=1;
int head[maxn];
int vis[maxn];void add(int x,int y){e[cnt].to=y;e[cnt].next=head[x];head[x]=cnt++;
}int dfs(int u){vis[u]=1;int ret=0;for(int i=head[u];i;i=e[i].next){int v=e[i].to;if(vis[v])continue;ret^=(dfs(v)+1);}return ret;
}void init(){cnt=1;memset(vis,0,sizeof(vis));memset(head,0,sizeof(head));
}int main()
{int N;while(~scanf("%d",&N)){int ans=0;int flag=0;for(int i=0;i<N;i++){init();int m;scanf("%d",&m);for(int i=0;i<m-1;i++){int u,v;scanf("%d%d",&u,&v);add(u,v);add(v,u);}int t=dfs(1);ans^=t;if(t>1) flag=1;}if((!ans&&!flag)||(ans&&flag)) printf("PP\n");else printf("QQ\n");}return 0;
}
【博弈论】博弈论题单题解相关推荐
- 基础博弈论题和一些题解
1.Brave Game HDU - 1846 题意:给出n个石子和m,俩人取,每次最多取m个,不能取的输,问输的是谁. 思路:典型巴什博弈,可以发现,如果一共有m+1个物品,我们去取它,先手至少 ...
- 【解题报告系列】超高质量题单 + 题解(ACM / OI)超高质量题解
整理的算法模板合集: ACM模板 点我看算法全家桶系列!!! 实际上是一个全新的精炼模板整合计划 繁凡出品的全新系列:解题报告系列 -- 超高质量算法题单,配套我新写的超高质量的题解和代码,题目难度不 ...
- 博弈论题表(好少~~~)
bzoj2017:[Usaco2009 Nov]硬币游戏 *用了一小点思想的傻逼dp(记忆化搜索) bzoj1188:[HNOI2007]分裂游戏 **很神奇的把游戏拆分为子游戏的方法 bzoj102 ...
- 【OJ】洛谷函数与结构体题单题解锦集
题单简介 题目解析 P5735 距离函数 P5736 质数筛 P5737 闰年展示 P5738 歌唱比赛 P5739 计算阶乘 P5461 赦免战俘 P5740 最厉害的学生 P5741 旗鼓相当的对 ...
- 【OJ】洛谷暴力枚举题单题解锦集
题单简介 题目解析 P2241 统计方形(数据加强版) P2089 烤鸡 P1618 三连击(升级版) P1036 选数 P1157 组合的输出 P1706 全排列问题 P1088 火星人 P3392 ...
- 【OJ】洛谷循环结构题单题解锦集
题单简介 题目解析 P5718[深基4.例2]找最小值 P5719[深基4.例3]分类平均 P5720[深基4.例4]一尺之棰 P5721[深基4.例6]数字直角三角形 P1009 阶乘之和 P198 ...
- 【OJ】洛谷字符串题单题解锦集
题单简介 题目解析 P5733[深基6.例1]自动修正 P1914 小书童--密码 P1125 笨小猴 P1957 口算练习题 P5015 标题统计 P5734[深基6.例6]文字处理软件 P1308 ...
- 【OJ】洛谷数组题单题解锦集
题单简介 题目解析 P1428 小鱼比可爱 P1427 小鱼的数字游戏 P5727[深基5.例3]冰雹猜想 P1047 校门外的树 P5728[深基5.例5]旗鼓相当的对手 P5729[深基5.例7] ...
- 【OJ】洛谷分支结构题单题解锦集
题单简介 题目解析 P5710[深基3.例2]数的性质 P5711[深基3.例3]闰年判断 P5712[深基3.例4]Apples P5713[深基3.例5]洛谷团队系统 P5714[深基3.例7]肥 ...
最新文章
- 如何构建虚拟护士应用程序?
- memset函数使用详解
- pycharm以及flask的安装
- grails的controller和action那点事---远程调试groovy代码
- js实现鼠标拖拽功能基本思路
- python学习-装饰器(decorator)
- IOS 创建简单表视图
- es 在数据量数亿级别提高查询效率?
- 二叉搜索树的创建和比较
- Knockout应用开发指南 第二章:监控属性(Observables)
- java文字生成水印图片
- 10款值得收藏的网站数据实时分析工具
- iOS XCode 解决 error: Unable to load contents of file list
- cadence 旋转快捷键_cadence常用快捷键自己总结
- 前端登录注册页面、多方式登录功能、腾讯云短信发送功能二次封装(包)、发送短信接口
- 【Spring AOP】静态代理设计模式、Spring 动态代理开发详解、切入点详解(切入点表达式、切入点函数)
- “日本以太坊”Cardano的“区域自治”王国
- 计算机激光鼠标,光电鼠标和激光鼠标的区别
- DNS服务详解及正向解析与反向解析
- API 设计好文收集