SG函数

在介绍SG函数和SG定理之前我们先介绍介绍必胜点与必败点吧.

必胜点和必败点的概念:

       P点:必败点,换而言之,就是谁处于此位置,则在双方操作正确的情况下必败。

       N点:必胜点,处于此情况下,双方操作均正确的情况下必胜。

必胜点和必败点的性质:

        1、所有终结点是 必败点 P 。(我们以此为基本前提进行推理,换句话说,我们以此为假设)

        2、从任何必胜点N 操作,至少有一种方式可以进入必败点 P。

        3、无论如何操作,必败点P 都只能进入 必胜点 N。

我们研究必胜点和必败点的目的时间为题进行简化,有助于我们的分析。通常我们分析必胜点和必败点都是以终结点进行逆序分析。我们以hdu 1847 Good Luck in CET-4 Everybody!为例:

当 n = 0 时,显然为必败点,因为此时你已经无法进行操作了

当 n = 1 时,因为你一次就可以拿完所有牌,故此时为必胜点

当 n = 2 时,也是一次就可以拿完,故此时为必胜点

当 n = 3 时,要么就是剩一张要么剩两张,无论怎么取对方都将面对必胜点,故这一点为必败点。

以此类推,最后你就可以得到;

      n    :   0    1    2    3    4   5    6 ...

position:  P    N   N    P   N   N   P ...

你发现了什么没有,对,他们就是成有规律,使用了 P/N来分析,有没有觉得问题变简单了。

现在给你一个稍微复杂一点点的: hdu 2147 kiki's game

        现在我们就来介绍今天的主角吧。组合游戏的和通常是很复杂的,但是有一种新工具,可以使组合问题变得简单————SG函数和SG定理。

Sprague-Grundy定理(SG定理):

        游戏和的SG函数等于各个游戏SG函数的Nim和。这样就可以将每一个子游戏分而治之,从而简化了问题。而Bouton定理就是Sprague-Grundy定理在Nim游戏中的直接应用,因为单堆的Nim游戏 SG函数满足 SG(x) = x。对博弈不是很清楚的请参照http://www.cnblogs.com/ECJTUACM-873284962/p/6398385.html进行进一步理解。

SG函数:

        首先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。

        对于任意状态 x , 定义 SG(x) = mex(S),其中 S 是 x 后继状态的SG函数值的集合。如 x 有三个后继状态分别为 SG(a),SG(b),SG(c),那么SG(x) = mex{SG(a),SG(b),SG(c)}。 这样 集合S 的终态必然是空集,所以SG函数的终态为 SG(x) = 0,当且仅当 x 为必败点P时。

【实例】取石子问题

有1堆n个的石子,每次只能取{ 1, 3, 4 }个石子,先取完石子者胜利,那么各个数的SG值为多少?

SG[0]=0,f[]={1,3,4},

x=1 时,可以取走1 - f{1}个石子,剩余{0}个,所以 SG[1] = mex{ SG[0] }= mex{0} = 1;

x=2 时,可以取走2 - f{1}个石子,剩余{1}个,所以 SG[2] = mex{ SG[1] }= mex{1} = 0;

x=3 时,可以取走3 - f{1,3}个石子,剩余{2,0}个,所以 SG[3] = mex{SG[2],SG[0]} = mex{0,0} =1;

x=4 时,可以取走4-  f{1,3,4}个石子,剩余{3,1,0}个,所以 SG[4] = mex{SG[3],SG[1],SG[0]} = mex{1,1,0} = 2;

x=5 时,可以取走5 - f{1,3,4}个石子,剩余{4,2,1}个,所以SG[5] = mex{SG[4],SG[2],SG[1]} =mex{2,0,1} = 3;

以此类推.....

   x        0  1  2  3  4  5  6  7  8....

SG[x]    0  1  0  1  2  3  2  0  1....

由上述实例我们就可以得到SG函数值求解步骤,那么计算1~n的SG函数值步骤如下:

1、使用 数组f 将 可改变当前状态 的方式记录下来。

