博弈论题目
一、【P1290 欧几里德的游戏】
https://www.luogu.com.cn/problem/P1290
对于数对 ( a , b ) , 不妨规定 a < b
首先确定终局条件 : a == 0
然后考虑面对 ( a , b ) 时,本质上只有两种选择:
1.得到 ( a , b%a )
2.得到 ( a , b%a + b )
而 2 定会去到 1 ,所以两种局面胜负性相反。
故当两种都可到达,则必胜;否则只能到 1 则需要 1 为必败才可取胜,反之亦然。
#include<iostream>
using namespace std;
int t;
int a,b;
bool dfs(int a,int b){
if(a>b) swap(a,b);
if(a==0) return 0;
if(b/a==1) return 1^dfs(a,b%a);
else return 1;
}
int main(){
cin>>t;
while(t--){
cin>>a>>b;
if(a>b) swap(a,b);
if(dfs(a,b)) cout<<"Stan wins\n";
else cout<<"Ollie wins\n";
}
return 0;
}
二、【P1288 取数游戏II】
https://www.luogu.com.cn/problem/P1288
发现移动时直接变为 0 即可,否则对方原路返回并变为 0 只会再回到原来局面的基础上变得更糟(一边为 0 ,没有了选择 )
故游戏过程为从起点出发,向左/右一直移动,碰到 0 为止
所以检查起点距离 0 边距离的奇偶性,两条路只要存在奇数长度(参考 1 )便必胜,否则必败
#include<iostream>
#define maxn 20
using namespace std;
int n;
int a[maxn];
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
int cntl=0,cntr=0;
for(int j=1;j<=n;j++){
if(a[j]==0) break;
cntr++;
}
for(int j=n;j>=1;j--){
if(a[j]==0) break;
cntl++;
}
if(cntl%2==1 || cntr%2==1) cout<<"YES\n";
else cout<<"NO\n";
return 0;
}
三、【CF917B MADMAX】
https://www.luogu.com.cn/problem/CF917B
描述一个局面需要双方所在位置以及目前的字母限制,这样 n ^ 2 * 26 的局面数可以直接dfs解决。
#include<iostream>
#include<vector>
#include<cstring>
#define maxn 105
using namespace std;
int n,m;
struct edge{
int v;
char w;
edge(int v=0,int w=0):v(v),w(w){}
};
vector<edge> e[maxn];
int sg[maxn][maxn][30];
bool dfs(int i,int j,int rs){
//cout<<i<<" "<<j<<" "<<rs<<endl;
if(sg[i][j][rs]!=-1) return sg[i][j][rs];
int v,r;
for(int k=0;k<e[i].size();k++){
v=e[i][k].v;
r=e[i][k].w;
if(r<rs) continue;
if(dfs(j,v,r)==0) return sg[i][j][rs]=1;
}
return sg[i][j][rs]=0;
}
int main(){
cin>>n>>m;
int u,v;
char c;
for(int i=1;i<=m;i++){
cin>>u>>v>>c;
e[u].push_back(edge(v,c-'a'));
}
memset(sg,-1,sizeof(sg));
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(dfs(i,j,0)==1) cout<<"A";
else cout<<"B";
}
cout<<endl;
}
return 0;
}
四、【P2575 高手过招】
https://www.luogu.com.cn/problem/P2575
可将每一行单独考虑,状压的方式计算每个局面的 sg 值,将每行对应的异或起来即可,理论的直接应用。。。
使用记忆化搜索,每种局面去到的下一个局面也是固定的
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
int t;
int n;
int m;
int sg[(1<<21)];
int dfs(int x){
if(sg[x]!=-1) return sg[x];
bool vis[30];
memset(vis,0,sizeof(vis));
int lst0=-1;
for(int j=20;j>=1;j--){
if((x&(1<<j))==0) {
lst0=j;
continue;
}
else {
if(lst0==-1) continue;
vis[dfs(x^(1<<j)^(1<<lst0))]=1;
}
}
for(int i=0;i<20;i++){
if(!vis[i]) return sg[x]=i;
}
}
int main(){
cin>>t;
memset(sg,-1,sizeof(sg));
while(t--){
cin>>n;
int ans=0;
for(int i=1;i<=n;i++){
int k,x=0;
cin>>k;
while(k--){
int j;
cin>>j;
x|=(1<<j);
}
ans^=dfs(x);
}
if(ans==0) cout<<"NO\n";
else cout<<"YES\n";
}
return 0;
}
五、【Acwing 219. 剪纸游戏】
https://www.acwing.com/problem/content/221/
每一个最基础的局面通过 ( x , y ) 来表示,即一张 x * y 的纸。
依然是 dfs 求 sg 值。对于一次剪出来的两片 (x1 , y1 ) 与 (x2 , y2 ) 应通过 或 的方式合并;一张纸不同的剪法,通过 mex 来合并。
#include<iostream>
#include<cstring>
using namespace std;
int n,m;
int sg[205][205];
int dfs(int x,int y){
if(sg[x][y]!=-1) return sg[x][y];
if(sg[y][x]!=-1) return sg[x][y]=sg[y][x];
if(x==1 && y==1) return sg[x][y]=0;
bool vis[400];
memset(vis,0,sizeof(vis));
for(int i=2;i<x-1;i++){
vis[dfs(i,y)^dfs(x-i,y)]=1;
}
for(int j=2;j<y-1;j++){
vis[dfs(x,j)^dfs(x,y-j)]=1;
}
for(int i=0;i<400;i++){
if(vis[i]==0) return sg[x][y]=i;
}
}
int main(){
memset(sg,-1,sizeof(sg));
while(cin>>n>>m){
if(dfs(n,m)!=0) cout<<"WIN\n";
else cout<<"LOSE\n";
}
return 0;
}
六、【AcWing 235. 魔法珠】
https://www.acwing.com/problem/content/description/237/
与上一题 剪纸游戏 基本一致,这里剪开两张纸对应分裂,不同剪法对应选择不同的进行分裂,终局为全 1 必败。
同样通过 dfs 合并 sg 值来实现
#include<iostream>
#include<cstring>
#define maxn 105
#define maxa 1005
using namespace std;
int n;
int sg[maxa];
int dfs(int x){
if(x==1) return sg[x]=0;
int d[40];
int top=0;
d[++top]=1;
for(int i=2;i*i<=x;i++){
if(x%i==0){
d[++top]=i;
if(i*i!=x) d[++top]=x/i;
}
}
int ans=0;
for(int i=1;i<=top;i++){
ans^=dfs(d[i]);
}
bool vis[40];
memset(vis,0,sizeof(vis));
for(int i=1;i<=top;i++){
vis[ans^dfs(d[i])]=1;
}
for(int i=0;i<40;i++){
if(vis[i]==0) return sg[x]=i;
}
}
int main(){
while(cin>>n){
int ans=0;
for(int i=1;i<=n;i++){
int x;
cin>>x;
ans^=dfs(x);
}
if(ans==0) cout<<"rainbow\n";
else cout<<"freda\n";
}
return 0;
}
七、【P1199 三国游戏】
https://www.luogu.com.cn/problem/P1199
将组合按照默契值排序之后,对于这个有序排列:
任意一个武将第一次出现的组合一定拿不到(当拿到其中一个后,立刻被抢走另一个……),而也正因为这样,第二次出现定能拿到,所以所有武将中第二次出现的最靠前的即是答案。
而考虑胜负,在为第二次出现做铺垫,选取第一次出现的组合时,电脑拿到的定也是第一次出现(因为你选择的是第二次出现最早的),所以不用担心电脑拿到第一次出现的组合。(大概是这样,需要想一想)
#include<iostream>
#define maxn 505
#include<algorithm>
using namespace std;
int n;
bool app[maxn];
struct partner{
int x,y,w;
partner(int x=0,int y=0,int w=0):x(x),y(y),w(w){}
bool operator < (const partner &t)const{
return w>t.w;
}
};
partner a[maxn*maxn];
int top=0;
int main(){
cin>>n;
int w;
for(int i=1;i<=n;i++){
for(int j=i+1;j<=n;j++){
cin>>w;
a[++top]=partner(i,j,w);
}
}
sort(a+1,a+1+top);
for(int i=1;i<=top;i++){
if(app[ a[i].x ] || app[ a[i].y ]) {
cout<<1<<"\n"<<a[i].w;
return 0;
}
app[ a[i].x ] = app[ a[i].y ] = 1;
}
return 0;
}
八、【AcWing 236. 格鲁吉亚和鲍勃】
https://www.acwing.com/problem/content/238/
通过神奇的转化:将石子按顺序两两配对,只需考虑每对之间的距离(因为每对可以通过两人的操作同时左移),转化为一个nim游戏的局面(n堆石子,每次从一堆取,取完为止),which 有一个特性,可通过每个值直接异或起来是否为零来判断先手胜负。
注意输入并不有序 > _ <
#include<iostream>
#define maxn 1005
#include<algorithm>
using namespace std;
int t;
int n;
int a[maxn];
int main(){
cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
if(n%2==1) a[++n]=0;
sort(a+1,a+1+n);
int ans=0;
for(int i=2;i<=n;i+=2){
ans^=a[i]-a[i-1]-1;
}
if(ans) cout<<"Georgia will win\n";
else cout<<"Bob will win\n";
}
return 0;
}