Nim 博弈和 sg 函数
sg 函数
几类经典的博弈问题
- 阶梯博弈: 只考虑奇数号楼梯Nim,若偶数楼梯只作容器,那么游戏变为Nim。题目
- 翻转硬币: 局面的SG值为局面中每个正面朝上的棋子单一存在时的SG值的异或和。题目
- Multi-SG游戏: 对于一个单一游戏,不同的方法可能会将其分成不同的多个单一游戏。每一方法对应的多个单一游戏的游戏的和即可以表示这种方法的NP状态。而这个单一游戏的SG函数即为未在所有方法的SG函数值中出现过的最小值。题目
- Anti-SG游戏和SJ定理 (在论文中有详细的论述和证明)
- Every-SG游戏: 对于Every-SG游戏先手必胜当且仅当单一游戏中最大的step为奇数。题目
- 树的删边游戏: 叶子节点的SG值为0;中间节点的SG值为它的所有子节点的SG值加1后的异或和。题目
- 无向图的删边游戏: 我们可以对无向图做如下改动:将图中的任意一个偶环缩成一个新点,任意一个奇环缩成一个新点加一个新边;所有连到原先环上的边全部改为与新点相连。这样的改动不会影响图的SG 值。
题目
hdu3032
题意
Multi-SG
给 n 堆石子, 每次有两步可选的操作,
- 将一堆 石子分成两堆
- 拿走一堆石子的任意个
最后不能行动的输。
分析
看完论文就会做了,
一堆石子数量为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\n", 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
题意
- 有 N个局部联通的图。
- Harry 和 Sally轮流从图中删边,删去一条边后,不与根节点相连的部分将被移走。Sally为先手。
- 图是通过从基础树中加一些边得到的。
- 所有形成的环保证不共用边,且只与基础树有一个公共点。
- 谁无法移动谁输。
分析
树的删边游戏,参见论文。
参考代码
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;
}