2、然后我们使用 另一个数组 将当前状态x 的后继状态标记。

3、最后模拟mex运算,也就是我们在标记值中 搜索 未被标记值 的最小值,将其赋值给SG(x)。

4、我们不断的重复 2 - 3 的步骤,就完成了 计算1~n 的函数值。

//f[N]:可改变当前状态的方式,N为方式的种类,f[N]要在getSG之前先预处理
//SG[]:0~n的SG函数值
//S[]:为x后继状态的集合
int f[N],SG[MAXN],S[MAXN];
void  getSG(int n){
    int i,j;
    memset(SG,0,sizeof(SG));
    //因为SG[0]始终等于0,所以i从1开始
    for(i = 1; i <= n; i++){
        //每一次都要将上一状态 的 后继集合 重置
        memset(S,0,sizeof(S));
        for(j = 0; f[j] <= i && j <= N; j++)
            S[SG[i-f[j]]] = 1;  //将后继状态的SG函数值进行标记
        for(j = 0;; j++) if(!S[j]){   //查询当前后继状态SG值中最小的非零值
            SG[i] = j;
            break;
        }
    }
}

 

 

SG函数:

首先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。

这一步应该是非常简单的,就是定义了新的运算为mex。

对于任意状态 x , 定义 SG(x) = mex(F),其中F 是 x 后继状态的SG函数值的集合(就是上述mex中的数值)。最后返回值(也就是SG(X))为0为必败点,不为零必胜点。

进一步解释一下F,就是题意中给出的可以移动的次数。举个例子来说,一堆石子,每次只能拿1,3,5,7个,那么S数组就是1,3,5,7。

假如说是在一个游戏中有多个石子堆该怎么办了。我们只需要把对每个石子堆进行sg函数的调用,将得到的所有的值进行异或。得出来的结果为0则该情形为必败态。否则为必胜态。

 

对于一个给定的有向无环图,定义关于图的每个顶点的Sprague-Grundy函数g如下:g(x)=mex{ g(y) | y是x的后继 },这里的g(x)即sg[x]
例如:取石子问题,有1堆n个的石子,每次只能取{1,3,4}个石子,先取完石子者胜利,那么各个数的SG值为多少?
sg[0]=0,f[]={1,3,4},
x=1时,可以取走1-f{1}个石子,剩余{0}个,mex{sg[0]}={0},故sg[1]=1;
x=2时,可以取走2-f{1}个石子,剩余{1}个,mex{sg[1]}={1},故sg[2]=0;
x=3时,可以取走3-f{1,3}个石子,剩余{2,0}个,mex{sg[2],sg[0]}={0,0},故sg[3]=1;
x=4时,可以取走4-f{1,3,4}个石子,剩余{3,1,0}个,mex{sg[3],sg[1],sg[0]}={1,1,0},故sg[4]=2;
x=5时,可以取走5-f{1,3,4}个石子,剩余{4,2,1}个,mex{sg[4],sg[2],sg[1]}={2,0,1},故sg[5]=3;
以此类推.....
x 0 1 2 3 4 5 6 7 8....
sg[x] 0 1 0 1 2 3 2 0 1....

计算从1-n范围内的SG值。
f(存储可以走的步数,f[0]表示可以有多少种走法)
f[]需要从小到大排序
1.可选步数为1~m的连续整数,直接取模即可,SG(x) = x % (m+1);
2.可选步数为任意步,SG(x) = x;
3.可选步数为一系列不连续的数,用GetSG()计算

 

例题:1 hdu 1847

Good Luck in CET-4 Everybody!

Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 14820    Accepted Submission(s): 9453

Problem Description

