尼姆博弈+SG函数

博弈这个东西真的很费脑诶..

尼姆博奕(Nim Game):游戏者轮流从一堆棋子(或者任何道具)中取走一个或者多个,最后不能再取的就是输家。当指定相应数量时,一堆这样的棋子称作一个尼姆堆

当n堆棋子的数量满足a1 xor a2 xor a3 xor.......xor an=0(Bouton's Theorem)时 为必败态,即先手必败(对于这种局面我们叫它奇异局面),对于尼姆博弈这种游戏,寻找必败态是非常重要的,那么对于必败态 有:

1.无法进行任何移动的自然是必败态

2.可以移动到必败态的是非必败态

3.必败态无论怎么操作都是非必败态,就是说如果自己处于必败态的话,无论怎么移动,都不可能赢(必败了嘛...迫真)。

对于a1 xor a2 xor a3 xor.......xor an=0做个解释:

1.对于(0,0,0)我们无法做出任何移动,先手必败,即0 xor 0 xor 0=0

2.如果对于某个局面(a1,a2,.....an),若a1 xor a2 xor a3 xor.......xor an=k(k≠0),那么k的二进制最高位的1必定来自于其中一个ai对应的的二进制位上的1,显然a1 xor k<=a1,那么只需要通过移动棋子将ai变为a1 xor k,那么等式变为a1 xor a2 xor a3 xor.......xor an xor k=k xor k=0,即可变为必败态

3.若处于某个局面(a1,a2.....an),,若a1 xor a2 xor a3 xor.......xor an=0,如果我们将ai变为ai',使得异或结果为0,但是由于异或满足消去律,那么对于a1 xor a2 xor a3....xor ai xor .....xor an=a1 xor a2 xor a3....xor ai' xor .....xor an,则说明ai=ai',该移动不合法(根本没移动好伐),与假设相矛盾

那么勉强证出来了。

对于取走棋子个数最多为m个的,只需将每堆棋子个数%(m+1)即可。

但是!如果问题突然蛇皮,比如有n堆石子,每次可以从第1堆石子里取1颗、2颗或3颗,可以从第2堆石子里取奇数颗,可以从第3堆及以后石子里取任意颗,那咋整啊,总不能电波求解吧

那肯定不是 这个时候SG(Sprague-Grundy)函数就开始发挥自己的作用了

定义P-position和N-position,分别表示先手必败的局面和后手必败的局面,p表示previous,n表示next,更严谨的定义是:1.无法进行任何移动的局面(也就是terminal position)是P-position;2.可以移动到P-position的局面是N-position;3.所有移动都导致N-position的局面是P-position。那么我们将这个游戏转化为图,给定一个有向无环图和一个起始点上的一个棋子,两个玩家分别在图上顺着有向边移动棋子,当无法移动时说明现在操作的玩家输了,我们可以将所有的组合游戏(Impartial Combinatorial Games),通过将每个局面看到一个顶点,每个局面和每个子局面以变换方式作为有向边相连,抽象成这个图模型,下面我们就在有向无环图的顶点上定义Sprague-Garundy函数。

首先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。

对于一个给定的有向无环图,定义关于图的每个顶点的SG函数g如下:g(x)=mex{ g(y) | y是x的后继 }

来看一下SG函数的性质。首先,所有没有出边的顶点(terminal position所对应的顶点),其SG值为0,因为它的后继集合是空集。然后对于一个g(x)=0的顶点x,它所有后继y都满足g(y)!=0。对于一个g(x)!=0的顶点,必定存在一个后继y满足g(y)=0。画个图大概会好懂一点。

那么顶点x所代表的postion是P-position当且仅当g(x)=0(跟P-positioin/N-position的定义的那三句话是完全对应的)。如果我们的点从g(x)=0处出发,要么一开始我们就无路可走,就是输了,要么我们可以走到下面的g(y)!=0处,那么y的后继中必有z使得g(z)=0,当对手将棋子移动到这里时,我们要么无路可走,要么重复上面的步骤,最终总会无路可走,毕竟无环,那么即为先手必败。那么我们通过计算有向无环图的每个顶点的SG值,就可以对每种局面找到必胜策略了。但是SG函数的用途远没有这么简单。如果将这个有向图游戏变复杂一点,比如说,有向图上并不是只有一枚棋子,而是有n枚棋子,每次可以任意选一枚进行移动,那么这个时候我们又如何去找到必胜策略呢?

让我们再来考虑一下顶点的SG值的意义。当g(x)=k时,表明对于任意一个0<=i<k,都存在x的一个后继y满足g(y)=i。也就是说,当某枚棋子的SG值是k时,我们可以把它变成0、变成1、……、变成k-1,但绝对不能保持k不变。是不是感觉和Nim游戏很像?Nim游戏的规则就是:每次选择一堆数量为k的石子,可以把它变成0、变成1、……、变成k-1,但绝对不能保持k不变。这表明,如果将n枚棋子所在的顶点的SG值看作n堆相应数量的石子,那么这个Nim游戏的每个必胜策略都对应于原来这n枚棋子的必胜策略!

对于n个棋子,设它们对应的顶点的SG值分别为(a1,a2,...,an),再设局面(a1,a2,...,an)时的Nim游戏的一种必胜策略是把ai变成k,那么原游戏的一种必胜策略就是把第i枚棋子移动到一个SG值为k的顶点。我们从Nim到SG,然后又从SG到了Nim,=- =这的确很神奇。

其实我们还是只要证明这种多棋子的有向图游戏的局面是P-position当且仅当所有棋子所在的位置的SG函数的异或为0。这个证明与上节的Bouton's Theorem几乎是完全相同的,只需要适当的改几个名词就行了。

刚才我们为了简化问题,将n枚棋子放在同一个有向图上移动,但如果是每个棋子在其对应的有向图上,每次任选一个棋子(就是任选一个有向图)进行移动,显然对结论也不会有什么影响。

所以我们可以定义有向图游戏的和(Sum of Graph Games):设G1、G2、……、Gn是n个有向图游戏,定义游戏G是G1、G2、……、Gn的和(Sum),游戏G的移动规则是:任选一个子游戏Gi并移动上面的棋子。Sprague-Grundy Theorem就是:g(G)=g(G1)^g(G2)^...^g(Gn)。也就是说,游戏的和的SG函数值是它的所有子游戏的SG函数值的异或。

再考虑之前说的:任何一个组合游戏(ICG)都可以抽象成一个有向图游戏。所以“SG函数”和“游戏的和”的概念就不是局限于有向图游戏。我们给每个ICG的每个position定义SG值,也可以定义n个ICG的和。所以说当我们面对由n个游戏组合成的一个游戏时,只需对于每个游戏找出求它的每个局面的SG值的方法,就可以把这些SG值全部看成Nim的石子堆,然后依照找Nim的必胜策略的方法来找这个游戏的必胜策略了!(Nim其实就是n个从一堆中拿石子的游戏求SG的变型,总SG=n个sg的异或)。

回到之前问题。有n堆石子,每次可以从第1堆石子里取1颗、2颗或3颗,可以从第2堆石子里取奇数颗,可以从第3堆及以后石子里取任意颗……我们可以把它看作3个子游戏,第1个子游戏只有一堆石子,每次可以取1、2、3颗,可以看出x颗式子的局面的SG值是x%4。(尽量自己画图试试)第2个子游戏也是只有一堆石子,每次可以取奇数颗,经过简单的画图可以知道这个游戏有x颗石子时的SG值是x%2。第3个游戏有n-2堆石子,就是一个Nim游戏。对于原游戏的每个局面,把三个子游戏的SG值异或一下就得到了整个游戏的SG值,然后就可以根据这个SG值判断是否有必胜策略以及做出决策了。其实看作3个子游戏还是保守了些,干脆看作n个子游戏,其中第1、2个子游戏如上所述,第3个及以后的子游戏都是“1堆石子,每次取几颗都可以”,称为“任取石子游戏”,这个超简单的游戏有x颗石子的SG值显然就是x。其实,n堆石子的Nim游戏本身不就是n个“任取石子游戏”的和吗?

所以,对于我们来说,SG函数与“游戏的和”的概念不是让我们去组合、制造稀奇古怪的游戏,而是把遇到的看上去有些复杂的游戏试图分成若干个子游戏,对于每个比原游戏简化很多的子游戏找出它的SG函数,然后全部异或起来就得到了原游戏的SG函数,就可以解决原游戏了。

解题模型:

1.把原游戏分解成多个独立的子游戏,则原游戏的SG函数值是它的所有子游戏的SG函数值的异或。

       即sg(G)=sg(G1)^sg(G2)^...^sg(Gn)。

2.分别考虑没一个子游戏,计算其SG值。

     SG值的计算方法:

1.可选步数为1~m的连续整数,直接取模即可,SG(x) = x % (m+1);

2.可选步数为任意步,SG(x) = x;

3.可选步数为一系列不连续的数,用模板计算。(我比较倾向dfs..

模版1:打表

 1 /* 1.可选步数为1~m的连续整数,直接取模即可,SG(x) = x % (m+1);
 2 2.可选步数为任意步,SG(x) = x;
 3 3.可选步数为一系列不连续的数,用GetSG计算 */
 4 //f[]:可以取走的石子个数,f[0]表示有几种取法
 5 //SG[]:0~n的SG函数值
 6 //vis[]:mex{}
 7 int f[105], SG[MAXN], vis[MAXN];
 8 void Get_SG(int n)
 9 {
10     memset(SG, 0, sizeof(SG)); //SG[0]必为0
11     for (int i = 1; i <= n; i++)
12     {
13         memset(vis, 0, sizeof(vis));
14         for (int j = 1; j <= f[0]; j++)
15         {
16             if (i < f[j])
17                 break;
18             else
19                 vis[SG[i - f[j]]] = 1;
20         }
21         for (int j = 0; j <= n; j++)
22             if (!vis[j])
23             {
24                 SG[i] = j;
25                 break;
26             }
27     }
28 }
View Code

模版2:DFS

 1 //注意 S数组要按从小到大排序 SG函数要初始化为-1 对于每个集合只需初始化1遍
 2 //n是集合s的大小 S[i]是定义的特殊取法规则的数组
 3 int s[110],sg[10010],n;
 4 int SG_dfs(int x)
 5 {
 6     int i;
 7     if(sg[x]!=-1)
 8         return sg[x];
 9     bool vis[110];
10     memset(vis,0,sizeof(vis));
11     for(i=0;i<n;i++)
12     {
13         if(x>=s[i])
14         {
15             SG_dfs(x-s[i]);
16             vis[sg[x-s[i]]]=1;
17         }
18     }
19     int e;
20     for(i=0;;i++)
21         if(!vis[i])
22         {
23             e=i;
24             break;
25         }
26     return sg[x]=e;
27 }
View Code

 附上HDU-1538http://acm.hdu.edu.cn/showproblem.php?pid=1536

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 typedef long long ll;
 4 typedef unsigned long long ull;
 5 #define INF 0x3f3f3f3f
 6 const ll MAXN = 1e4 + 7;
 7 const ll MOD = 1e9 + 7;
 8 const double pi = acos(-1);
 9 /* 1.可选步数为1~m的连续整数,直接取模即可,SG(x) = x % (m+1);
10 2.可选步数为任意步,SG(x) = x;
11 3.可选步数为一系列不连续的数,用GetSG计算 */
12 //注意 S数组要按从小到大排序 SG函数要初始化为-1 对于每个集合只需初始化1遍
13 //n是集合s的大小 S[i]是定义的特殊取法规则的数组
14 int s[110], sg[10010], n;
15 int SG_dfs(int x)
16 {
17     int i;
18     if (sg[x] != -1)
19         return sg[x];
20     bool vis[110];
21     memset(vis, 0, sizeof(vis));
22     for (i = 0; i < n; i++)
23     {
24         if (x >= s[i])
25         {
26             SG_dfs(x - s[i]);
27             vis[sg[x - s[i]]] = 1;
28         }
29     }
30     int e;
31     for (i = 0;; i++)
32         if (!vis[i])
33         {
34             e = i;
35             break;
36         }
37     return sg[x] = e;
38 }
39 int main()
40 {
41     int k;
42     while (~scanf("%d", &k), k)
43     {
44         n=k;
45         string str = "";
46         for (int i = 0; i < k; i++)
47             scanf("%d", &s[i]);
48         sort(s, s + k);
49         int m;
50         memset(sg, -1, sizeof(sg));
51         scanf("%d", &m);
52         for (int i = 0; i < m; i++)
53         {
54             int ans = 0;
55             int n;
56             scanf("%d", &n);
57             for (int j = 0; j < n; j++)
58             {
59                 int c;
60                 scanf("%d", &c);
61                 ans ^= SG_dfs(c);
62             }
63             if (ans)
64                 str += "W";
65             else
66                 str += "L";
67         }
68         cout << str << endl;
69     }
70     return 0;
71 }
原文地址:https://www.cnblogs.com/graytido/p/10771907.html