Nim 博弈和 sg 函数

sg 函数

参考 通俗易懂
论文

几类经典的博弈问题

  1. 阶梯博弈: 只考虑奇数号楼梯Nim,若偶数楼梯只作容器,那么游戏变为Nim。题目
  2. 翻转硬币: 局面的SG值为局面中每个正面朝上的棋子单一存在时的SG值的异或和。题目
  3. Multi-SG游戏: 对于一个单一游戏,不同的方法可能会将其分成不同的多个单一游戏。每一方法对应的多个单一游戏的游戏的和即可以表示这种方法的NP状态。而这个单一游戏的SG函数即为未在所有方法的SG函数值中出现过的最小值。题目
  4. Anti-SG游戏和SJ定理 (在论文中有详细的论述和证明)
  5. Every-SG游戏: 对于Every-SG游戏先手必胜当且仅当单一游戏中最大的step为奇数。题目
  6. 树的删边游戏: 叶子节点的SG值为0;中间节点的SG值为它的所有子节点的SG值加1后的异或和。题目
  7. 无向图的删边游戏: 我们可以对无向图做如下改动:将图中的任意一个偶环缩成一个新点,任意一个奇环缩成一个新点加一个新边;所有连到原先环上的边全部改为与新点相连。这样的改动不会影响图的SG 值。

题目

hdu3032

题意

Multi-SG

给 n 堆石子, 每次有两步可选的操作,

  1. 将一堆 石子分成两堆
  2. 拿走一堆石子的任意个

最后不能行动的输。

分析

看完论文就会做了,

一堆石子数量为2 时 的后继有:0,1和(1,1),他们的SG值分别为0,1,0,所以sg(2) =2。

一堆石子数量为3 时的后继有:0、1、2、(1,2),他们的SG值分别为0、1、2、3,所以sg(3) = 4。

一堆石子数量为4 时的后继有:0、1、2、3、(1,3)和(2,2),他们的SG值分别为0,1,2,4,5,0,所以sg(4) = 3.

打表可以找到规律,见代码。

code

#include<cstdio>
#include<cstring>
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 5;
const int INF = 1e9;

int vis[30], sg[30];
void init()
{
    sg[0] = 0;
    for(int i = 0; i < 30; i++)
    {
        memset(vis, 0, sizeof vis);
        for(int j = 1; j < i; j++)
        {
            vis[sg[j] ^ sg[i - j]] = 1;
        }
        for(int j = 0; j < i; j++) vis[sg[j]] = 1;
        for(int j = 0; ; j++)
        {
            if(!vis[j])
            {
                sg[i] = j;
                printf("sg[%d]:%d
", i, j);
                break;
            }
        }
    }
}

int main()
{
    //init();
    int T;
    scanf("%d", &T);
    while(T--)
    {
        int n;
        scanf("%d", &n);
        int ans = 0;
        while(n--)
        {
            int x;
            scanf("%d", &x);
            if(x % 4 == 3) x++;
            else if(x % 4 == 0) x--;
            ans ^= x;
        }
        if(ans) puts("Alice");
        else puts("Bob");
    }
    return 0;
}

hdu3595

题意

一共有n个游戏,每一个游戏有两堆石子,一次移动可以从大的那堆石子里拿小的那堆石子的整数倍的石子。只要是可以操作的游戏都要进行操作,不能进行操作的人负。

分析

Every-SG游戏,对于Every-SG游戏先手必胜当且仅当单一游戏中最大的step为奇数,通过dfs找到必败态和必胜态的转移,对于必胜态,尽可能的慢结束游戏,对于必败态尽可能快的结束游戏。

code

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int INF = 1e9;
const int MAXN = 1e3 + 10;

int dp[MAXN][MAXN];

int dfs(int p, int q)
{
    if(p > q) swap(p, q);
    if(!p) return dp[p][q] = 0;
    int k = q / p, tp = q % p, tq = p;
    int flag = dfs(tp, tq);
    if(k == 1)
    {
        dp[p][q] = dp[tp][tq] + 1;
        return flag ^ 1;
    }
    else
    {
        if(!flag) dp[p][q] = dp[tp][tq] + 1;
        else dp[p][q] = dp[tp][tq] + 2; // 后面存在一个不是终止态的必败态,这样至少会增加2步。
        return 1;
    }
}

int main()
{
    int N;
    memset(dp, -1, sizeof dp);
    while(~scanf("%d", &N))
    {
        int ans = 0;
        while(N--)
        {
            int p, q;
            scanf("%d%d", &p, &q);
            dfs(p, q);
            ans = max(ans, dp[min(p, q)][max(p, q)]);
        }
        puts((ans & 1) ? "MM" : "GG");
    }
    return 0;
}

poj3710

题意

  1. 有 N个局部联通的图。
  2. Harry 和 Sally轮流从图中删边,删去一条边后,不与根节点相连的部分将被移走。Sally为先手。
  3. 图是通过从基础树中加一些边得到的。
  4. 所有形成的环保证不共用边,且只与基础树有一个公共点。
  5. 谁无法移动谁输。

分析

树的删边游戏,参见论文。
参考代码

code

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long ll;
const int INF = 1e9;
const int MAXN = 1e3 + 10;

int next[MAXN], head[MAXN], to[MAXN]; // 前向星
int vis[MAXN]; // 标记数组
// v[i]: 这里就解决了冲突,因为每添加一条边,其实是加入两条边,但是题目本身会存在重边的情况,注意到cnt初始化为1,第一条边是从2开始的,
//       这样每经过一条边,会把相应的v[i]和v[i^1]置为1,正好使得每条边只会被访问一次而不和题目的重边冲突。
int v[MAXN];
int w[MAXN];   // 标记环
int s[MAXN];   // 栈
int cnt, top;
int add(int u, int v)
{
    to[++cnt] = v;
    next[cnt] = head[u];
    head[u] = cnt;
}

int dfs(int x)
{
    vis[x] = 1;
    s[++top] = x;
    int ans = 0;
    for(int i = head[x]; i; i = next[i])
    {
        if(!v[i])
        {
            v[i] = 1; v[i ^ 1] = 1;
            int tmp = 0;
            if(!vis[to[i]]) tmp = dfs(to[i]) + 1;
            else
            {
                int q = s[top--];
                while(q != to[i])
                {
                    w[q] = 1;
                    q = s[top--];
                }
                top++;
                return 1;
            }
            if(w[to[i]]) ans ^= (tmp & 1); // 本来要判断环的奇偶性,回溯的时候是0,1交替,010101...
            else ans ^= tmp;
        }
    }
    return ans;
}

int main()
{
    int n;
    while(~scanf("%d", &n))
    {
        int ans = 0;
        while(n--)
        {
            memset(next, 0, sizeof next);
            memset(head, 0, sizeof head);
            memset(vis, 0, sizeof vis);
            memset(v, 0, sizeof v);
            memset(w, 0, sizeof w);
            int m, k; cnt = 1; top = 0;
            scanf("%d%d", &m, &k);
            for(int i = 0; i < k; i++)
            {
                int x, y;
                scanf("%d%d", &x, &y);
                add(x, y);
                add(y, x);
            }
            ans ^= dfs(1);
        }
        if(ans) puts("Sally");
        else puts("Harry");
    }
    return 0;
}
原文地址:https://www.cnblogs.com/ftae/p/6791252.html