大学英语四级考试就要来临了,你是不是在紧张的复习?也许紧张得连短学期的ACM都没工夫练习了,反正我知道的Kiki和Cici都是如此。当然,作为在考场浸润了十几载的当代大学生,Kiki和Cici更懂得考前的放松,所谓“张弛有道”就是这个意思。这不,Kiki和Cici在每天晚上休息之前都要玩一会儿扑克牌以放松神经。
“升级”?“双扣”?“红五”?还是“斗地主”?
当然都不是!那多俗啊~
作为计算机学院的学生,Kiki和Cici打牌的时候可没忘记专业,她们打牌的规则是这样的:
1、  总共n张牌;
2、  双方轮流抓牌;
3、  每人每次抓牌的个数只能是2的幂次(即:1,2,4,8,16…)
4、  抓完牌,胜负结果也出来了:最后抓完牌的人为胜者;
假设Kiki和Cici都是足够聪明(其实不用假设,哪有不聪明的学生~),并且每次都是Kiki先抓牌,请问谁能赢呢?
当然,打牌无论谁赢都问题不大,重要的是马上到来的CET-4能有好的状态。

Good luck in CET-4 everybody!

Input

输入数据包含多个测试用例,每个测试用例占一行,包含一个整数n(1<=n<=1000)。

Output

如果Kiki能赢的话,请输出“Kiki”,否则请输出“Cici”,每个实例的输出占一行。

Sample Input

1 3

Sample Output

Kiki Cici

Author

lcy

分析 :这题就在一堆上做操作 所以直接判断异或  直接看sg值

#include<bits/stdc++.h>
using namespace std;
const int N=1010;//没堆最大的数量 
const int M=11;//取牌的方法数 
int sg[N];//sg[i]表示牌数为i时的sg函数值
int f[M];// 可取牌数的集合 
bool Hash[N];//标记一个数是否在mex{}集合中出现 
void getGS(int m)
{
	memset(sg,0,sizeof(sg));
	for(int i=1;i<N;i++){
		memset(Hash,false,sizeof(Hash));
		for(int j=0;j<m&&f[j]<=i;j++){
			Hash[sg[i-f[j]]]=true;
		}
		for(int j=0;j<N;j++){
			if(!Hash[j]){
				sg[i]=j;
				break;
			}
		}
	}
}
int main()
{
	int t=1;
	for(int i=0;i<M;i++,t<<=1){
		f[i]=t;
	}
	getGS(M);
	int n;
	while(scanf("%d",&n)!=EOF){
		//只有sg值为0时后手才赢 
		if(sg[n]) cout<<"Kiki"<<endl;
		else cout<<"Cici"<<endl;
	}
	return 0;
}

 

 

例题2:hdu 1848

Fibonacci again and again

Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 12577    Accepted Submission(s): 5481

 

Problem Description

任何一个大学生对菲波那契数列(Fibonacci numbers)应该都不会陌生,它是这样定义的:
F(1)=1;
F(2)=2;
F(n)=F(n-1)+F(n-2)(n>=3);
所以,1,2,3,5,8,13……就是菲波那契数列。
在HDOJ上有不少相关的题目,比如1005 Fibonacci again就是曾经的浙江省赛题。
今天,又一个关于Fibonacci的题目出现了,它是一个小游戏,定义如下:
1、  这是一个二人游戏;
2、  一共有3堆石子,数量分别是m, n, p个;
3、  两人轮流走;
4、  每走一步可以选择任意一堆石子,然后取走f个;
5、  f只能是菲波那契数列中的元素(即每次只能取1,2,3,5,8…等数量);
6、  最先取光所有石子的人为胜者;

假设双方都使用最优策略,请判断先手的人会赢还是后手的人会赢。

Input

输入数据包含多个测试用例,每个测试用例占一行,包含3个整数m,n,p(1<=m,n,p<=1000)。
m=n=p=0则表示输入结束。

Output

如果先手的人能赢,请输出“Fibo”,否则请输出“Nacci”,每个实例的输出占一行。

Sample Input

1 1 1

1 4 1

0 0 0

Sample Outpu

Fibo

Nacci

 分析:这题不是在一堆上做操作 那么就是异或了

#include <iostream>
#include <cstring>
#define MAXN 1010
#define MAXM 100
using namespace std;
int sg[MAXN], f[MAXM];
bool Hash[MAXN];

