uva 10561 Treblecross 【博弈】【SG函数打表】【先手第一步】【子游戏】

题目链接https://vjudge.net/problem/UVA-10561

 • 题目大意:

给定一行只含'.'和'X'的字符串且没有连续三个“XXX”的情况,两个玩家依次将'.'改成‘X’,

当一方操作后出现“XXX”的情况时,该方胜利。问先手是否能获胜?

若能,输出所有先手第一步可以操作的位置。

• 分析:

 一步胜利的情况:

首先要把一步就能取胜的情况单独处理,即“.....XX.....”和“.....X.X.......”,

一旦存在这种情况就不需要考虑一般的情况了,输出所有获胜位置即可。

这里要注意边界位置的处理。

对于一般情况下:

枚举每两个X之间的区间,并求其SG值,可以对1~N长度的SG值在线打表。

求出其区间长度作为下标访问其SG值,整个游戏的SG值ns^=该子游戏的SG值,

并记录下该区间的信息(起始位置和长度),用来后续求先手第一步操作的位置。

注意:

对于全为'.'的情况需要特判。

对X枚举完后,右边界可能会剩下的全'.'区间。(只枚举到了最右边的X,右边可能还有位置)

若为先手胜利,需对之前记录下的每个区间(子游戏)的每个位置进行枚举,

相当于模拟先手第一步的位置,当该位置变为X时使得整个游戏的SG值ns==0时,

留给对手的局面就是ns==0的必败点,这种位置就是我们所要求的位置。

• 求先手第一步的具体原理:

 •AC代码:

#include<algorithm>
#include<math.h> 
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string> 
#include<vector>
#include<map>
#define ll long long 
#define INF 0x3f3f3f3f
#define	For(i,a,b) for(int i=a;i<b;i++)
#define	Forr(i,a,b) for(int i=a;i<=b;i++)
#define mkp make_pair
#define lm(x) 1<<(x) 
using namespace std;
#define N 203
int n,a[N],st[N],cnt,sg[N],vis[N];
string s;
vector<int>ret; 
void go(){ //对1~N的SG值打表 
	int t1,t2;
	sg[1]=sg[2]=sg[3]=1;
	Forr(i,4,N){
		memset(vis,0,sizeof(vis));
		Forr(j,1,i/2+1){//i为区间长度 
			t1=(j-3<=0)?0:sg[j-3];//左侧SG 
			t2=(i-j-2<=0)?0:sg[i-j-2];//右侧SG 
			vis[t1^t2]=1;
		}
		For(j,0,N)
			if(vis[j]==0){
				sg[i]=j;
				break;
			}
	}
}
void check(int st,int len,int v){//check某个区间 
	int i,t1,t2;
	memset(vis,0,sizeof(vis));
	Forr(i,1,len){//在该区间内枚举所有位置 
		t1=(i-3<=0)?0:sg[i-3];//左侧SG 
		t2=(len-i-2<=0)?0:sg[len-i-2];//右侧SG 
		if(v==(t1^t2))//此时将sg[len]换为t1^t2即可使ns==0 
			ret.push_back(st+i);
	}
}

int main()
{ios::sync_with_stdio(false);
	int t;cin>>t;
	go();
	while(t--){
		cin>>s;
		int sz=s.size();
		int cnt=0,pre=-1,flg=0;//pre表示距离当前x最近的前一个X 
		ret.clear();
		vector<pair<int,int>>sub;//记录区间(每一堆/每一个子游戏),first是区间开头位置,second为区间长度 
		int ns=0;  //nim-sum
		For(i,0,sz){
			if(s[i]=='X'){
				if(pre!=-1&&i-pre==1){//相邻XX 
					if(pre-1>=0&&s[pre-1]=='.')//.XX
						ret.push_back(pre-1);
					if(i+1<sz&&s[i+1]=='.')//XX.
						ret.push_back(i+1);
					flg=1;
				}
				else if(pre!=-1&&i-pre==2){//X.X 
					ret.push_back(i-1);
					flg=1;
				}
				if(flg){//一步胜利 
					pre=i;
					continue;//不能break因为要记录胜利位置 
				}
				if(pre==-1){//X位于左边界三个位置时 
					if(i>2){
						sub.push_back(mkp(0,i-2));//这种情况下区间一定从0开始 
						ns^=sg[i-2];
					}
				}else if(i-pre>5){//x..【这里即为此处所讨论的区间】..x
					sub.push_back(mkp(pre+3,i-pre-5));
					ns^=sg[i-pre-5];
				}
				pre=i;//更新上一个X的位置 
			}
		}
		if(pre==-1){//全.无X  
			sub.push_back(mkp(0,sz));
			ns^=sg[sz];
		}
		else if(pre<sz-1&&sz-pre>3){//特判右边界可能剩下的全.区间 
			sub.push_back(mkp(pre+3,sz-pre-3));
			ns^=sg[sz-pre-3];
		}
		if(flg){//一步胜利 
			cout<<"WINNING"<<endl;
			For(i,0,ret.size()-1)
				cout<<ret[i]+1<<' ';//s从0开始,所以+1 
			cout<<ret[ret.size()-1]+1<<endl;
		}else if(ns==0){//nim-sum==0先手必败 
			cout<<"LOSING"<<endl<<endl;
			
		}else{	
			cout<<"WINNING"<<endl;
			
			//在所记录的区间(子游戏)中找出符合要求的位置 
			
			ret.clear();
			For(i,0,sub.size())//遍历所记录的区间,找出能获胜的位置 
				check(sub[i].first,sub[i].second,ns^sg[sub[i].second]);
			For(i,0,ret.size()-1)
				cout<<ret[i]<<' ';
			cout<<ret[ret.size()-1]<<endl;
			
		}
	}
	return 0;
} 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值