本文始发于个人公众号:TechFlow,原创不易,求个关注

今天是算法与数据结构专题26篇文章,我们来看看一个新的博弈论模型——Nim取子问题。

这个博弈问题非常古老,延续长度千年之久,一直到20世纪初才被哈佛大学的一个数学家找到解法,可见其思维的难度。但是这个问题本身却很有意思,推导的过程更是有趣,哪怕你没有多少数据基础也一定可以看明白。

Nim取子问题

这个问题的题面是这样的,我们有3堆石子,有A和B两个人轮流从其中的一堆取石子。规定每个人每次最少取1颗,最多可以取完当前堆,无法继续拿取石子的人落败。请问如果你是先手,你有必胜策略吗?

根据我们之前分析威佐夫博弈问题的套路,我们需要先来分析一下问题,找到一些典型的局面。比如说(0, 0, 0)对于先手来说一定是必败的,同理,对于一个(0, n, n)的局面,也一样是必败的。因为不论先手怎么取石子,后手只需要在另外一堆石子当中如法炮制,那么留给先手的依然是一个(0, n, n)的局面。在博弈论问题当中,我们通常会将先手必败的局面称为奇异局势

那么这些奇异局势之间有没有什么关联呢?我们能不能找到这些局面之间的联系或者是公式呢?

我们光是靠脑子想或者是用纸笔去罗列我们所能想到的奇异局面是很难想出来的,不然也不会困扰人们长达一千多年了。但是这个问题的谜底却又如此简单,简单到让人不可思议。

首先,我们先来思考一个问题,这个问题之所以复杂,根本原因在于石子有3堆,而不是两堆。如果石子有两堆,那么就很容易了,先手除非面临两堆石子相等的情况,否则必胜。因为它可以通过拿取石子留下两堆一样的给后手,这样不论后手如何拿取,先手只需要在另一堆当中采取同样的操作,就必然可以给后手留下奇异局势。这和我们刚才分析的(0, n, n)的局面是一样的。

但是题目明确说了是3堆而不是两堆,我们不禁就开始设想起了一个问题,我们能不能想到一种策略,使得可以将三堆石子”转化“或者是看成是两堆石子呢?这样我们就可以非常容易地判断石子的输赢情况了。

解法分析

明明是3堆石子,怎么看成是两堆呢?怎么看都是自说自话,但如果你对二进制熟悉的话,你会发现这个问题可能并不是不可能的。

是的,二进制就是天生的二维“生物”,在二进制的世界当中,一切都只有两种,0和1。所以从直观上我们会觉得,也许可以将石子的数量和二进制取得关联。也许这样的关联会有助于我们找到解法。剩下的问题就成了,这个关联究竟是什么?

我们来思考另外一个问题,对于一堆石子来说,我们取走一个数量,石子的数量会减少,这是显而易见的。体现在石子的总数上,就是表示这堆石子数量的数字,减去了另外一个数字。这个是减法的操作,小学生都知道。但是小学生不知道的是,减法在二进制当中是怎么进行的,或者是它有什么规律呢?

我们先不急着回答,先来仔细分析一波。首先,减数和被减数都可以化作是二进制,也就是若干个1和0组成的数字。我们假设减数每一个为1的二进制位对应的被减数的值也是1,那么这个减法会进行得非常顺利。对应的就是从被减数当中移除掉若干个1的过程。

举个例子,被减数是9,减数是1。我们都知道9写成二进制是1001,而1的二进制是1。所以被减数减去减数的值为8,也就是1000,可以看成是1001移除了末尾的1。

如果减数存在二进制位被减数为0,比如10 - 3的情况,10的二进制是1010,3是11。很明显3的第0位是1,而10是0,这种情况下怎么办?首先,我们先把3和10当中都是1的二进制位去除。剩下的就是1000 减去 1,那么我们可以先把1000 减1 变成111,这样就回到了上面说的第一种情况,完成减法之后再加回来,所以得到的结果就是111,这其实就是一个向高位借位的过程。纵观整个减法的计算过程,其实就是被减数当中二进制位变化的过程,减去某一个数,等价于将被减数当中若干个0变成1,1变成0。

结合二进制,我们可以想到一种策略。就是统计这3个数所有的二进制位,由于我们有3个数,所以每一个二进制位最多有3个1,最少有0个1。如果每一位的1的数量和都是偶数,也就是不是0就是2的话,那么这一定是一个奇异局面。

举个例子,比如[10, 8, 2]是一个奇异局面,我们把它们写成二进制。10的二进制是1010,8的二进制是1000,2的二进制是10。所以我们可以发现这三个数的二进制位加起来,第1、2、3位都出现了两个1。这个时候先手不论如何操作,后手只需要保证剩下的三个数的二进制位维持这个特性即可。这样做可以保证最后一次拿取结束之后,给先手留下[0, 0, 0]的局面。本质上来说,它的原理和两堆石子的时候是一样的,只不过转化了一种形式。