int getFib() {
    int i;
    f[0] = 1, f[1] = 2;
    for (i = 2; f[i] <= MAXN; i++) f[i] = f[i-1] + f[i-2];
    return i;
}
void getSG(int m) {
    memset(sg, 0, sizeof(sg));
    for (int i = 1; i < MAXN; i++) {
        memset(Hash, false, sizeof(Hash));
        for (int j = 0; j < m && f[j] <= i; j++)
            Hash[sg[i-f[j]]] = true;
        for (int j = 0; j < MAXN; j++) {
            if (!Hash[j]) {
                sg[i] = j;
                break;
            }
        }
    }
}

int main() {
    int a, b, c;
    getSG(getFib());
    while (cin >> a >> b >> c && (a || b || c)) {
        if (sg[a] ^ sg[b] ^ sg[c]) cout << "Fibo" << endl;
        else cout << "Nacci" << endl;
    }
    return 0;
}

 

例题三:hdu 1536

题意:m中取法 每种取法有一个值   m不大于100    n次询问 没次询问 有m堆石子 m不大于10000 问俩人操作后谁会赢

#include <iostream>
#include <algorithm>
#include <cstring>
#define MAXN 10010  // 最大堆数
#define MAXM 110    // 最多有MAXM种不同个数的取石子方法
using namespace std;
int f[MAXM];   // f为可取石子数的集合
int sg[MAXN];  // sg[i]表示石子数为i时的sg函数值
bool Hash[MAXN];  // 标记一个数是否在mex{}集合中出现
// 打表预处理sg数组
void getSG(int m)
{
    memset(sg, 0, sizeof(sg));
    for (int i = 1; i < MAXN; i++)
    {
        memset(Hash, false, sizeof(Hash));
        for (int j = 0; j < m && f[j] <= i; j++)
            Hash[sg[i-f[j]]] = true;  // 当前石子数为i,i-f[i]表示由i所能达到的石子数,将其sg值标记为已出现
        for (int j = 0; j < MAXN; j++)    // mex(minimal excludant)运算
        {
            if (!Hash[j])
            {
                sg[i] = j;
                break;
            }
        }
    }
}

int main()
{
    int n, m;
    while (cin >> m && m)
    {
        for (int i = 0; i < m; i++) cin >> f[i];
        sort(f, f + m);
        getSG(m);
        cin >> n;
        while (n--)
        {
            int num, sum = 0;
            cin >> num;
            for (int i = 0; i < num; i++)
            {
                int each;
                cin >> each;
                sum ^= sg[each];
            }
            if (sum) cout << 'W';
            else cout << 'L';
        }
        cout << endl;
    }
    return 0;
}

分析:这题就是多堆的问题。

 

另一种解法:dfs解法

一般DFS只在打表解决不了的情况下用,首选打表预处理。具体用法如下:

#include <iostream>
#include <algorithm>
#include <cstring>
#define MAXN 10010  // 最大堆数
#define MAXM 110    // 最多有MAXM种不同个数的取石子方法
using namespace std;
int m;
int f[MAXM];   // f为可取石子数的集合
int sg[MAXN];  // sg[i]表示石子数为i时的sg函数值
bool Hash[MAXN];  // 标记一个数是否在mex{}集合中出现
// 加一个dfs预处理sg数组,注意sg数组需要初始化为-1,而上面打表解法需要初始化为0
// 一般首选打表预处理,难以打表才用dfs
int SG_dfs(int x)
{
    if(sg[x]!=-1)
        return sg[x];
    bool vis[110];
    memset(vis,0,sizeof(vis));
    for(int i=0;i<m;i++)
    {
        if(x>=f[i])
        {
            SG_dfs(x-f[i]);
            vis[sg[x-f[i]]]=1;
        }
    }
    int e;
    for(int i=0;;i++)
        if(!vis[i])
        {
            e=i;
            break;
        }
    return sg[x]=e;
}

