博弈论
一. 抽象表示
将组合游戏中的每一个状态抽象成图中的一个点,将每一步决策抽象为图中的一条边。这样,对于组合游戏的每一次博弈,我们都可以将其抽象成游戏图中的一条从某一顶点到出度为0的路径
二. P, N状态
一个先手胜状态被认为是一个N-状态(因为下一个玩家即将获胜),一个后手胜状态被认为是一个P-状态(因为前一个玩家即将获胜)。
P-和N-状态归纳性地描述如下:
一个点v是P状态当且仅当它的所有后继为N状态
一个点v是N状态当且仅当它的一些后继为P状态
三. SG函数
给定一个有限子集S
⊂
\subset
⊂N,令mexS(最小排斥值)为没有出现在S中的最小自然数
现在给定一个游戏图G = (V, E),则有
g
(
v
)
=
m
e
x
{
g
(
w
)
∣
(
v
,
w
)
∈
E
}
g(v) = mex\lbrace{g(w) | (v, w) {\in} E}\rbrace
g(v)=mex{g(w)∣(v,w)∈E}
性质:
对于任意的局面,若它的SG值为0,则它任何一个后继局面的SG值不为0
对于任意的局面,若它的SG值不为0,那么它一定有一个后继局面的SG值为0
在我们每次只能进行一步操作的情况下,对于任何的游戏的和,我们若将其中的任一单一 SG-组合游戏换成数目为它的SG 值的一堆石子,该单一 SG-组合游戏的规则变成取石子游戏的规则(可以任意取,甚至取完),则游戏的和的胜负情况不变。
四. Anti-SG游戏
定义:决策集合为空的游戏者赢,即取走最后一个石子的人败
SJ定理:对于任意一个 Anti-SG 游戏,如果我们规定当局面中所有的单一游戏的 SG 值为 0 时,游戏结束,则先手必胜当且仅当:
- 游戏的 SG 函数不为 0 且游戏中某个单一游戏的 SG 函数大于 1
- 游戏的 SG 函数为 0 且游戏中没有单一游戏的 SG 函数大于 1。
五. Multi-SG游戏
定义:在符合拓扑原则的前提下,一个单一游戏的后继可以为多个单一游戏,即可以将一堆石子分为多堆石子
先从Multi-Nim开始:有n堆石子,两个人可以从任意一堆石子中拿任意多个石子(不能不拿)或把一堆数量不少于2石子分为两堆不为空的石子,没法拿的人失败。问谁会胜利。这有一神奇定义:
S G ( x ) = { x − 1 , x m o d 4 = 0 x , x m o d 4 = 1 o r 2 x + 1 , x m o d 4 = 3 SG(x) = \begin{cases} x-1, & \text{$x\;mod\;4 = 0$} \\ x, & \text{$x\;mod\;4 = 1\,or\,2$} \\ x + 1, & \text{$x\;mod\;4 = 3$} \end{cases} SG(x)=⎩ ⎨ ⎧x−1,x,x+1,xmod4=0xmod4=1or2xmod4=3
六. Every-SG游戏
定义:对于还没有结束的单一游戏,游戏者必须对该游戏进行一步决策,即每一个可以移动的棋子都要移动
有如下解法:在通过拓扑关系计算某一个状态点的 SG 函数时(事实上,我们只需要计算该状态点的 SG 值是否为 0)对于 SG 值为 0 的点,我们需要知道最快几步能将游戏带入终止状态,对于 SG 值不为 0 的点,我们需要知道最慢几步游戏会被带入终止状态,我们用step函数来表示这个值。
s t e p ( v ) = { 0 , v 为终止状态 m a x ( s t e p ( u ) ) + 1 , S G ( v ) > 0 ⋂ u 为 v 的后继状态 ⋂ S G ( u ) = 0 m i n ( s t e p ( u ) ) + 1 , S G ( v ) = 0 ⋂ u 为 v 的后继状态 step(v) = \begin{cases} 0, & \text{$v为终止状态$} \\ max(step(u))+1, & \text{$SG(v) > 0 \bigcap u为v的后继状态 \bigcap SG(u) = 0$} \\ min(step(u))+1, & \text{$SG(v) = 0 \bigcap u为v的后继状态$} \end{cases} step(v)=⎩ ⎨ ⎧0,max(step(u))+1,min(step(u))+1,v为终止状态SG(v)>0⋂u为v的后继状态⋂SG(u)=0SG(v)=0⋂u为v的后继状态
定理:对于Every-SG游戏先手必胜当且仅当单一游戏中最大的step为奇数
七. 翻硬币游戏
一般规则如下:
- N 枚硬币排成一排,有的正面朝上,有的反面朝上。我们从左开始对硬币按 1 到 N 编号。
- 游戏者根据某些约束翻硬币(如:每次只能翻一或两枚,或者每次只能翻连续的几枚),但他所翻动的硬币中,最右边的必须是从正面翻到反面。
- 谁不能翻谁输。
结论:局面的SG值为局面中每个正面朝上的棋子单一存在时的SG值的异或和
八. 树的删边游戏
规则如下:
- 给出一个有 N 个点的树,有一个点作为树的根节点。
- 游戏者轮流从树中删去边,删去一条边后,不与根节点相连的部分将被移走。
- 谁无路可走谁输。
定理:叶子节点的SG值为0;中间节点的SG值为它的所有子节点的SG值加1后的异或和
九. 无向图删边游戏
1. Christmas Game
题目大意:
- 有 N 个局部联通的图。
- Harry 和 Sally 轮流从图中删边,删去一条边后,不与根节点相连的部分将被移走。Sally 为先手。
- 图是通过从基础树中加一些边得到的。
- 所有形成的环保证不共用边,且只与基础树有一个公共点。
- 谁无路可走谁输
有如下性质:
- 对于长度为奇数的环,去掉其中任意一个边之后,剩下的两个链长度同奇偶,异或之后的 SG 值不可能为奇数,所以它的 SG 值为 1;
- 对于长度为偶数的环,去掉其中任意一个边之后,剩下的两个链长度异奇偶,异或之后的 SG 值不可能为 0,所以它的 SG 值为 0;
所以我们可以去掉所有的偶环,将所有的奇环变为长短为1的链,这样就改造成了上一节的模型,算出每一棵树的SG值之后,再按Nim游戏异或即可
2. 无向图删边游戏
对Christmas Game进行一步拓展——去掉对环的限制,规则如下:
- 一个无相联通图,有一个点作为图的根。
- 游戏者轮流从图中删去边,删去一条边后,不与根节点相连的部分将被移走。
- 谁无路可走谁输。
对于这一模型,有一著名定理Fusion Principle :
我们可以对无向图做如下改动:将图中的任意一个偶环缩成一个新点,任意一个奇环缩成一个新点加一个新 边;所有连到原先环上的边全部改为与新点相连。这样的改动不会影响图的 SG 值。
这样,我们可以将任意一个无向图改成树结构,“无向图删边游戏” 就变成了 “树的删边游戏”
十.简单博弈
1.斐波那契博弈
1堆石子有n个,两人轮流取.先取者第1次可以取任意多个,但不能全部取完.以后每次取的石子数不能超过上次取子数的2倍。取完者胜.先取者负输出"Second win".先取者胜输出"First win".
Zeckendorf定理(齐肯多夫定理):任何正整数可以表示为若干个不连续的Fibonacci数之和
#include<cstdio>
#include<iostream>
using namespace std;
int main()
{
int n, fib[50];
fib[0] = 2; fib[1] = 3;
for(int i = 2; i < 50; i++)
fib[i] = fib[i-1] + fib[i-2];
while(~scanf("%d", &n) && n){
int flag = 1;
for(int i = 0; i < 50; i++){
if(fib[i] == n){
flag = 0;
cout << "Second win" << endl;
}
if(fib[i] > n)
break;
}
if(flag){
cout << "First win" << endl;
}
}
return 0;
}
2.威佐夫博弈
有两堆各若干个物品,两个人轮流从某一堆或同时从两堆中取同样多的物品,规定每次至少取一个,多者不限,最后取光者得胜
#include <bits/stdc++.h>
using namespace std;
int main()
{
int a, b, c;
while(~scanf("%d %d", &a, &b)){
if(a > b)
swap(a, b);
c = floor(b-a) *((sqrt(5.0)+1)/2);
if(a == c) cout << "0" << endl;
else cout << "1" << endl;
}
return 0;
}
3.Nimk博弈
有n堆各若干个物品,两个人轮流从K堆取任意多的物品,规定每次至少取一个,多者不限,最后取光者得胜
const int MAXN = 10005;
int SG[MAXN];//需要处理的SG值数组
int XOR[MAXN];//储存每一个二进制位上的和
int xxx;//储存每一个SG值的临时变量
int num;//储存当前SG值有多少位的临时变量
int maxn;//储存最大的SG值位数
bool solve(int N,int M)//N表示SG数组的大小,从1到N,M表示每次可以取1到M堆
{
memset(XOR, 0, sizeof XOR);
maxn = -1;
for (int i = 1; i <= N; i++)
{
xxx = SG[i];
num = 0;
while (xxx) {
XOR[num] += xxx&1;
num++;
xxx >>= 1;
}
maxn = max(maxn, num);
}
for (int i = 0; i < maxn; i++)
if (XOR[i] % (M + 1))
return true;//返回true表示先手必胜
return false;//返回false表示先手必败
}
十一.例题
1.HDU–1536
在最简单的取石子情况中,有如下规律:
{
s
g
(
x
)
=
x
可以取任意个石子
s
g
(
x
)
=
x
m
o
d
(
1
+
m
)
可以取1 m个石子
s
g
打表
可以取石子个数为不连续整数
\begin{cases} sg(x) = x & \text{$可以取任意个石子$} \\ sg(x) = x\;mod\;(1+m) & \text{可以取1~m个石子}\\ sg打表 & \text{$可以取石子个数为不连续整数$} \end{cases}
⎩
⎨
⎧sg(x)=xsg(x)=xmod(1+m)sg打表可以取任意个石子可以取1 m个石子可以取石子个数为不连续整数
#include <bits/stdc++.h>
using namespace std;
const int N=10008;//N为所有堆最多石子的数量
int f[108],sg[N];//f[]用来保存只能拿多少个,sg[]来保存SG值
bool hash[N];//mex{}
void sg_solve(int t,int N)
{
int i,j;
memset(sg,0,sizeof(sg));
for(i=1;i<=N;i++) {
memset(hash,0,sizeof(hash));
for(j=1;j<=t;j++)
if(i - f[j] >= 0)
hash[sg[i-f[j]]] = 1;
for(j=0;j<=N;j++)
if(!hash[j])
break;
sg[i] = j;
}
}
int main()
{
int k, m, l, num;
while(scanf("%d",&k), k) {
for(i= 1;i <= k;i++) scanf("%d",&f[i]);
sg_solve(k, N);
scanf("%d", &m);
string ans = "";
for (int i = 1; i <= m; i++) {
int sum = 0;
scanf("%d", &l);
for (int j = 1; j <= l; j++) {
scanf("%d", &num);
sum ^= sg[num];
}
if(sum == 0) ans+="L";
else ans+="W";
}
cout<<ans<<endl;
}
return 0;
}
2.HDU–1524
博弈的抽象表示,帮助理解SG
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1005;
int sg[maxn], pat[maxn];
vector<int> G[maxn];
int getSG(int u)
{
if (sg[u] != -1) return sg[u];
bool mex[10*maxn];
memset(mex, false, sizeof(mex));
for (int i = 0; i < G[u].size(); ++i) {
int v = G[u][i];
getSG(v);
mex[sg[v]] = true;
}
for (int i = 0; ;++i) {
if (!mex[i]) {
sg[u] = i;
break;
}
}
return sg[u];
}
int main()
{
int N, m, p, x;
while (~scanf("%d", &N)) {
for (int i = 0; i < N; ++i) {
G[i].clear();
pat[i] = 0;
}
for (int i = 0; i < N; ++i) {
scanf("%d", &p);
while (p--) {
scanf("%d", &x);
G[i].push_back(x);
pat[x]++;
}
}
memset(sg, -1, sizeof(sg));
for (int i = 0; i < N; ++i) {
if (pat[i] == 0) getSG(i);
}
while (~scanf("%d", &m), m) {
int ans = 0;
for (int i = 0; i < m; ++i) {
scanf("%d", &x);
getSG(x);
ans ^= sg[x];
}
if (ans != 0) cout << "WIN" << '\n';
else cout << "LOSE" << '\n';
}
}
return 0;
}
3.BZOJ–1299
对于一个博弈游戏而言,重要的是通过操作留给对面一个 s g = 0 sg=0 sg=0的局面
#include<cstring>
#include<cstdio>
using namespace std;
int n,sg[2001],a[2001];
bool found;
int dfs(int x,int used,int now)
{
if(x==n+1)
{
if(!now&&used>0)found=1;
return 0;
}
dfs(x+1,used,now);
dfs(x+1,used+1,now^a[x]);
}
int main()
{
for(int t=1;t<=10;t++)
{
memset(sg,-1,sizeof(sg));
found=0;
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
dfs(1,0,0);
if(found)puts("NO");
else puts("YES");
}
return 0;
}
类Green博弈
现有一棵树,树的每条边都有长度,A和B轮流染色,每次可选择一条边进行染色,染色长度 ≤ \leq ≤边长,已染色的边不能再染色,同时将子节点删除。
green博弈变形,对于都是1的就是green博弈 S G [ u ] SG[u] SG[u] ^ = S G [ v ] + 1 =SG[v]+1 =SG[v]+1
对于大于1的边,偶数对其没有贡献,奇数有贡献, S G [ u ] SG[u] SG[u] ^$= SG[v] KaTeX parse error: Expected group after '^' at position 1: ^̲(val[v] % 2)$
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2005;
int cnt;
struct Node {
int to, val, next;
}edge[maxn];
int sg[maxn], head[maxn];
void add(int u, int v, int w)
{
edge[cnt].to = v;
edge[cnt].val = w;
edge[cnt].next = head[u];
head[u] = cnt++;
}
void dfs(int u, int fa)
{
sg[u] = 0;
for (int i = head[u]; ~i; i = edge[i].next) {
int v = edge[i].to;
if (v == fa) continue;
dfs(v, u);
if (edge[i].val == 1) sg[u] ^= (sg[v]+1);
else sg[u] ^= (sg[v] ^ (edge[i].val % 2));
}
}
int main()
{
int T;
cin >> T;
for (int j = 1; j <= T; ++j) {
memset(head, -1, sizeof(head));
cnt = 0;
int n, u, v, w;
scanf("%d", &n);
for (int i = 1; i < n; ++i) {
scanf("%d %d %d", &u, &v, &w);
add(u, v, w);
add(v, u, w);
}
dfs(0, 0);
printf("Case %d: ", j);
if (sg[0]) cout << "Emily" << '\n';
else cout << "Jolly" << '\n';
}
return 0;
}
阶梯博弈
n个人随机的站在若干级台阶上,最高一级台阶必须站人,A和B分别移动某个人向上走任意走多级台阶,不能越过更高级台阶上的人,且同一台阶上只能站一人,无法继续移动者输。给出n个人位置,计算先手必胜策略。
#include <stdio.h>
#include <string.h>
const int N = 100 + 10;
int a[N],b[N];
int main()
{
int n = 0,i,j,k,sum = 0;
while(scanf("%d",&a[n])!=EOF)
n++;
for(i=1; i<n; ++i)
b[i-1] = a[i] - a[i-1] - 1;
for(i=0; i<n-1; i+=2)
sum ^= b[i];
if(sum==0)
printf("-1\n");
else
{
//枚举第i个人移动j步,使得剩下的局面异或等于0,
for(i=0; i<n-1; ++i)
for(j=1; a[i]+j<a[i+1]; ++j)
{
b[i] -= j;
if(i!=0)
b[i-1] += j;
sum = 0;
for(k=0; k<n-1; k+=2)
sum ^= b[k];
if(sum==0)
{
printf("%d %d\n",a[i],a[i]+j);
break;
}
b[i] += j;
if(i!=0)
b[i-1] -= j;
}
}
return 0;
}