举个例子,比如我们从10当中拿走3颗石子,得到(7, 8, 2),我们观察二进制位分别是111, 1000, 10。会发现每一位1的数量从低到高分别是[1, 2, 1, 1]。所以我们可以从1000拿取3个石子,保证留下的数量是101,也就是5。这样剩下的1的个数就是[2, 2, 2],依然是偶数。所以先手不论如何拿,后手都可以保证一定可以让留下的数字在二进制上保持偶数,先手一定必败。在不满足这个条件的局面当中先手一定必胜,因为先手可以在第一次通过拿取掉多余的1,保证留下一个必败的局面给后手。

这也是这题的解法,即通过二进制位来判断是否先手必胜。我们要判断每个二进制位当中出现的1的次数和是否是偶数,可以通过位运算的亦或来完成。在亦或操作当中,对每一个二进制位进行计算,奇数为1,偶数为0。所以我们只需要计算一下这三堆石子亦或之后的结果是否为0,就可以知道是否每一个二进制位的1的数量是否都是偶数了。

我们写成代码非常简单,我们通常用^这个符号表示亦或运算,那么代码只需要一行:

  1. def win_or_lose(a, b, c):
  2. return (a ^ b ^ c) == 0

推广以及证明

这里还没有结束,我们同样可以将3堆石子的局面推广到n堆,不管游戏当中玩家面临的是多少堆石子,这个结论依然都是成立的。这个成立的原因我们很容易想明白,为了严谨起见,我们可以用博弈问题常用的证明套路来证明一下。

在一个博弈问题当中,如果存在奇异局面,也就是必败局面,那么一定满足三个条件。第一个条件是无法进行任何操作的局面是奇异局面。第二个条件是可以移动到奇异局面的局面是非奇异局面。第三个条件是在奇异局面当中所作的任何操作得到的都是非奇异局面。

只要能够证明这三点,就可以证明我们的思路是正确的。

对于第一点毋庸置疑,所有石堆都没有石子的时候无法移动,这是必败状态。

我们来看第二个条件,我们假设这n堆石子的数量是a1, a2, ... an。如果当前局面是非奇异局面,根据我们的理论,那么a1 ^ a2 ^ a3 ^... ^an > 0。也就是说存在某个二进制位1的数量是奇数。

我们假设a1 ^ a2 ^ a3 ^... ^an = k,那么必然可以找到一个ai, 使得它的二进制表示在k的最高位上是1,因为k的所有二进制的1都是从这n个数当中来的,所以这样的ai一定存在。那么我们可以继续推导得到:ai ^ k < ai。因为最高位的1经过亦或之后变成了0,所以亦或操作之后一定是减小的。我们令p = ai ^ k,我们在a1^a2^a3^...^an = k 的等式两边同时亦或ai,可以得到a1 ^ a2 ^ ...^ai-1^ai+1^...^an = k ^ai,所以a1 ^ a2 ^ ...^ p ^...an = 0。

第三个条件也很好证明,因为如果当前是必败局面,也就是说a1 ^ a2 ^ ... ^ an=0。我们假设我们将an转变成了p之后依然有a1 ^ a2 ^ ... ^p=0, p < an。我们在等式两边同时亦或上p和an,可以得到:an ^ p = 0,也就是说p = an。这与p < an矛盾,所以不存在这样的转化使得奇异局面操作之后仍然是奇异局面。

这样我们就从数学上证明了这个推理的正确性,实际上已经有人对Nim取子问题有过深入的研究,这也是一个已经得到过证明的定理,叫做Bouton定理。定理的内容是先手可以在非平衡的Nim博弈中取胜,而后手可以在平衡的Nim博弈中取胜。这里的平衡就是指的是所有二进制位1的数量是偶数。

那么我们写出代码也非常简单:

  1. def win_or_lose(nums):
  2. ret = 0
  3. for i in nums:
  4. ret ^= i
  5. return ret == 0

总结

到这里,关于Nim博弈的问题就讲完了。通过亦或操作去判断的解法真的是非常简单,但是这其中的推导过程想明白却不容易。我看过很多博客,都是直接给出的亦或这个结论,很少能够看到详细的推导过程。直接记住结论是简单的,但也很容易忘记,只有亲自推导一遍,才会明白亦或这个神奇的操作是怎么来的,为什么它可以解决Nim博弈的问题。

在整个思考推理和证明的过程当中,我们大量使用了亦或这个位运算操作,如果对它不熟悉的同学可能会看起来有些困扰。建议可以先了解学习一下二进制当中亦或的性质之后再来阅读本文,效果会更好。

目前为止,我们已经介绍完了巴什博奕、威佐夫博弈和Nim博弈这三种相对比较简单的博弈模型。在后续的文章当中,我们将会继续深入博弈论这个问题,一起去研究更加困难的博弈论问题,看看在复杂的场景当中,我们怎么样寻找奇异状态。

文章就到这里,如果喜欢本文,可以的话,请点个关注,给我一点鼓励,也方便获取更多文章。

本文使用 mdnice 排版