int main()
{
    while (cin >> m && m)
    {
        for (int i = 0; i < m; i++)
            cin >> f[i];
        sort(f, f + m);
        memset(sg,-1,sizeof(sg));
        int n;
        cin >> n;
        while (n--)
        {
            int num, sum = 0;
            cin >> num;
            for (int i = 0; i < num; i++)
            {
                int each;
                cin >> each;
                sum ^= SG_dfs(each);
            }
            if (sum) cout << 'W';
            else cout << 'L';
        }
        cout << endl;
    }
    return 0;
}

 

 

例题四:

原题连接:http://www.lightoj.com/volume_showproblem.php?problem=1315

A Hyper Knight is like a chess knight except it has some special moves that a regular knight cannot do. Alice and Bob are playing this game (you may wonder why they always play these games!). As always, they both alternate turns, play optimally and Alice starts first. For this game, there are 6 valid moves for a hyper knight, and they are shown in the following figure (circle shows the knight).

They are playing the game in an infinite chessboard where the upper left cell is (0, 0), the cell right to (0, 0) is (0, 1). There are some hyper knights in the board initially and in each turn a player selects a knight and gives a valid knight move as given. And the player who cannot make a valid move loses. Multiple knights can go to the same cell, but exactly one knight should be moved in each turn.

Now you are given the initial knight positions in the board, you have to find the winner of the game.

Input
Input starts with an integer T (≤ 200), denoting the number of test cases.

Each case starts with a line containing an integer n (1 ≤ n ≤ 1000) where n denotes the number of hyper knights. Each of the next n lines contains two integers x y (0 ≤ x, y < 500) denoting the position of a knight.

Output
For each case, print the case number and the name of the winning player.

Sample Input
Output for Sample Input
2

1

1 0

2

2 5

3 5

Case 1: Bob

Case 2: Alice

题意:给定一个棋盘,左上角为(0,0),棋盘中有多个骑士,每一个骑士只能按照图中的6种方式移动,两个人轮流移动棋盘中任意一个骑士,当轮到某一个人移动骑士时,棋盘中的骑士都已经不能移动了则判定为输,Alice先移动棋盘中的骑士,最后输出Alice和Bob谁赢谁输。

题解:典型的博弈SG函数,对每一个骑士的起始位置求SG值,然后将所有的SG值进行异或,如果其值为0,则先手必败,即Bob 获胜,否则先手必胜,Alice获胜。又由于这道题是二维的,因此每一个位置都是由x和y两个值来决定的,因此这道题无法使用打表的方式进行求SG值,需要时dfs的方式,SG初始化为-1即可。

/*memset(sg, -1, sizeof(sg)) 要放在while(t--)外面,不然会超时*/

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int next[6][2]={{-2,1},{1,-2},{-2,-1},{-1,-2},{-3,-1},{-1,-3}};
#define maxn 1005
int sg[maxn][maxn];
int dfs(int x,int y)
{
    int vis[105]={0}; //注意该数组是一维的 表示该点后继的sg值的情况
    if(sg[x][y]!=-1)
        return sg[x][y];
    for(int i=0;i<6;i++)
    {
        int nx=x+next[i][0];
        int ny=y+next[i][1];
        if(nx>=0&&ny>=0) //注意不能不加符号就判断 等于0也是算在内的
            vis[dfs(nx,ny)]=1; //因为可能走到的点的sg值还没有求过 所以要用递归深搜
            //之前写的非递归是因为之前的sg值都求过了 不用搜索也可以
    }
    for(int j=0;j<100;j++)
    if(!vis[j]) return sg[x][y]=j;
}
int main()
{
    memset(sg,-1,sizeof(sg)); //这里定义成-1 比0 好 因为有的就是0 if的时候0还要再算一次浪费时间
    int t,cas=1;
    cin>>t;
    while(t--)
    {
        int n,x,y,ans=0;
        cin>>n;
        for(int i=0;i<n;i++)
        {
            cin>>x>>y;
            ans^=dfs(x,y);
        }
        printf("Case %d: %s\n",cas++,ans?"Alice":"Bob");
    }
    return 0;
}


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值