为了此题,三天狂学SG,博弈饶我啊!!!
说一下自己对于SG的理解.
1.
首先对于SG(C)=SG(A+B)=SG(A)^SG(B)这个定理知道就好了,具体怎么证我也不想知道
这个的意思就是说对于一个游戏如果我们可以把它分成两个游戏,互相独立,那么我们想求当前这个游戏的SG值,就可以求出其子游戏的SG然后异或得到。
就如同这道例题POJ 3537 Crosses and Crosses
在这道题里面我们对一个空格画×之后那么我们肯定不能再这个格左右相邻的两个格划×,因为这样对手直接就可以获胜。
也就是我们只能选择绿色框之外的
那么我们就把一个完整的问题拆成了两个也就是我们选择左边进行和选择右边进行,
那么我们就
for(int i = 1; i <= x ; i++){
int xx=SG_DFS(max(0,i-3));左边的长度
int yy=SG_DFS(max(0,x-i-2));右边的长度
vis[xx^yy]=1;
}
2.
其次我们利用Mex函数的原理是,如果一个状态的下一个状态都是必胜态,那么当前这个状态就是必败态,反之如果存在一个下个状态是必败态,那么我们就一定是必胜态,因为我们总会选这个最佳状态。所以转换成数就是0为必败,那么就是说如果他的子代都是大于0的(都是必败态),那么按照vis找第一个值的话就是0,也就是败态。如果说他的子代存在败态,那么就是说有0,那么他的Mexi就一定不是零,也就是必胜态。
所以说我们找到他的每一个子游戏的情况,然后标记一下,求出它的Mex是正确的。
!!注意这里的子游戏和1里面的子游戏不一样,1里面的完全分成两个游戏,而这里面的子游戏是每一种情况。所以说上面的代码里面xx,yy对应当前情况下把它拆分成的两个游戏,而他们异或之后得到的合成的游戏只是我们选择的一种情况。
对于求SG函数大致分为两种方法
1.直接打表求出所求情况
(vis设成bool类型,不然有的题会超时)
#include <iostream>
#include <iostream>
using namespace std;
const int maxn = 1e5+10;
int sg[maxn],f[maxn];
int vis[maxn];
void init(){
for(int i = 1 ; i <= n ; i++){
memset(vis,0,sizeof vis);
for(int j = 1; j <= tot ; j++){
if(f[j]>i)break;
vis[i-f[j]]=1;
}
for(int j=0;;j++){
if(!vis[j]){
sg[i]=i;break;
}
}
}
}
int main()
{
for(int i = 1; i <= n ; i++){
scanf("%d",&f[i]);
}sort(f+1,f+n+1);
tot=unique(f+1,f+n+1)-f-1;
init();
}
2、dfs递归求解
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 1e5+10;
int sg[maxn];
int SG_DFS(int x){
if(sg[x]!=-1) return sg[x];
bool vis[maxn];memset(vis,0,sizeof vis);
for(int i = 1; i <= n ; i++){
if(f[i]>x) break;
vis[SG_DFS(x-f[i])]=1;
}
for(int i=0;;i++){
if(!vis[i])
return sg[x]=i;
}
}
int main()
{
memset(sg,0,sizeof sg);
sg[0]=0;
for(int i = 1; i <= n ; i++){
scanf("%d",f[i]);
}sort(f+1,f+n+1);
for(int i =1 ; i <= m ; i++){
int x;scanf("%d",&x);
sum^=SG_DFS(x);
}
}
3.然后对于开销比较大的,可以考虑把vis数组改成set。
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 1e5+10;
int sg[maxn];
int SG_DFS(int x){
if(sg[x]!=-1) return sg[x];
set<int>hrs;
for(int i = 1; i <= n ; i++){
if(f[i]>x) break;
hrs.insert(SG_DFS(x-f[i]));
}
int Mex=0;
for(auto &it:hrs){
if(it!=Mex) break;
Mex++;
}
return sg[x]=Mex;
}
int main()
{
memset(sg,0,sizeof sg);
sg[0]=0;
for(int i = 1; i <= n ; i++){
scanf("%d",f[i]);
}sort(f+1,f+n+1);
for(int i =1 ; i <= m ; i++){
int x;scanf("%d",&x);
sum^=SG_DFS(x);
}
}
然后我们来讲一下这道题吧
其实这道题就是一个爆搜,做多了之后这道题的思路其实并没有很多题那么精妙,但是爆搜的处理方法确实佩服。
首先我们可以用一个vetor去存我们26个字符有多少个,这里需要知道的是对于一种状态的确立要求是:其字母的种数或者是相同的的字母但数量不同于其他字符串。因为我们的操作是拿走一个或两个类型不同的字符,也就之和字符类型数,以及一种字符的数量有关了。
那么对于aabb和ccdd我们就可以视为一种状态都是有两个均是两个字符的。
然后可以先排个序(降序),这样有利于后面进行两种操作。
其次我们对这种状态标记的话,我们可以把当前的vector转化成其哈希值!,然后用一个map去存,注意这里要用unordered_map,因为我们只标记就好了,不用排序,不然会超时
ull get_Hash(VI v){
ull res=0;
for(auto &it:v){
if(!it) break;
res=res*Hash+it;
}return res;
}
然后我们对于标记的话可以用一个set,这样可以自动排序,帮助我们确立第一个不存在的值还可以节省空间。
int Mex=0;
for(auto &it:Jq){
if(it!=Mex) break;
Mex++;
}
return cp[Hrs]=Mex;
然后就是完整代码
#include <iostream>
#include <cstdio>
#include <unordered_map>
#include <set>
#include <vector>
#include <algorithm>
using namespace std;
const int Hash = 131;
typedef unsigned long long ull;
typedef vector<int> VI;
char s[40];
unordered_map<ull,int> cp;
ull get_Hash(VI v){
ull res=0;
for(auto &it:v){
if(!it) break;
res=res*Hash+it;
}return res;
}
int SG_DFS(VI v){
sort(v.rbegin(),v.rend());
ull Hrs=get_Hash(v);
if(cp.count(Hrs)) return cp[Hrs];
if(!Hrs) return 0;
set<int>Jq;
for(int i = 0 ; i < v.size() ; i++){
if(v[i]){
v[i]--;
Jq.insert(SG_DFS(v));
v[i]++;
}
}
for(int i = 0 ; i < v.size() ; i++){
if(!v[i]) break;
for(int j = i+1 ; j < v.size() ; j++){
if(!v[j]) break;
v[i]--,v[j]--;
Jq.insert(SG_DFS(v));
v[i]++,v[j]++;
}
}
int Mex=0;
for(auto &it:Jq){
if(it!=Mex) break;
Mex++;
}
return cp[Hrs]=Mex;
}
int main()
{
int T;scanf("%d",&T);
while(T--){
int n;scanf("%d",&n);
int sum=0;
for(int i = 0 ; i < n ;++i){
VI v(26);
scanf("%s",s);
for(int j = 0 ;s[j]; j++){
v[s[j]-'a']++;
}
sum^=SG_DFS(v);
}
if(sum) printf("Alice\n");
else printf("Bob\n");
}
return 0;
}