博弈论Nim取子问题,困扰千年的问题一行代码解决的更多相关文章

  1. dede取子栏目时重复显示同级栏目的终极解决方法

    使用channelartlist标签时,当栏目没有子栏目是,会出现重复同级栏目的问题,解决方法如下: 先看下面的代码{dede:channelartlist typeid='2'}  {dede:ty ...

  2. 萌新笔记之Nim取石子游戏

    以下笔记摘自计算机丛书组合数学,机械工业出版社. Nim取石子游戏 Nim(来自德语Nimm!,意为拿取)取石子游戏. 前言: 哇咔咔,让我们来追寻娱乐数学的组合数学起源! 游戏内容: 有两个玩家面对 ...

  3. jquery 取子节点及当前节点属性值

    分享下jquery取子节点及当前节点属性值的方法. <li class="menulink"><a href="#" rel="ex ...

  4. phpcms直接取子栏目的内容、调用点击量的方法

    子栏目里面的内容可以直接取,而不需要通过循环. {$CATEGORYS[$catid][catname]}//取子栏目的栏目名称 {$CATEGORYS[$catid][image]}//取子栏目的栏 ...

  5. 51nod1069【Nim取石子游戏】

    具体看:萌新笔记之Nim取石子游戏可以这么写: #include <bits/stdc++.h> using namespace std; typedef long long LL; in ...

  6. BZOJ.1299.[LLH邀请赛]巧克力棒(博弈论 Nim)

    题目链接 \(Description\) 两人轮流走,每次可以从盒子(容量给定)中取出任意堆石子加入Nim游戏,或是拿走任意一堆中正整数个石子.无法操作的人输.10组数据. \(Solution\) ...

  7. 使用Python爬取淘宝两千款套套

    各位同学们,好久没写原创技术文章了,最近有些忙,所以进度很慢,给大家道个歉. 警告:本教程仅用作学习交流,请勿用作商业盈利,违者后果自负!如本文有侵犯任何组织集团公司的隐私或利益,请告知联系猪哥删除! ...

  8. JAVA中取子字符串的几种方式

    有这样一串字符串:String s = "共 100 页, 1 2 3 4..."; 假如我想把"100"给取出来,该如何做? 方法一: 采用split的方式 ...

  9. Jquery-获取子元素children,find

    1.查找子元素方式1:> 例如:var aNods = $("ul > a");查找ul下的所有a标签 2.查找子元素方式2:children() 3.查找子元素方式3 ...

随机推荐

  1. pip安装Python库速度慢的解决方法

    最近在写大数据文本挖掘的考查报告,需要用到 jieba切词,于是在pycharm中安装 jieba 库 首先是在 File—settings中通过搜索安装,然而安了五分钟之后还是失败了 于是通过终端输 ...

  2. 注解@NotNull/@NotEmpty/@NotBlank

    @NotNull:不能为null,但可以为empty @NotEmpty:不能为null,而且长度必须大于0 @NotBlank:只能作用在String上,不能为null,而且调用trim()后,长度 ...

  3. Java实现 LeetCode 546 移除盒子(递归,vivo秋招)

    546. 移除盒子 给出一些不同颜色的盒子,盒子的颜色由数字表示,即不同的数字表示不同的颜色. 你将经过若干轮操作去去掉盒子,直到所有的盒子都去掉为止.每一轮你可以移除具有相同颜色的连续 k 个盒子( ...

  4. Java实现 LeetCode 457 环形数组循环

    457. 环形数组循环 给定一个含有正整数和负整数的环形数组 nums. 如果某个索引中的数 k 为正数,则向前移动 k 个索引.相反,如果是负数 (-k),则向后移动 k 个索引.因为数组是环形的, ...

  5. Java实现 LeetCode 337 打家劫舍 III(三)

    337. 打家劫舍 III 在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区.这个地区只有一个入口,我们称之为"根". 除了"根"之外,每 ...

  6. 小程序-你不得不知的Promise封装请求

    放在开头 这是一个小程序的轮播图,但是为我们在请求api数据时,将请求wx.request代码封装调用 效果展示 代码篇 页面wxml 这里需要注意的是我们设置swiper和image标签时,有默认属 ...

  7. [OpenGL](翻译+补充)投影矩阵的推导

    1.简介 基本是翻译和补充 http://www.songho.ca/opengl/gl_projectionmatrix.html 计算机显示器是一个2D的平面,一个3D的场景要被OpenGL渲染必 ...

  8. 5分钟速成Markdown

    一.认识 Markdown Markdown 是一种用来写作的轻量级「标记语言」,它用简洁的语法代替排版,而不像一般我们用的字处理软件 Word 或 Pages 有大量的排版.字体设置.它使我们专心于 ...

  9. Rust异步之Future

    对异步的学习,我们先从Future开始,学习异步的实现原理.等理解了异步是怎么实现的后,再学习Rust异步编程涉及的2个库(futures.tokio)的时候就容易理解多了. Future rust中 ...

  10. Java 入门教程

    Java 入门教程 Java 是由Sun Microsystems公司于1995年5月推出的高级程序设计语言. Java可运行于多个平台,如Windows, Mac OS,及其他多种UNIX版本的系统 ...