博弈论 | 详解搞定组合博弈问题的SG函数
本文始发于个人公众号:TechFlow,原创不易,求个关注
今天这篇是算法与数据结构专题的第27篇文章,我们继续深入博弈论问题。今天我们要介绍博弈论当中非常重要的一个定理和函数,通过它我们可以解决许多看起来杂乱无章的博弈问题,使得我们可以轻松地解决一大类博弈问题。
有了SG函数和SG定理,我们不再是单纯地通过构思、分析和找规律去解决问题了。并且我们之前学过的巴什博奕、威佐夫博弈以及Nim博弈都可以使用SG函数来解决,相当于我们找到了这一大类问题的通解。下面,我们来看几个基本定理和基本概念。
基本定理
ICG游戏
前面我们说了,SG函数和SG定理可以解决一大类的博弈问题。这一大类的博弈问题称为ICG游戏,我们之前介绍过的三种博弈模型,本质上都属于ICG游戏。
关于ICG游戏,它的定义如下,需要满足三个条件:
游戏有两人参与,两人轮流做出决策,并且两人做出的决策都是对自己最优的 当有一人无法决策的时候,该人失败。无论两人如何决策,该游戏都必然会在有限时间内结束 游戏中同一个状态不能达到多次,且游戏没有平局。游戏者在某个确定状态做出的决策集合只与状态有关,与游戏者无关
必胜态与必败态
也就是奇异状态与非奇异状态,我们定义P状态是必败态,N状态是必胜态。我们可以简单理解成,在P状态的玩家一定会输,而在N状态的玩家一定会赢。
这一点在之前的Nim取子的文章当中我们曾经深入地分析过,展开来说,其实也有三条:
无法移动的状态为P状态 可以移动到P状态的状态为N 所有移动都会进入N局面的局面为P
我们曾经在分析威佐夫博弈问题的时候,将游戏局面抽象成了二维平面坐标系当中的点。其实所有ICG游戏都可以想象成一张有向无环图(DAG),游戏开始时有一颗放在起点的棋子,两个玩家轮流移动棋子,直到不能移动的玩家落败。所有只能移动到终点的局面都是必胜的,所有只能连接必胜点的点是必败的。我们用递归的思路可以计算出所有点的状态。
当然我们用算法去搜索遍历所有状态这耗时太多了,我们可以通过一个函数来计算它,这就是我们今天文章讨论的重点——SG函数。
Sprague-Grundy数的推导
SG是Sprague-Grundy的缩写,我没有记错,这应该是两个人名,它使用起来非常简单,但是推导过程有些复杂。如果我们忽略推导过程直接去研究它的使用的话,你会有一种在运用魔法的感觉。因为你完全猜测不到它其中的原理,所以我们需要详细解释一下它的推导过程,这样才能加深理解。
我们先明确几个概念,首先对于ICG游戏来说,失败的最终状态只有一个,就是无法移动的点,可以认为是DAG图中的终点。从这个终点倒推,所有能够直接连接终点的点是N点。这个很好理解,假如在当前的状态当中,可以移动到一个对手的必败状态,那么对于当前玩家当然是必胜的。
我们对这些状态做一个简单的分层,直接可以连接P点的点是一级胜态。比如nim游戏当中的(1, 0)状态就是一级胜态,它只能通往P点。我们把可以变成败态也可以进入一级胜态的点称为二级胜态,比如nim游戏当中的(0, 2)。比如它进入(1, 0)便是一级胜态,而也可以直接进入(0, 0)变成败态,我们把这样的状态称为二级胜态。类似的,如果一个胜态可以变成败态也可以变成1至n-1级的所有胜态,则称为n级胜态,败态可以认为是0级胜态。
接着我们来看胜态的组合,我们可以把Nim取子问题中的结论照搬过来。两个同级的胜态组合是败态,两个不同级的胜态组合是胜态。原因也很简单,就和Nim取子问题中面临两堆同样多的石子必败一样。因为后手可以拷贝先手的操作,直到无法继续操作,游戏结束。而两个不同的胜态,先手可以将其中一个转化成和另一个一样,从而让后手面临两个一样的胜态,所以不同的胜态组合是胜态。
注意这个结论要成立,有两个前提条件,第一个是一个n级胜态可以转移到1 - n-1级任意胜态。第二个是胜态级数只能降低不能升高,其实能升高也没问题,后手可以将先手升高的级数再降低回去,并不会影响结论。
如果你理解了这一层,其实我们对级数的定义其实就是SG函数的值。SG函数能够用来将一个状态映射成一个非负整数的值。它的公式写成这样:
式子中的A和B表示状态,表示A状态可以达到B状态。mex是一个定义在集合上的函数,返回的是不属于这个集合的最小非负整数。比如mex(0, 1)= 2,mex(0, 2) = 1, mex(0,1, 2, 3)=4。也就是说我们可以通过A能达到的状态的SG函数值推算出A的SG值。
比如在Nim取子问题当中,没有石子是必败,它没有后继状态,所以SG(0) = 0。1颗石子的时候可以移动到0,所以SG(1) = mex{SG(0)} = 1。这样我们就把Nim取子问题和ICG问题当中胜态的级数对应起来了。一个SG数对应Nim当中的一个石子堆,如果我们有多个石子堆,我们怎么计算开始时候的胜负状态?通过Nim取子问题的推导,我们知道是计算各个石子堆石子数量的亦或值,如果亦或之后的结果为0,那么先手必败,否则先手必胜。同样我们计算所有状态的SG值,如果最后得到的SG值亦或的结果为0,说明先手必败,否则先手必胜。
关于SG(A + B) = SG(A) xor SG(B)我们可以用数学归纳法来证明,但这个证明没什么必要,我们更重要的是理解SG函数的思想和推导过程。最后,我们利用SG函数来看一道例题。
例题实战
我们来看一道改进版的Nim取子问题,假设有n堆石子,每堆当中有若干个石子。现在两个人轮流从其中取石子,一个人可以选择从任意一堆石子当中取走若干个石子,或者是选择一堆大于1颗石子的石堆将它拆分成两堆。最后无法取走石子的人落败,请问给定每堆石子的数量,谁会获胜。
这题如果去除掉石堆可以拆分的限制,那么它就是一道裸的Nim取子问题。但是加上了限制之后,我们就无法直接使用Nim取子的规则来求解了。但是我们分析一下会发现,这个虽然加上了限制条件,但是仍然满足ICG游戏的限制,所以我们可以使用SG函数来求解。
对于状态N来说,它可以转移到0-N-1任意状态,并且可以拆分成i和N-i两个状态。根据上文的公式:SG(A+B) = SG xor SG(B),所以我们SG(N)而言,它可以转移到0-N-1状态,**以及(i, N-i)**状态。
根据状态之间的关系,我们可以很容易写出求解SG函数的代码:
sg_arr = [0 for _ in range(50)]
def mex(nums):
# 返回第一个不在nums当中的自然数
if len(nums) == 0:
return 0
for i in range(1, len(nums)+1):
if i not in nums:
return i
return len(nums) + 1
def sg(n):
sgs = []
# 记录下0-N-1状态
for i in range(n):
sgs.append(sg_arr[i])
# N也可以达到(i, N-i)状态,SG(i, N-i) = SG(i) ^ SG(N-i)
for i in range(1, n):
ret = sg_arr[i] ^ sg_arr[n-i]
if ret not in sgs:
sgs.append(ret)
# 通过mex函数求解出sg[n]
sg_arr[n] = mex(sorted(sgs))
print(sg_arr[n])
由于我们计算后面的SG值也需要用到前面的SG值,所以我们需要将之前的答案存储在数组当中。否则的话,如果用递归执行的话会非常耗时。实际上我们计算每一个数的SG值本身也非常耗时,尤其是N很大的时候。所以这个时候可以采取取巧的办法,就是打出一些状态的SG值来进行观察,寻找其中的规律。
打表找规律这种方法不甚高明,但是在比赛当中经常使用。
我们打印出一些SG值之后,可以发现对于n而言,如果n % 4 == 0,那么SG(n) = n-1, n % 4 == 3, SG(n) = n+1,否则SG(n) = n。
利用这个规律,我们可以直接得到SG值,把每堆石子的SG值亦或在一起就是最终的答案了。
总结
到这里,我们关于博弈论当中SG函数值的使用就介绍完了。虽然我们用了很多笔墨去说明其中的原理,但是对于初学者而言,估计还是蒙圈的。这是非常正常的,博弈论问题本身就比较考验思维,加上SG函数的推导过程也不是很直观,初学会觉得烧脑和想不明白也是肯定的。
有一个技巧是抓住SG值和Nim取子游戏这个模型的对应关系,从Nim游戏入手,会简单一些。实际上SG值最初也的确是从Nim取子游戏当中推导出来的。
理解了SG函数之后,就足够我们解决绝大多数博弈论算法问题了。这也是博弈论领域非常重要的概念和方法,希望大家都能理解。
今天的文章就到这里,如果喜欢本文,可以的话,请点个关注,给我一点鼓励,也方便获取更多文章。
本文使用 mdnice 排版
博弈论 | 详解搞定组合博弈问题的SG函数的更多相关文章
- 详解如何在Laravel中增加自定义全局函数
http://www.php.cn/php-weizijiaocheng-383928.html 如何在Laravel中增加自定义全局函数?在我们的应用里经常会有一些全局都可能会用的函数,我们应该怎么 ...
- 【转】博弈问题及SG函数(真的很经典)
博弈问题若你想仔细学习博弈论,我强烈推荐加利福尼亚大学的Thomas S. Ferguson教授精心撰写并免费提供的这份教材,它使我受益太多.(如果你的英文水平不足以阅读它,我只能说,恐怕你还没到需要 ...
- (转)博弈问题与SG函数
博弈问题若你想仔细学习博弈论,我强烈推荐加利福尼亚大学的Thomas S. Ferguson教授精心撰写并免费提供的这份教材,它使我受益太多.(如果你的英文水平不足以阅读它,我只能说,恐怕你还没到需要 ...
- 转载--博弈问题及SG函数(真的很经典)
博弈问题若你想仔细学习博弈论,我强烈推荐加利福尼亚大学的Thomas S. Ferguson教授精心撰写并免费提供的这份教材,它使我受益太多.(如果你的英文水平不足以阅读它,我只能说,恐怕你还没到需要 ...
- 博弈问题之SG函数博弈小结
SG函数: 给定一个有向无环图和一个起始顶点上的一枚棋子,两名选手交替的将这枚棋子沿有向边进行移动,无法移 动者判负.事实上,这个游戏可以认为是所有Impartial Combinatorial Ga ...
- jQuery动画高级用法(上)——详解animation中的.queue()动画队列插队函数
决定对animate方面做一些总结,希望能给大家一些启发和帮助 从一个实际应用谈起 今天不谈animate().fadeIn().fadeOut().slideUp().show().hide()诸如 ...
- 详解C#中通过委托来实现回调函数功能的方法
委托:类型安全的指向函数的指针 使用步骤 1:声明一个委托 delegate string DelString(string s) 2:定义一个委托变量 DelString del = new Del ...
- hdu 1847 博弈基础题 SG函数 或者规律2种方法
Good Luck in CET-4 Everybody! Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K ...
- 201709019工作日记--sleep、wait、notify的使用详解
1. sleep()和wait()的区分 (1)这两个方法来自不同的类分别是,sleep来自Thread类,wait来自Object类. sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在 ...
随机推荐
- C# 反射详解一
首先反射是基于System.Reflection命名空间下,.Net框架提供的帮助类库,可以读取并使用metadata(元数据:描述对象信息的数据). 我们再来看下代码生成编译的总过程. 编译器编译( ...
- 真香,撸一个SpringBoot在线代码修改器
前言 项目上线之后,如果是后端报错,只能重新编译打包部署然后重启:如果仅仅是前端页面.样式.脚本修改,只需要替换到就可以了. 小公司的话可能比较自由,可以随意替换,但是有些公司权限设置的比较严格,需要 ...
- [C#.NET 拾遗补漏]04:你必须知道的反射
阅读本文大概需要 3 分钟. 通常,反射用于动态获取对象的类型.属性和方法等信息.今天带你玩转反射,来汇总一下反射的各种常见操作,捡漏看看有没有你不知道的. 获取类型的成员 Type 类的 GetMe ...
- 无监督LDA、PCA、k-means三种方法之间的的联系及推导
\(LDA\)是一种比较常见的有监督分类方法,常用于降维和分类任务中:而\(PCA\)是一种无监督降维技术:\(k\)-means则是一种在聚类任务中应用非常广泛的数据预处理方法. 本文的 ...
- 8000字长文让你彻底了解 Java 8 的 Lambda、函数式接口、Stream 用法和原理
我是风筝,公众号「古时的风筝」.一个兼具深度与广度的程序员鼓励师,一个本打算写诗却写起了代码的田园码农! 文章会收录在 JavaNewBee 中,更有 Java 后端知识图谱,从小白到大牛要走的路都在 ...
- C# 9.0 新特性之模式匹配简化
阅读本文大概需要 2 分钟. 记得在 MS Build 2020 大会上,C# 语言开发项目经理 Mads Torgersen 宣称 C# 9.0 将会随着 .NET 5 在今年 11 月份正式发布. ...
- (三)JavaMail发送附件
代码如下: package cases; import com.sun.mail.util.MailSSLSocketFactory; import javax.activation.DataHand ...
- numpy中transpose的功能
看了网上一堆解释,有用相互交换来解释的,我看了半天也看不出所以然来.心想着自己试验一下. numpy.transpose的用法很简单:假如你有一个四维的数组,那么四个维度就是0,1,2,3.风格会像下 ...
- SpringBoot 启动配置原理
几个重要的事件回调机制 ApplicationContextInitializer SpringApplicationRunListener ApplicationRunner CommandLine ...
- 第二章、Web中使用shiro(实现登陆)
虽然Apache Shiro的核心设计目标允许它用于保护任何基于JVM的应用程序(如命令行应用程序,服务器守护程序,Web应用程序等),但本指南将着重讨论最常见的用例:确保运行的Web应用程序一个Se ...