算法 {博弈论,组合游戏,SG游戏}
@LOC_COUNTER: 10
博弈论
定义
博弈论非常庞大, 根据算法来分类:
1: SG游戏; (通过对SG值进行异或操作)
2: DAG游戏; (直接对整个DAG图进行DFS遍历, 每个节点设置一个bool的胜负态);
3: 思维技巧型; (没有统一算法…)
相关定义
#局面有向图#;
以初始局面为起点, 根据游戏规则的定义, 枚举所有可能的操作 得到新局面, 从而构成一个弱连通的有向图;
.
比如a -> b
意味着: 当局面是a时, 操作之后 会变成b;
性质
有一类游戏 跟组合游戏很相像, 但只有1点不同: 其局面有向图 不是DAG 而是存在环的 (如果没有环 他就是组合游戏); 但是, 虽然有环 但可以证明 在双方采取最优策略下 一定不会进行死循环(即平局) 而是一定以一胜一负结束;
.
此时, 你需要关注 所有双方都采取最优策略下的路径, 这种路径有个性质: 对于任意一点, 如果他是必胜态 则他的{前驱,后驱节点}一定是必败态; (即不断胜负交替)
.
即, 其局面有向图 虽然不是DAG, 但是每个节点 也满足: 要么是必胜态 要么是必败态;
LINK:
;
非组合游戏
定义
不是通过{SG定理/ 遍历整个局面DAG}求解的;
例题
找到必败态的等价条件: LINK: https://editor.csdn.net/md/?not_checkout=1&articleId=131363391
;
组合游戏
定义
组合游戏 Combinatorial game theory
, 需要满足:
1: #最优性# LR两人 轮流操作(且确立谁先手), 且都采用最优策略 (只要能让对手输 我就赢了; 否则不管我怎么选择对手都赢 则我就输了);
2: #有限性#: 不管怎样操作, 游戏一定会在有限次操作后 变成终止态 以一胜一败结束 (不会是平局);
3: #确定性#: 该游戏的局面有向图 (即所有局面节点(一个节点由{棋盘状态 + 谁操作}组成)) 是一个DAG, 该DAG只有1个起点(初始局面) 所有的叶子节点都是终止态局面(意味着游戏结束 胜负已定);
.
#推论#: 所有可能的局面 (即{棋盘状态 + 谁操作}) 要么是必胜态 要么是必败态 (即游戏无需进行 便知胜负);
@DELI;
为什么称之为组合Combinatorial
?
该游戏DAG上任意一个节点 都可视为一个单独的游戏; 比如对于一个游戏 他的初始局面是X
其DAG图上有Y,Z
后驱节点, 那么对于Y 可以把他当成是一个独立的游戏 (即一个游戏 他的初始局面是Y) 他跟X是完全无关的, 也就是 Y, Z
是两个独立的游戏 (比如说 他俩的结果 是一个必胜 一个是必败), 那么现在要进行一个新游戏X 其实他的结果已经可以确定了 因为他由Y,Z
组合而成 即X = {Y, Z}
, 后驱节点存在必败态 因此X为必胜态;
.
这是组合一词的含义, 即每个节点/局面 都可以视为一个独立的游戏, 因此 一个初始状态为X的游戏 其实由所有以后驱节点为初始状态的独立游戏 组合而成;
相关定义
#组合游戏DAG#;
满足:
1: 只有1个起点 (即该起点可以到达所有点);
2: 每个点都有一个胜负态的属性 (要么是必胜态, 要么是必败态), 且要满足: 对于非叶子节点C, 如果他的某个儿子是必败态 则C是必胜态; 否则C的所有儿子都是必胜态 则C为必败态 (对于叶子节点 其胜负态随意, 由游戏所规定 因为叶子节点对应为游戏的终止态 即当游戏到了叶子节点 游戏就结束了);
@DELI;
组合游戏DAG 依据叶子节点的胜负态, 可分为3类:
1: #标准型组合游戏DAG# Normal play convention
;
.
所有的叶子节点, 都是必败态; (等价于: 游戏对终止态局面的定义为: 当前玩家失败);
2: #反常型组合游戏DAG# Misere
;
.
所有的叶子节点, 都是必胜态; (等价于: 游戏对终止态局面的定义为: 当前玩家胜利);
3: #其他型组合游戏DAG#;
.
如果所有叶子节点里 既有必胜态 又有必败态;
@DELI;
#终止局面# Terminal Position
;
游戏规定 当局面满足怎么样的条件时 游戏结束 (且胜负已定), 则把满足这样的条件的局面 称为终止局面 (其对应于DAG里的叶子节点);
.
终止局面 是完全根据游戏规则来定义的, 他可以是必胜态 也可以是必败态;
@DELI;
组合游戏 依据操作的定义 是否因人而异, 可分为2类: (中立的imparital
)(非中立的 partisan
);
性质
令Li
表示一个组合游戏DAG的i
级叶子节点 (L1
为一级…), 则一定有:
1: 如果L1
都是必胜态, 则L1,L3,L5...
都是必胜态 L2,L4,L6...
都是必败态;
2: 如果L1
都是必败态, 则L1,L3,L5...
都是必败态 L2,L4,L6...
都是必胜态;
因为: Li
叶子节点的所有子节点 一定都是 L(i-1)
级叶子节点;
@DELI;
所谓组合游戏, 即任意局面 都是一个单独的游戏; 这个概念很重要 尤其是在SG游戏;
@DELI;
#所有标准型组合游戏DAG, 都等价于 DAG上移动棋子游戏#;
详见: LINK: @LOC_2
;
@DELI;
对于组合游戏DAG, 不关心他是{标准型/反常型/…} (即不关系他叶子节点的胜负态), 对于一个非叶子节点X, 如果其所有后驱节点都是必胜态 ⟺ \iff ⟺ X为必败态, 否则存在后驱节点为必败态 ⟺ \iff ⟺ X为必胜态;
即不管是{标准/反常/…}组合游戏DAG, 他们的非叶子节点 都有这个性质! 其实 这已经不是个性质了, 非叶子节点的胜负态的定义 就是这样定义的, 具体算法为:
组合游戏DAG的拓扑序, 一定形如
[
.
.
.
四级
,
三级
,
二级
,
一级叶子节点
]
[... 四级, 三级, 二级, 一级叶子节点]
[...四级,三级,二级,一级叶子节点], 以逆序方式遍历该序列:
对于一级叶子节点: 其胜负态取决于 游戏规则里对终止态怎么定义的, 因此要特判;
对于
X
>
1
X>1
X>1级叶子, 其所有后驱节点(一定是<=(X-1)
级叶子节点)的胜负态都已经确定, 根据上面的规则 就可以确定当前节点的胜负态;
@DELI;
MARK: @LOC_8
;
#只要组合游戏DAG的点数
>
1
> 1
>1, 那么他一定 (既有必胜态的节点, 又有必败态的节点)#
只需分析(标准型,反常型)DAG, 因为否则 叶子节点已经满足条件了;
#引理#: (1: 点数
>
1
>1
>1的DAG 一定存在二级叶子节点)(2: 二级叶子节点的所有后驱节点 均是一级叶子节点);
根据这两条引理, 对于标准型DAG: 二级叶子节点一定是必胜态, 对于反常型DAG: 二级叶子节点一定是必败态;
@DELI;
将组合游戏里的(局面 + 谁操作)作为图论的一个节点, 向 操作后的(局面 + 另一人)的节点 连接一条有向边(边表示操作), 则构成一个组合游戏DAG;
.
即: 任何组合游戏
⟺
\iff
⟺ 组合游戏DAG;
@DELI;
组合游戏, 不是公平游戏 (这里指的是fair
, 而不是博弈论里的impartial
);
因为 {初始局面 + 谁先手}这个问题确定后, 胜败就决定了! 与人的智商无关, 当然不是公平的;
@DELI;
象棋不是组合游戏, 因为(L人 前进一步, R人也前进一步, L人再退回来, R人也退回来, 如此往复循环), 所以他不是个 D A G DAG DAG图;
五子棋是组合游戏, 因此 对于任意一个 n ∗ m n*m n∗m的空局面 然后确定谁先手, 此时胜败已成定局 (因为他是个DAG图, 我们 通过暴力DFS就可以判定出来);
@DELI;
MARK: @LOC_0
;
组合游戏的取胜规则 有2个版本:
1:
normal play
正常版本; 当前局面无法进行操作
⟺
\iff
⟺ 当前玩家失败;
2:
misere
反常版本; 当前局面无论怎么操作后 新局面让对手无法操作
⟺
\iff
⟺ 当前玩家失败;
但也不用去记忆这两种版本, 因为不管是什么取胜规则 他判定的 就是终止态, 这点是相同的, 只要到了终止态 游戏就结束了;
换句话说, 你只需关注 DAG图里的叶子节点 怎么定义, 这非常非常重要! 判定什么局面是叶子节点 也就等价于判定 当出现当前局面时 游戏就结束了 并且, 谁面临当前局面 谁就是必败的;
举例:
1:
对于正常版本, (终止态/叶子节点)
⟺
\iff
⟺(该局面无法进行操作);
2:
对于反常版本, (终止态/叶子节点)
⟺
\iff
⟺(该局面虽然可以操作, 但没有继续进行操作的必要, 因为不管怎么操作 后驱局面都是无法操作的);
可以看到, 虽然不同的取胜规则 导致对终止态的定义 是不同的, 但如果从图论的角度 你要做的 就是保证: 叶子节点 就是终止态;
错误
@DELI;
#终止局面 就是无法再进行操作的局面#;
非常非常容易犯这个错误, 其实 怎样的局面是终止态 这要看你的取胜规则是怎么定义的, 终止态是必败态这是肯定的 他与是否可以继续操作无关, 可能当前局面还能操作 但是当前局面是终止态 (即谁面临这个局面 谁是必败的; 比如反常版本就是这样的);
算法
因为他是个DAG图, 遍历其所有的节点bool DFS( point)
, 因为所有叶子节点 都符合结束游戏的条件 此时可以判断其到底是 胜利还是失败;
对于非叶子节点: (1: 如果他的儿子节点 全都是必胜态, 则当前节点为必败态)(2: 否则 当前节点为必胜态);
bool DFS( point){ // `point`由{局面, 谁操作}组成;
if( point是叶子节点){
return true/false;
}
for( nex : `point的儿子节点`){ // 即所有可能的操作
if( false == DFS( nex)){ // 儿子是必败态
return true; // 当前是必胜态
}
}
return false;
}
就是个简单的图的遍历, 不需要记忆化 因为是DAG图; 也不要跟SG函数有牵连 那是公平组合游戏的事情 这里很简单 就是个DFS遍历;
既然提到SG函数 多说一句, 你使用SG函数作为返回值也可以(当然效率大大降低); 即叶子节点返回0
然后非叶子节点对其所有儿子的返回值进行MEX操作, 其实最终达到的效果 和这里直接使用bool返回值, 是一样的;
但几乎不会使用这样的做法 (否则也太简单了…), 因为 这个DAG图 可能非常非常大, 注意这个DAG是要包含所有局面的, 即所有可能的操作 导致所有的局面 都要体现在这个DAG图里, 因此 通常去遍历这个DAG图 几乎不可行;
组合游戏的算法:
1: 上面讲的这种暴力DFS方式;
2: 下面会讲到的 SG游戏;
组合游戏/中立的
定义
对于组合游戏G, 如果对于任意局面: 让玩家L操作后的所有新局面是S1, 让玩家R操作后的所有新局面是S2, 那么一定有
S
1
=
S
2
S1 = S2
S1=S2; 此时组合游戏G 称为中立的Impartial
;
.
即, 在组合游戏里 其DAG图的节点 由{局面, L/R}组合, 而在中立组合游戏里 DAG图的节点 可以只由局面组合 无需关注谁操作;
性质
有人把Impartial
翻译为公平的, 我认为不好,
@DELI;
在组合游戏里, 对于同一局面P, 可能(P, L)
节点是必胜的 而(P, R)
节点是必败的; 但是在公平组合游戏 (P, L)
与(P, R)
的 胜败态 一定是相同的, 因此 我们无需关注这个局面是谁在操作, 只需关注最优策略下 操作这个局面, 是否可以必胜这个问题;
.
也就是, 胜负态 是绑定在局面上的 与玩家无关, 如果一个局面是必胜的 那么谁来操作都是必胜的;
@DELI;
SG游戏(独立集游戏)
定义
SG: Sprague, Grundy 两个人名
;
#游戏描述#: 给定
>
1
>1
>1个只有1个起点的 有限的 DAG {S1, S2, ...}
, 他们的起点上 有1个棋子 (对应为下面的标题SG游戏/经典模型/DAG上移动棋子游戏, 也就是: 标准型中立组合游戏DAG);
#游戏操作#: 选择一个棋子不在叶子节点的DAG, 将该图上的棋子 移动到 任意后驱节点上;
#游戏终止#: 如果所有DAG的棋子 都位于叶子节点, 则当前玩家失败;
即 当前SG游戏的局面X, 等价于 一个独立集{S1, S2, ...}
(每个Si
是一个标准型中立组合游戏 即上面讲的DAG上移动棋子游戏 他俩等价);
.
即X 可以表示成(P1, P2, ...)
(Pi
为标准型中立组合游戏Si
的局面);
@DELI;
有点抽象, 但他确实非常重要 SG游戏是算法博弈论的主流, 因此下面举个例子;
即有2个DAGG, G'
, 初始时 A, A'
各有1个棋子, 我们以(A, A')
表示该SG游戏的局面, 那么该SG游戏DAG 如图所示 (由于他是标准型组合游戏DAG, 因此他可以定义SG函数, 橙色数字表示其SG值);
初始局面(A, A')
是必胜的, 但如果初始局面是(C,A')
他是必败的;
性质
SG游戏 比如某个局面是(P1, P2, ...)
Pi
是一个DAGGi
的初始节点;
如果通过朴素做法 即遍历该SG游戏的整个局面DAG, 这是不可行的 他非常非常大! 他的节点个数 比|G1| * |G2| * ...
还大的多!
SG定理表明, 你无非就是想求(P1, P2, ...)
这1个局面的SG值, 他就等于 Pi
在Gi
的SG值 的异或和; 即此时 时间复杂度转换为了|G1| + |G2| + ...
;
@DELI;
不要讲: 某个游戏 是SG游戏, 这是错误的;
因为组合游戏, 每个局面(即节点) 都是一个独立的游戏, 换句话说 同一个组合游戏G, 他的不同局面 是不同的(虽然游戏规则等都相同);
有点含糊, 这里要讲的问题是: 同一个游戏G, 他的有些局面X 可以表示成SG游戏 即计算X的SG值 无需通过求其后驱节点的SG值然后取MEX这种常规操作 而是可以将X表述成(P1, P2, ...)
(每个Pi
是一个子DAGSi
的局面) 那么求所有Pi
在Si
里的SG值的异或值 就等于X的SG值 (这是SG游戏即SG定理的核心), 但是G的有些局面 无法表示成SG游戏 (即转换成子游戏DAG); 因此, 不要说某个游戏G是SG游戏, 应该讲 某个游戏G的某个局面X 他是SG游戏;
详见: LINK: @TODO
;
@DELI;
SG游戏是 组合游戏;
1: #最优性# 成立;
2: #有限性# 由于所有DAG是有限的, 成立;
3: #确定性# 由于棋子只能后驱移动(不能往前移动), 因此其局面有向图 一定是个DAG;
SG游戏是 中立的; 因为从游戏操作可看出 两个人的操作是相同的;
SG游戏是 标准型; 因为终止态局面定义为 当前玩家失败;
算法
#SG定理#
SG游戏的初始局面为X, 其对应为:
>
1
>1
>1个标准型中立组合DAG{G1, G2, ...}
且他们的起点依次为{S1, S2, ...}
, 则:
(由于{SG游戏, Gi
} 都是标准型中立组合游戏) 他们都可以定义SG值, 则SG定理表明: 初始局面X的SG值
⟺
\iff
⟺ 所有Si
的SG值的异或和;
@DELI;
在#定义#章节里有个图, 根据其DAG图 可知起点的SG值为3, 而A
在其子DAG里的SG值为2, A'
在其子DAG里的SG值为1, 因此1^2 = 3
;
即, 这个SG游戏 他的DAG图非常大(通过暴力遍历整个DAG图 构造其每个节点的胜负态, 是不可行的 会超时), 而(A, A')
他的SG值 无需通过遍历整个DAG图获得, 他就等于 A的SG值 ^ A'的SG值
;
证明: 令SG游戏为G, 初始局面 可等价为 独立集{S1, S2}
标准中立DAG (对于
>
2
>2
>2的情况 可通过数学归纳法证明);
1: 对于G的DAG里的叶子节点X, 即(L1, L2)
(他们都是Si
里面的叶子节点), X的SG值 为0, 而Li的SG值
也是0, 此时成立(0^0) = 0
;
2: 对于G的DAG里的非叶子节点X = (P1, P2)
(Pi
不会全都是叶子节点), 以*二级->三级->四级叶子节点->…*的顺序遍历 (也就是此时他的所有后驱节点(x,y)
的SG值 满足定理 即满足SG( (x,y)) = SG(x) ^ SG(y)
);
记a = SG(P1), b = SG(P2)
, 令其子节点为(PP1, PP2)
记c = SG(PP1), d = SG(PP2)
, 则一定有: c = [0, a), d = b
; c = a, d = [0, b)
; 可能有c > a, d = b
可能有c = a, d > b
;
.
举个例子说明下, 比如当前SG值为(a=2, b=3)
, 那么他的子节点(SG值) 一定有(0,3) (1,3) (2,0) (2,1) (2,2)
可能有(>2, 3)
可能有(2, >3)
;
.
注意 根据前提, 此时已经有: 其所有后驱节点的SG值 就等于子游戏的异或值, 因此SG( (PP1,PP2)) = c ^ d
;
.
对于后两种可能情况 (即变大的情况 c>a, d=b; c=a,d>b
), 他的SG值 一定不会等于 a^b
(根据异或性质);
.
令a^b = T
, 根据异或性质, 对于变小的情况 他们一定会覆盖掉[0, T)
; LINK: (https://editor.csdn.net/md/?articleId=126951968)-(@LOC_0);
.因此综上, 当前(a,b)
的SG值 等于a^b
;
@DELI;
对于一个游戏G 其初始局面为X, 你无非想要的 就是求X的胜负态! 此时有2种情况:
1: X是SG游戏, X = (P1, P2, ...)
Pi
为一个标准型中立DAG的初始局面, 即对X操作 等价于 对(P1, P2, ...)
独立集的操作, 那么X的SG值 等于Pi
的SG值的异或值;
2: X不是SG游戏, 那么令X的所有后驱节点为S, 此时的目的转换为: 求S的SG值 (然后通过MEX就可以求X的SG值), 因此 假如其所有后驱节点S 都是SG游戏, 这样后驱节点S的SG值 就不需要(通过递归整个局面DAG来求) 而是通过SG定理来求;
SG游戏/经典例题
Nim游戏
定义
给定一个数字
X
≥
0
X \geq 0
X≥0, 操作: 选择0 < k <= X
执行X -= k
; 终止态: X = 0
时 当前玩家失败;
他是标准型中立组合游戏;
其局面DAG为:
X=3
3--0(0)
--1(1)--0(0)
--2(2)--0(0)
--1(1)--0(0)
`a(b)表示: 当前局面是a, b表示其SG值`;
性质
可以发现: 一个局面X 他的SG值 就等于X; (因此 X > 0 X > 0 X>0必胜);
Nim游戏/SG游戏
MARK: @LOC_5
;
定义
给定若干个数字
(
X
1
,
X
2
,
.
.
.
)
,
X
i
≥
0
(X1,X2,...),X_i \geq 0
(X1,X2,...),Xi≥0, 操作: 选择一个数Xi
, 选择0 < k <= X
执行Xi -= k
; 终止态: 所有Xi
为0时 当前玩家失败;
他是标准型中立组合游戏;
其局面DAG为:
[1,2](3)--[0,2](2)--[0,1](1)--[0,0](0)
--[0,0](0)
--[1,1](0)--[0,1](1)--[0,0](0)
--[1,0](1)--[0,0](0)
--[1,0](1)--[0,0](0)
`[a](b)表示: a为局面, b为其SG值`;
他是SG游戏, 证明:
对于(X1, X2, ...)
, 令Si
表示朴素Nim游戏(X = Xi
), 可以发现 该游戏 等价于: 每次选择一个不在终止态的Si
然后将Si
移动到后驱节点;
因此, 对于初始局面(X1, X2, ...)
(虽然他的局面DAG非常非常大 通过遍历来构造胜负态不可行), 根据SG定理: 他在局面DAG里的SG值 就等于 Xi
在Si
的SG值(根据朴素Nim游戏, 他的值等于Xi
)的异或和;
例题
LINK: https://editor.csdn.net/md/?articleId=131420502
;
Nim游戏/拆分Nim
定义
MARK: @LOC_6
;
给定集合S = {X>=0}
, 操作: 从集合中选择1个数Xi
, 选择0 <= a,b < X
, 将Xi
替换为a,b
; 终止态: 如果集合所有元素都是0 则当前玩家失败;
S = {2}
{3}(4)--{00}(0)
--{01}(1)--{000}(0)
--{02}(2)--{000}(0)
--{001}(1)--{0000}(0)
--{011}(0)--{0001}(1)--{00000}(0)
--{11}(0)--{001}(1)--{0000}(0)
--{12}(3)--{002}(2)--{0000}(0)
--{0001}(1)--{00000}(0)
--{0011}(0)--{00001}(1)--{00000}(0)
--{001}(1)
--{011}(0)
--{111}(1)--{0011}(0)--{00001}(1)--{000000}(0)
--{22}(局面DAG太大了,省略...)
{a}(b)表示: 局面是a, 其SG值为b
注意, 你可能觉得 {a,b,c}
的SG 就等于a^b^c
, 这是错误的! (虽然图中有些局面确实满足, 但不可以以偏概全, 还是用通过严格的推理证明);
因为: {X}
的SG 并不是X
(比如{3}
的SG值为4);
虽然他的初始局面{X}
不是SG游戏, 但其儿子节点{a,b}
等价于 (拆分Nim(a), 拆分Nim(b))
这个独立集 他是SG游戏!
因此, 对于{X} -> {a,b}
我们需要求: a这个独立的拆分Nim的SG值; 但是现在 画出a这个子DAG 仍然十分困难 因为他也形如a -> {a1,b1} -> ...
非常大, 因此继续递归的处理, 即a
虽然不是SG游戏 但a的儿子 {a1,b1}
是SG游戏;
变形
LINK: @LOC_7
;
初始局面是{X1, X2, ...}
, 他是SG游戏, 每个Xi
就是朴素的拆分Nim;
做法是: Get_SG( X1) ^ Get_SG( X2) ^ ...
;
算法
int Get_SG( int _a){ // `a >= 0`;
static int __record[ 102];
{ static bool __is_first = true; if( __is_first){ __is_first = false; memset( __record, -1, sizeof( __record));}}
auto & ANS = __record[ _a];
if( ANS != -1){ return ANS;}
//--
{ // 检查终止态;
bool is_leaf = false;
if( _a == 0){
is_leaf = true;
}
if( is_leaf){ return ANS = 0;}
}
unordered_set< int> mex;
for( int a = 0; a < _a; ++a){
for( int b = a; b < _a; ++b){
auto aa = Get_SG( a);
auto bb = Get_SG( b);
mex.insert( aa ^ bb);
}
}
//> 对`mex`进行MEX操作;
for( ANS = 0; ; ++ANS){
if( mex.find( ANS) == mex.end()){
break;
}
}
ASSERT_( ANS > 0);
return ANS;
}
例题
LINK: https://editor.csdn.net/md/?articleId=131488394
;
DAG上移动棋子游戏
定义
给定一个DAG, X点上有一个棋子, 操作定义为: 将该棋子 移动到 任意后驱节点上, 终止局面为: 该棋子位于叶子节点时 必败;
令G为该DAG的子图: 由所有X可以到达的点和边组成(X无法到达的点和边 无需关注), 则G为标准型的组合游戏DAG;
即, 所有叶子节点为必败态, 然后递归定义 如果一个非叶子节点的所有后驱为必胜态 则其为必败态, 否则为必胜态;
算法: 使用组合游戏的遍历DAG算法 (即获取每个节点的胜负态), 从而得到初始局面的胜负态;
性质
MARK; @LOC_2
;
#所有标准型组合游戏DAG, 都等价于 DAG上移动棋子游戏#;
对于标准型组合游戏DAG, 他的游戏规则 即从初始局面(即起点) 操作之后 变成了其某个后驱局面, 这个操作 就等价于: 你想象在该DAG的起点上 有一个棋子 然后每次操作把这个棋子移动到后驱节点上;
游戏终止为: 如果局面变成了任意终止态局面(即叶子节点的局面) 则谁面临谁失败; 这个也等价于: 当棋子移动到了叶子节点 谁面临谁失败;
因此, 这两个游戏 其实是完全等价的; 只不过 一个是局面在变化 另一个是在移动棋子;
DAG上移动棋子游戏/SG游戏
MARK: @LOC_4
定义
给定一个DAG, 上面有若干个棋子1,2,3,...
, 每次操作: 选择任意一个不在叶子节点上的棋子 将它移动到后驱节点上; 终止局面: 所有棋子均在叶子节点上 则当前玩家失败;
该游戏为SG游戏, 证明:
令Gi
表示 该DAG上只有1个i
号棋子 的DAG上移动棋子游戏 (即普通版本的 DAG上移动棋子游戏);
.
根据DAG上移动棋子游戏的性质, 其局面DAG 就等于 这个图DAG, 记i
号棋子所在的节点位置为Pi
(Pi
不仅是DAG的节点, 也是其局面DAG的局面);
则可发现: 当前游戏的局面X 等价于 (P1, P2, ...)
(每次选择1个不在叶子节点上的Pi
让他移动到后驱节点);
性质
因此, 如果你去根据X局面 去遍历整个他的局面DAG(注意 不是题目里给定那个DAG), 肯定是超时的! 这个图是非常非常大的 (P1', P2, ...), (P1, P2', ...) ...
;
但是, 因为SG( (P1,P2,...)) = SG(P1) ^ SG(P2) ^ ...
, 所以只通过|DAG|
就可以解决;
.
即对这个DAG图构造SG值, 然后把所有棋子所在的节点的SG值 异或起来 就是答案;
例题
模板: LINK: https://editor.csdn.net/md/?articleId=131501592
;
SG游戏/2种常见题型
题型1: 初始局面本身就是SG游戏
定义
游戏G的初始局面为(P1, P2, ...)
为SG游戏, 其中每个Pi
就是一个独立的标准型中立DAG;
这种题比较简单, 直接求Pi
在其DAG上的SG值即可;
例题
LINK: @LOC_5
;
LINK: @LOC_4
;
题型2: 初始局面不是SG游戏, 但其所有后驱节点是SG游戏
定义
游戏G的初始局面 可能不是SG游戏(例如LINK: @LOC_6
) 也可能是SG游戏(形如{X1,X2...}
; 但即便是SG游戏, 通过遍历Xi
的DAG来求其SG值不可行, Xi
不是SG游戏 但他的后驱节点是SG; 例如LINK: @LOC_7
), 他是递归的定义的
此时的做法是: 他的后驱节点 一定形如{X1,X2,...}
他一定是SG游戏, 虽然直接求Xi
在其DAG的SG值 不可行, 而且Xi
不是SG游戏 但Xi
的后驱节点 是SG游戏, 因此继续拆分 他是递归的进行的;
例题
LINK: @LOC_6
;
LINK: @LOC_7
;
将反常型DAG转换为标准型DAG: LINK: https://editor.csdn.net/md/?articleId=131472839
;
SG游戏/将非标准型转换为标准型
定义
SG游戏的前提是: 他是标准型, 但如果他不是标准型 也可能可以把他转换为标准型;
因为对于一个点数 > 1 >1 >1的组合游戏DAG, 你删除必胜态的叶子节点, 并不会影响起点局面的胜负态 (我们的唯一目的, 就是求起点局面的胜负态 其实并不关心其后代节点)
a(V)--b(V)
--c(X)
`V为必胜态, X为必败`;
你把叶子节点b删除掉 这个图 仍然是符合组合游戏DAG的要求, 但是如果你把叶子节点c删除掉 这就出错了 此时a(V)--b(V)
显然根据组合游戏DAG要求 (如果叶子节点全都是必胜
⟺
\iff
⟺ 当前是必败态) 因此a应该是必败态;
.
因为 任意一点的胜负态 其实看的是是否有 必败态的儿子节点, 只要你不删除必败态的节点, 删除必胜态叶子节点(其实 删除必胜态的非叶子及其子树也可以, 但好像不常用) 该DAG依然符合组合游戏DAG的要求 注意前提是不能删除根节点 这是自然的 否则根都没了你的答案没了;
根据LINK: @LOC_8
, 对于一个点数
>
1
>1
>1的非标准型中立组合游戏DAG, 递推执行: 将其所有必胜态的叶子节点删除掉, 最终一定会变成一个点数
>
0
>0
>0的标准型中立组合游戏DAG;
算法
你的做法可能是: 令L为原来的叶子节点, 然后你去推理 满足怎样的条件的非叶子节点N 他的后驱节点都是L, 那么 这个N(一定是必败态) 就是 新DAG的叶子节点;
这个思路是正确的 但比较复杂 你需要进行逻辑推理, 有更简洁的做法;
原来你的DAG是:
DFS( cur){ `cur`为DAG的节点;
if( cur为终止态){ // 必胜态
return;
}
for( nex : cur){
}
}
现在改为:
DFS( cur){ `cur`为DAG的节点;
if( cur为终止态){ // 这里的if判断和上面版本一样;
ASSERT; 报警!
}
int cont = 0;
for( nex : cur){
if( nex为终止态){ // 跟上面的if判断跟上面的都一样;
continue;
}
++ cont;
}
`cont为0, 则cur为新DAG的叶子节点 即必败态`;
}
这个做法很巧妙, 你无需人为的去找 新的叶子节点符合怎样的条件 (因为可能很复杂), 现在 你就按照原来遍历DAG的方式 只不过 跳过原来的叶子节点, 这样只要cont=0
就是新DAG的叶子节点;
非标准型游戏, 转换为: SG游戏
MARK: @LOC_9
;
给定若干个中立组合游戏DAGG1, G2,...
令其起点为P1,P2,...
, 定义游戏:
初始局面为(P1, P2, ...)
, 操作为: 选择一个Pi
将其移动到后驱节点, 终止态: 只要某个Pi
为叶子节点 则当前玩家必胜
.
其实终止态里 一定是: 1个叶子节点 其他都不是叶子节点, 否则如果有多个叶子节点 那游戏早结束了;
G1: a--l G2: c--l
--b--l
(b,c)局面是必败态(不论怎么操作 对手的局面一定有个叶子节点);
而(a,c)局面是必胜态;
注意: Gi
就是个中立组合游戏DAG, 并没有定义胜负态!
令LLi
为Gi
的二级叶子节点, 令当前游戏的局面DAG为G;
根据题意有: G的叶子节点(为必胜态)为: 存在某个Pi
为叶子节点;
结论: G的二级叶子节点(为必败态) 一定为: 所有的Pi
均为LLi
;
#算法#
根据结论, 我们将G的所有必胜态叶子节点删除掉 (即原来的所有二级叶子节点 变成了新图G'
的叶子节点), 此时G'
的叶子 都是必胜态; 同时把Gi
的所有叶子节点也都给删除掉 变成了Gi'
;
原先问题转换为: 给定若干个中立组合游戏DAGGi', G2',...
令其起点为P1,P2,...
, 定义游戏:
初始局面为(P1, P2, ...)
, 操作为: 选择一个Pi
将其移动到后驱节点, 终止态: 当所有Pi
为叶子节点 则当前玩家必胜 (这里的定义变了);
.
显然, 这个新G'
游戏 是SG游戏;
例题
(非标准型游戏, 转换为: SG游戏) + (拆分Nim): LINK: https://editor.csdn.net/md/?not_checkout=1&articleId=131472839
;
错误
假如修改下条件: 终止态: 只要某个Pi
为叶子节点 则当前玩家必胜, 改为: 有
>
1
>1
>1个叶子节点时 当前玩家必胜;
那么, 此时 上面的算法 (删除所有叶子节点 就可以转换为标准型) 就失效了!
因为: 此时G的叶子节点 有
>
1
>1
>1个Pi
为叶子节点, 那么G的二级叶子节点 就不再是: 所有的Pi
均为LLi
了 (而是: 有1个Pi
为叶子 其他的Pi
都是LLi
);
.
因此你把所有Gi
的叶子节点给删除掉 是错误的, 因为G的二级叶子节点 会用到Gi
的叶子节点;
例题
(非标准型游戏, 转换为: SG游戏) + (拆分Nim): LINK: https://editor.csdn.net/md/?not_checkout=1&articleId=131472839
;
SG游戏/SG函数
定义
对于一个标准型的 中立的 组合游戏DAG, 其上可定义一个SG函数 SG( 节点) = 非负整数
(即该图上的每个节点 都有一个SG值), 具体算法为:
1: 对于叶子节点, 其SG值为
0
0
0;
2: 对于非叶子节点X, 令其所有后驱节点的SG值集合为S, 则SG( X) = MEX( S)
; (MEX操作定义为: S集合里 最小的 未出现的非负整数);
int Get_SG( 局面cur){
if( `cur为终止局面`){ // 前提是: 所有终止局面 一定是*必败态*;
return 0;
}
unordered_set< int> Mex; // 也可以开bool数组记录
for( `cur的所有后驱nex`){
Mex.insert( Get_SG( nex));
}
int ANS;
for( ANS = 0; ; ++ANS){
if( Mex.find( ANS) == mex.end()){
break;
}
}
return ANS;
}
性质
注意, 只有标准型(即所有终止局面(即叶子节点) 都是必败态的, 而且是中立的 组合游戏上, 才可以定义SG函数;
.
假如不是标准型, 或者不是中立的, 就不可以在其DAG上定义SG函数;
@DELI;
#任意节点X为必败态 ⟺ \iff ⟺ SG(X)为0# (等价于: 必胜态的节点 其SG值一定是 > 0 >0 >0的)
证明:
DAG的拓扑序一定形如[..., 四级, 三级, 二级, 一级叶子节点]
以逆序方式遍历:
1: 一级叶子 (全是必败态 即SG值全为
0
0
0); 成立;
2: 二级叶子 (全是必胜态 即SG值全为
>
0
>0
>0); 成立;
3: 三级叶子 (如果存在后驱为必败(其SG值为
0
0
0) 则为必胜态(SG值
>
0
)
>0)
>0); 否则后驱均为必胜态(SG值为
>
0
>0
>0) 则为必败态(SG值为
0
0
0)); 成立;
4: 以此类推;
错误
如果你要求解一个标准型的 中立的组合游戏G, 那么你根本无需 对其DAG图 去构造SG函数, 然后通过判断 初始局面的SG值 来判断游戏的输赢;
.
显然就多此一举了, 你明明可以简单的 通过上面组合游戏的算法 (即遍历整个DAG, 求各个节点的胜负态); 否则, 求SG值 还得再乘以个常数;
SG函数的用途, 即一个 即SG游戏;
@DEPRECATED
定义
A,B轮流操作 (操作规则相同), 谁遇到必输状态谁输;
性质
任何局面必然属于一种确定的状态
状态分为两种: 必输和必赢;
任何一个局面, 要么是必输, 要是必赢;
因此, 谁会赢 完全取决于 起始局面, 换句话说, 谁会赢 取决于 谁先手; (假如起始局面是必输的, 那么谁先手谁必输; 输赢与
A
,
B
A,B
A,B两人无关, 仅仅取决于 谁先手);
@Delimiter
终结状态, 必然是必输的
所谓终结状态, 也就是游戏停止终结的条件;
任何一个局面, 游戏进行下去, 必然会遇到必输状态, 不会永远进行下去;
算法模板
从必输状态倒推
我们令
l
o
s
s
loss
loss为必输状态,
w
i
n
win
win为必赢状态;
我们从终结状态
l
o
s
s
loss
loss开始, 然后倒推
w
i
n
win
win, 最终得到一个序列
l
o
s
s
1
(终结)
→
w
i
n
1
→
l
o
s
s
2
→
w
i
n
2...
loss1 \text{(终结)} \to win1 \to loss2 \to win2 ...
loss1(终结)→win1→loss2→win2...;
当我们根据
l
o
s
s
loss
loss 去反推
w
i
n
win
win时, 要保证:
.
对于
w
i
n
win
win局面, 一定存在
∃
\exists
∃ 一种操作, 使得
w
i
n
win
win局面可以变成
l
o
s
s
loss
loss局面;
当我们根据
w
i
n
win
win 去反推
l
o
s
s
loss
loss时, 要保证:
.
对于
l
o
s
s
loss
loss局面, 对于所有
∀
\forall
∀ 操作 (不管怎么操作),
l
o
s
s
loss
loss局面都必然变成
w
i
n
win
win局面;
例题
CSDN-129662569