博弈学习 3 - SG函数与“游戏的和”
链接 1 :http://blog.csdn.net/logic_nut/article/details/4711489
链接 2 :http://blog.sina.com.cn/s/blog_83d1d5c70100y9yd.html
链接 3 :http://blog.csdn.net/luomingjun12315/article/details/45479073
继续总结~~~
上一次学到了 Nim 游戏,
并且了解了找出必胜策略的方法。
通常的Nim游戏的定义是这样的:
有若干堆石子,每堆石子的数量都是有限的,合法的移动是“选择一堆石子并拿走若干颗(不能不拿)”,
如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动)。
但如果把Nim的规则略加改变,
比如说:有 n堆石子,每次可以从第1堆石子里取1颗、2颗或3颗,可以从第2堆石子里取奇数颗,可以从第3堆及以后石子里取任意颗…应该如何解题。
(1)Sprague-Garundy函数:
现在我们来研究一个看上去似乎更为一般的游戏:
给定一个有向无环图和一个起始顶点上的一枚棋子,两名选手交替的将这枚棋子沿有向边进行移动,
无法移动者判负。
也就是把游戏抽象成一个有向图。
可以通过把每个局面看成一个顶点,对每个局面和它的子局面连一条有向边来抽象成这个 "有向图游戏 "。
下面就在有向无环图的顶点上定义Sprague-Garundy函数。
Sprague-Grundy函数,在此简称 sg函数。
首先定义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 移动到的点,即后继节点}。
引用链接 2 中的例子:
如果在取子游戏中每次只能取{1,2,3},那么各个数的SG值是多少?
x 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14. . .
g(x) 0 1 2 3 0 1 2 3 0 1 2 3 0 1 2 . . .
利用上次学到的结论也可以推出上表 , 但这次要学习的是 如何计算 Sg 函数。
由于没有找到求 sg 函数值的具体过程,所以就按照定义自己脑补了,如有不对望指正 (^-^)
g(0) = mex{ 空集 } = 0;
因为 0 不能拿走任何棋子,没有后继 ;
g(1) = mex{g(0)} = 1;
因为合法的移动集合是{1,2,3},当还有 1 颗石子时,可以拿 1,那么 他的后继节点就是 0 ,
又 g(0) = 0; 所以 g(1) = mex{ g(0)} = mex{ 0 } = 1;
g(2) = mex{g(1),g(0)} = mex{0,1} = 2 ; ( g(1),g(0)在上面已算出)
之后的同理可得。
(2) “游戏的和”
引用链接 1 原文:
再考虑在本文一开头的一句话:任何一个ICG都可以抽象成一个有向图游戏。
所以“SG函数”和“游戏的和”的概念就不是局限于有向图游戏。
我们给每个 ICG的每个position定义SG值,也可以定义n个ICG的和。
所以说当我们面对由n个游戏组合成的一个游戏时,只需对于每个游戏找出求它的每个局面的SG值的方法,
就可以把这些SG值全部看成Nim的石子堆,然后依照找Nim的必胜策略的方法来找这个游戏的必胜策略了!
回到本文开头的问题。有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函数,就可以解决原游戏了。
(3) 模板
(1) 求出 1 -n 范围的 sg 值
/* s 数组表示移动集合,即可以走的步数或可以取石子的数量;k 表示集合大小; s 数组要从小到大排序,以保证更方便的找到它的所有后继 sg 数组保存 sg 值; vis 数组标记已访问的点; 另: 1.如果可以移动的步数为 1-m 的连续整数,那么它的 sg 值就是 g(x) = x % (m+1) ; */ int sg[maxn],vis[maxn],s[maxn],k; void getsg() { for(int i=0;i<=n;i++){ // g(x)=mex{ g(y) | y是能够由 x 移动到的点,即后继节点}。 memset(vis,0,sizeof(vis)); for(int j=0;s[j] <= i && j<k;j++){ // 找到它所有能够移动到的点(即后继)并标记 vis[sg[x-s[i]]] = 1; } for(int j = 0;;j++){ // 找到最小的不属于集合 g(y) 的非负整数 if(vis[j] == 0){ sg[i] = j; break; } } } }
(2) 得到单个 sg 值
int sg[maxn],s[maxn],k; int getsg(int x) /// 得到单个 sg 值 { int hash[110] = {0}; for(int i=0;s[i]<=x && i<k;i++){ if(sg[x-s[i]] == -1) sg[x-s[i]] = getsg(x-s[i]); hash[ sg[x-s[i]]] = 1; } for(int i=0;;i++){ if(hash[i] == 0) return i; } }
题目: HDU1847 1848 1536
HDU1847
题意:
KiKi 和 CiCi 玩游戏,总共n张牌;
双方轮流抓牌;
每人每次抓牌的个数只能是2的幂次(即:1,2,4,8,16…)
如果Kiki能赢的话,请输出“Kiki”,否则请输出“Cici”。
解题:
巴什博奕,用上述方法计算出各点的 sg值 就可以发现规律辣 ~
#include<cstdio> #include<cstring> #include<algorithm> #include<iostream> #include<string> using namespace std; int main() { int n; while(scanf("%d",&n)!=EOF){ if(n%3 == 0) printf("Cici\n"); else printf("Kiki\n"); } return 0; }
HDU 1848
题意:
一共有3堆石子,数量分别是m, n, p个;两人轮流走;每次可以选择任意一堆石子,然后取走f个;
f只能是菲波那契数列中的元素; 先取完所有石子的人为胜;
解题:
求出所有 sg 值,然后异或一下就好辣~ (> <)
#include<cstdio> #include<cstring> #include<algorithm> #include<iostream> #include<string> using namespace std; int fib[20],sg[1010],vis[1010]; void init() { fib[1] = 1,fib[2] = 2; for(int i=3;i<20;i++){ fib[i] = fib[i-1]+fib[i-2]; if(fib[i] > 1000) break; } } void getsg() { sg[0] = 0; // 此点为先手必败点,sg 值为 0 ; for(int i=1;i<1001;i++){ // n,m ,p 的范围在1000以内 所以只要算 1000个点 的 sg 值 memset(vis,0,sizeof(vis)); // 每次清空 vis 数组 ; for(int j=1;fib[j]<=i;j++){ // 找出后继节点 sg 值并标记 ;g(x) = mex(g(y)); vis[ sg[ i-fib[j] ] ] = 1; } for(int j=0;j<1010;j++){ // 找到在它后继节点里 未出现过的 最小的数 if(vis[j] == 0) { sg[i] = j; break; } } } } int main() { int n,m,p; init(); getsg(); while(scanf("%d%d%d",&n,&m,&p)!=EOF && (n||m||p)){ if( (sg[n] ^ sg[m] ^ sg[p] ) != 0 ) printf("Fibo\n"); else printf("Nacci\n"); } return 0; }
HDU 1536
题意:
首先输入 k ,表示集合大小,,接下来 k 个数,表示可以取的石子的个数 ;
输入 m 表示m次询问
接下来m行 每行输入一个n,表示有 n 堆,接下来 n 个数,表示每堆有ni个石子;
输出每次询问的结果,赢输出 W ,否则 L ;
解题:
求出每一堆的 sg 值,异或一下就好辣;(因为眼瞎数组开的不够大WA好几次= = 百思不得其解,重新看了一下数据范围= =)
值得注意的是,这里不能每次都把所有的 sg 值都求出来,会超时。
类似的,有时候会遇到 只需要某些 sg 值就可以了,而其他的有些 sg 值是用不到的 ,
这时候只要求出单个 sg 值就可以,如果之后还有可能用到,可以用类似 "记忆化搜索"的方法保存下来。
#include<cstdio> #include<cstring> #include<algorithm> #include<iostream> #include<string> using namespace std; const int maxn = 10010; int sg[maxn],k,n,m,s[110],x; int getsg(int x) /// 得到单个 sg 值 { int hash[110] = {0}; for(int i=0;s[i]<=x && i<k;i++){ if(sg[x-s[i]] == -1) sg[x-s[i]] = getsg(x-s[i]); hash[ sg[x-s[i]]] = 1; } for(int i=0;;i++){ if(hash[i] == 0) return i; } } int main() { while(scanf("%d",&k)!=EOF && k){ memset(s,0,sizeof(s)); for(int i=0;i<k;i++) scanf("%d",&s[i]); sort(s,s+k); memset(sg,-1,sizeof(sg)); int fans[110]; scanf("%d",&m); for(int i=0;i<m;i++){ int ans = 0; scanf("%d",&n); for(int j=0;j<n;j++){ scanf("%d",&x); if(sg[x] == -1) sg[x] = getsg(x); // 若是 sg 值未得出就调用函数求出来,已经得出 直接异或 ans ^= sg[x]; } fans[i] = ans; } for(int i=0;i<m;i++) printf((fans[i])?"W":"L"); cout<<endl; } return 0; }