博弈论
若一个游戏满足:
1.由两名玩家交替行动
2.在游戏进行的任意时刻,可以执行的合法行动与轮到哪位玩家无关
3.不能行动的玩家判负
则称该游戏为一个公平组合游戏 。
则称该游戏为一个公平组合游戏。
NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。
给定一个有向无环图,图中有一个唯一的起点,在起点放有一枚棋子。两名玩家交替把这枚棋子沿着有向边进行移动,每次可以移动一步,无法移动判负。这类游戏被称为有向图游戏 。
任何一个公平组合游戏都可以转化为有向图游戏。具体方法是,把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面连接有向边。
NIM游戏
给定nnn堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
必胜状态 :先手进行某一个操作,留给后手是一个必败状态时,对于先手来说是一个必胜状态。即先手可以走到某一个必败状态。
必败状态:先手无论如何操作,留给后手都是一个必胜状态时,对于先手来说是一个必败状态。即先手走不到任何一个必败状态。
结论:
假设nnn堆石子,石子数目分别是a1,a2,...,ana_1,a_2,...,a_na1,a2,...,an,如果a1⊕a2⊕...⊕an≠0a_1⊕a_2⊕...⊕a_n≠0a1⊕a2⊕...⊕an=0,先手必胜;否则先手必败。
证明:
1.结束状态每一堆都是0个石子,0⊕0⊕...⊕0=00⊕0⊕...⊕0=00⊕0⊕...⊕0=0
2.如果a1⊕a2⊕...⊕an=xa_1⊕a_2⊕...⊕a_n=xa1⊕a2⊕...⊕an=x,x≠0x≠0x=0,设xxx在二进制表示下,最高位的1在第kkk位,那么在a1~ana_1~a_na1~an中一定有一个aia_iai,它的第kkk位也是1。显然ai⊕x<aia_i⊕x<a_iai⊕x<ai,我们在aia_iai中取出来ai⊕xa_i⊕xai⊕x个石子,这样aia_iai中还剩下ai−(ai−ai⊕x)=ai⊕xa_i-(a_i-a_i⊕x)=a_i⊕xai−(ai−ai⊕x)=ai⊕x个。a1⊕a2⊕...⊕ai⊕x...⊕an=x⊕x=0a_1⊕a_2⊕...⊕a_i⊕x...⊕a_n=x⊕x=0a1⊕a2⊕...⊕ai⊕x...⊕an=x⊕x=0,这样就得到了各堆石子数异或为0的局面。
3.如果a1⊕a2⊕...⊕an=0a_1⊕a_2⊕...⊕a_n=0a1⊕a2⊕...⊕an=0,那么不管我们怎么取石子,最后都会得到各堆石子数异或不为0的局面。反证法:
假设在第iii堆中取石子,把aia_iai取成ai′a_i^{'}ai′,可以得到a1⊕a2⊕...⊕an=0a_1⊕a_2⊕...⊕a_n=0a1⊕a2⊕...⊕an=0,那么就会有a1⊕a2⊕...⊕ai⊕...⊕an⊕a1⊕a2⊕...⊕ai′⊕...⊕an=ai⊕ai′=0a_1⊕a_2⊕...⊕a_i⊕...⊕a_n⊕a_1⊕a_2⊕...⊕a_i^{'}⊕...⊕a_n=a_i⊕a_i^{'}=0a1⊕a2⊕...⊕ai⊕...⊕an⊕a1⊕a2⊕...⊕ai′⊕...⊕an=ai⊕ai′=0,所以aia_iai和ai′a_i^{'}ai′相同,但是实际上ai>ai′a_i>a_i^{'}ai>ai′,产生矛盾,假设不成立。
综上,如果当前状态异或和不为0,那么就会让对手陷入到石子数异或为0的局面;而对手不管如何操作,我们面临的状态依然是异或和不为0。以此类推,最后我们一定可以把对手逼到各堆石子都为0的局面。
台阶------NIM游戏
现在,有一个 nnn 级台阶的楼梯,每级台阶上都有若干个石子,其中第 i
级台阶上有 aia_iai 个石子(i≥1)(i≥1)(i≥1)。
两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。
已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
假设nnn堆石子,石子数目分别是a1,a2,...,ana_1,a_2,...,a_na1,a2,...,an,如果a1⊕a3⊕a5⊕...≠0a_1⊕a_3⊕a_5⊕...≠0a1⊕a3⊕a5⊕...=0,先手必胜;否则先手必败。
证明:
1.结束状态每一堆都是0个石子,0⊕0⊕...⊕0=00⊕0⊕...⊕0=00⊕0⊕...⊕0=0
2.如果a1⊕a3⊕...=xa_1⊕a_3⊕...=xa1⊕a3⊕...=x,x≠0x≠0x=0,设xxx在二进制表示下,最高位的1在第kkk位,那么在a1,a3,...a_1,a_3,...a1,a3,...中一定有一个aia_iai,它的第kkk位也是1。显然ai⊕x<aia_i⊕x<a_iai⊕x<ai,我们在aia_iai中取出来ai⊕xa_i⊕xai⊕x个石子,这样aia_iai中还剩下ai−(ai−ai⊕x)=ai⊕xa_i-(a_i-a_i⊕x)=a_i⊕xai−(ai−ai⊕x)=ai⊕x个。a1⊕a2⊕...⊕ai⊕x...⊕an=x⊕x=0a_1⊕a_2⊕...⊕a_i⊕x...⊕a_n=x⊕x=0a1⊕a2⊕...⊕ai⊕x...⊕an=x⊕x=0,这样就得到了各堆石子数异或为0的局面。
3.如果a1⊕a3⊕...=0a_1⊕a_3⊕...=0a1⊕a3⊕...=0,那么如果我们取的是偶数堆的石子放到他的下一个奇数堆里面,那么下一个人可以把这个奇数堆里面刚刚被放的石子取出来放到下一个偶数堆,这样就保证了每一个奇数堆的石子总数和在这两个人操作之前一模一样,异或依然是0;如果取得是奇数堆石子,不管我们怎么取石子,最后都会得到各堆石子数异或不为0的局面。反证法:
假设在第iii堆中取石子,把aia_iai取成ai′a_i^{'}ai′,可以得到a1⊕a3⊕...=0a_1⊕a_3⊕...=0a1⊕a3⊕...=0,那么就会有a1⊕a3⊕...⊕ai⊕...⊕a1⊕a3⊕...⊕ai′⊕...=ai⊕ai′=0a_1⊕a_3⊕...⊕a_i⊕...⊕a_1⊕a_3⊕...⊕a_i^{'}⊕...=a_i⊕a_i^{'}=0a1⊕a3⊕...⊕ai⊕...⊕a1⊕a3⊕...⊕ai′⊕...=ai⊕ai′=0,所以aia_iai和ai′a_i^{'}ai′相同,但是实际上ai>ai′a_i>a_i^{'}ai>ai′,产生矛盾,假设不成立。
集合------NIM游戏
Mex运算
设SSS表示一个非负整数集合。定义mex(S)mex(S)mex(S)为求出不属于集合S的最小非负整数的运算,即:
mex(S)=minxmex(S) = min{x}mex(S)=minx, xxx属于自然数,且xxx不属于SSS
SG函数
在有向图游戏中,对于每个节点xxx,设从xxx出发共有kkk条有向边,分别到达节点y1,y2,...,yky_1, y_2, ..., y_ky1,y2,...,yk,定义SG(x)SG(x)SG(x)为xxx的后继节点y1,y2,...,yky_1, y_2, ..., y_ky1,y2,...,yk的SG函数值构成的集合再执行mex(S)mex(S)mex(S)运算的结果,即:
SG(x)=mex(SG(y1),SG(y2),...,SG(yk))SG(x) = mex({SG(y_1), SG(y_2), ..., SG(y_k)})SG(x)=mex(SG(y1),SG(y2),...,SG(yk)),终点的SGSGSG函数值为0。
特别地,整个有向图游戏GGG的SGSGSG函数值被定义为有向图游戏起点sss的SG函数值,即SG(G)=SG(s)SG(G) = SG(s)SG(G)=SG(s)。
定理
有向图游戏的某个局面必胜,当且仅当该局面对应节点的SGSGSG函数值大于0。
有向图游戏的某个局面必败,当且仅当该局面对应节点的SGSGSG函数值等于0。
证明:
1.如果局面对应节点的SGSGSG函数值大于0,说明我是可以从当前状态到达SGSGSG函数值为0的状态的。
2.如果局面对应节点的SGSGSG函数值为0,说明我只能从当前状态到达SGSGSG函数值不为0的状态。
综上,又因为终点的SGSGSG函数值为0,所以如果当前状态SGSGSG函数值不为0那么最后我就一定能把对手逼到终点,反之同理。
有向图游戏的和
设G1,G2,...,GmG_1, G_2, ..., G_mG1,G2,...,Gm 是mmm个有向图游戏。定义有向图游戏GGG,它的行动规则是任选某个有向图游戏GiG_iGi,并在GiG_iGi上行动一步。GGG被称为有向图游戏G1,G2,...,GmG_1, G_2, ..., G_mG1,G2,...,Gm的和。
有向图游戏的和的SGSGSG函数值等于它包含的各个子游戏SGSGSG函数值的异或和,即:
SG(G)=SG(G1)⊕SG(G2)⊕...⊕SG(Gm)SG(G) = SG(G_1)⊕SG(G_2)⊕ ... ⊕SG(G_m)SG(G)=SG(G1)⊕SG(G2)⊕...⊕SG(Gm)
证明:类似于NIM游戏中的证明,此处略。
给定nnn堆石子以及一个由kkk个不同正整数构成的数字集合 SSS。
现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合SSS,最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
cpp
int sg(int x)
{
if (f[x] != -1) return f[x];
unordered_set<int> S;
for (int i = 1; i <= k; i ++ )
{
if (x >= s[i]) S.insert(sg(x - s[i]));
else break;
}
for (int i = 0; ; i ++ )
if (!S.count(i)) return i;
}
int main()
{
cin >> k;
memset(f, -1, sizeof f);
for (int i = 1; i <= k; i ++ ) cin >> s[i];
cin >> n;
for (int i = 1; i <= n; i ++ )
{
int x;
cin >> x;
res ^= sg(x);
}
if (res) puts("Yes");
else puts("No");
return 0;
}
拆分------NIM游戏
给定nnn堆石子,两位玩家轮流操作,每次操作可以取走其中的一堆石子,然后放入两堆规模更小的石子(新堆规模可以为 0,且两个新堆的石子总数可以大于取走的那堆石子数),最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
相比于集合-NIM,这里的每一堆可以变成小于原来那堆的任意大小的两堆
即a[i]a[i]a[i]可以拆分成(b[i],b[j])(b[i],b[j])(b[i],b[j]),为了避免重复规定b[i]>=b[j]b[i]>=b[j]b[i]>=b[j],即:a[i]>b[i]>=b[j]a[i]>b[i]>=b[j]a[i]>b[i]>=b[j]
相当于一个局面拆分成了两个局面,由SGSGSG函数理论,多个独立局面的SGSGSG值,等于这些局面SGSGSG值的异或和。
因此需要存储的状态就是sg(b[i])⊕sg(b[j])sg(b[i])⊕sg(b[j])sg(b[i])⊕sg(b[j])(与集合-Nim的唯一区别)
例题
移棋子游戏
给定一个有 NNN 个节点的有向无环图,图中某些节点上有棋子,两名玩家交替移动棋子。
玩家每一步可将任意一颗棋子沿一条有向边移动到另一个点,无法移动者输掉游戏。
对于给定的图和棋子初始位置,双方都会采取最优的行动,询问先手必胜还是先手必败。
输入格式第一行,三个整数 N,M,KN,M,KN,M,K,NNN 表示图中节点总数,MMM 表示图中边的条数,KKK表示棋子的个数。
接下来 MMM 行,每行两个整数 X,YX,YX,Y 表示有一条边从点 XXX 出发指向点 YYY。
接下来一行, KKK 个空格间隔的整数,表示初始时,棋子所在的节点编号。
节点编号从 111 到 NNN。
输出格式
若先手胜,输出 win,否则输出 lose。
数据范围
1≤N≤2000,
1≤M≤6000,
1≤K≤N
输入样例:
6 8 4
2 1
2 4
1 4
1 5
4 5
1 3
3 5
3 6
1 2 4 6
输出样例:
win
求出所有棋子的 sg 函数值的异或值,若为0,则先手必败,否则先手必胜。
cpp
#include <iostream>
#include <cstring>
#include <set>
using namespace std;
const int N = 2010, M = 6010;
int n, m, k;
int h[N], e[M], ne[M], tot;
int sg[N];
void add(int a, int b)
{
e[tot] = b, ne[tot] = h[a], h[a] = tot ++ ;
}
int SG(int x)
{
if (~sg[x]) return sg[x];
set<int> S;
for (int i = h[x]; ~i; i = ne[i])
{
int j = e[i];
S.insert(SG(j));
}
for (int i = 0; ; i ++ )
{
if (!S.count(i))
{
sg[x] = i;
return i;
}
}
}
int main()
{
cin >> n >> m >> k;
memset(h, -1, sizeof h);
memset(sg, -1, sizeof sg);
for (int i = 0; i < m; i ++ )
{
int x, y;
cin >> x >> y;
add(x, y);
}
int res = 0;
while (k -- )
{
int x;
cin >> x;
res ^= SG(x);
}
if (res) puts("win");
else puts("lose");
return 0;
}
取石子
Alice 和 Bob 两个好朋友又开始玩取石子了。
游戏开始时,有 NNN 堆石子排成一排,然后他们轮流操作(Alice 先手),每次操作时从下面的规则中任选一个:
从某堆石子中取走一个;合并任意两堆石子。不能操作的人输。
Alice 想知道,她是否能有必胜策略。
输入格式第一行输入 TTT,表示数据组数。
对于每组测试数据,第一行读入 NNN;
接下来 NNN 个正整数 a1,a2,⋯,aNa_1,a_2,⋯,a_Na1,a2,⋯,aN ,表示每堆石子的数量。
输出格式
对于每组测试数据,输出一行。
输出 YES 表示 Alice 有必胜策略,输出 NO 表示 Alice 没有必胜策略。
数据范围
1≤T≤100,1≤T≤100,1≤T≤100,
1≤N≤50,1≤N≤50,1≤N≤50,
1≤ai≤10001≤a_i≤10001≤ai≤1000
输入样例:
3
3
1 1 2
2
3 4
3
2 3 5
输出样例:
YES
NO
NO
简单情况:所有堆的石子个数>1>1>1
设b=b=b=堆数+++石子总数−1−1−1
先手必胜 <=> bbb是奇数
对于任何一个奇数,一定存在一个偶数后继
对于任何一个偶数,所有后继必然是奇数
又当b=1b=1b=1时,有b=1b=1b=1(堆数)+1+1+1(石子总数)−1=1−1=1−1=1,则最终状态一定是奇数
证明思路:
奇数出发(自己)->能到偶数(对手)->必回奇数(自己)->...->剩1(自己)->自己赢
一.奇数:
堆数>1>1>1 => 合并两堆(堆数−1-1−1) bbb->偶数 => 取111石子(石子数−1-1−1)bbb->偶数
即任何一个奇数状态都可以转移到某一个偶数状态
二.偶数:
堆数>1>1>1 => 合并两堆(堆数−1-1−1) bbb->奇数
取一子
1 该堆>2>2>2 bbb->奇数
2 该堆=2=2=2 bbb->奇数
该堆剩111
2.1 总共堆数=1=1=1则奇对手赢
2.2 总共堆数>1>1>1则奇对手一定在之后把这剩111的堆给合并假设剩两堆 222数的堆+奇数个数的堆(b=2+b=2+b=2+奇−1+2=-1+2=−1+2=偶) 拿完后 1+1+1+奇对手合并两堆->最后只剩1堆偶数给偶对手(b=1+偶−1)
一般情况:有堆的石子个数=1=1=1
1石子个数=1=1=1的堆个数=a=a=a
b 其他个数(>1)(>1)(>1)的堆个数+其他堆石子总数 −1−1−1
f(a,b)={f(a−1,b),从a中取1个f(a,b−1),从b中取1个f(a−2,b+3),合并a组2个f(a,b−1),合并b组2个f(a−1,b+1),合并a中一个和b组1个f(a,b)= \begin{cases} f(a-1,b),从a中取1个 \\ f(a,b-1),从b中取1个 \\ f(a-2,b+3 ),合并a组2个 \\ f(a,b-1),合并b组2个 \\ f(a-1,b+1),合并a中一个和b组1个 \\ \end{cases} f(a,b)=⎩ ⎨ ⎧f(a−1,b),从a中取1个f(a,b−1),从b中取1个f(a−2,b+3),合并a组2个f(a,b−1),合并b组2个f(a−1,b+1),合并a中一个和b组1个
cpp
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 55, M = 50050;
int f[N][M];
int dp(int a, int b)
{
int &v = f[a][b];
if (v != -1) return v;
if (!a) return v = b % 2;
if (b == 1) return dp(a + 1, 0);
if (a && !dp(a - 1, b)) return v = 1;
if (b && !dp(a, b - 1)) return v = 1;
if (a >= 2 && !dp(a - 2, b + (b ? 3 : 2))) return v = 1;
if (a && b && !dp(a - 1, b + 1)) return v = 1;
return v = 0;
}
int main()
{
memset(f, -1, sizeof f);
int T;
scanf("%d", &T);
while (T -- )
{
int n;
scanf("%d", &n);
int a = 0, b = 0;
for (int i = 0; i < n; i ++ )
{
int x;
scanf("%d", &x);
if (x == 1) a ++ ;
else b += b ? x + 1 : x;
}
if (dp(a, b)) puts("YES");
else puts("NO");
}
return 0;
}