[关闭]
@dxbdly 2021-03-03T14:46:57.000000Z 字数 4463 阅读 261

G2020 19寒假集训Day2

信息学——19寒假集训


今天是G2020寒假集训Day2,主要学习博弈论,并且是组合游戏型

一.组合游戏

组合游戏满足以下特点:

  1. 1.必须是两个人轮流来。
  2. 2.一个局面的下一步能到达的状态必须只跟局面本身有关。
  3. 3.玩家之间没有差别。
  4. 4.可以在有限步内结束。
  5. 5.不能操作的人为败者。

二.必胜点与必败点

解决组合游戏问题时,我们常常将问题抽象成图。

对于一个组合游戏,我们可以将它抽象成一个有向无环图(DAG)。

图上的点对应游戏中的状态,边对应一个状态到另一个状态的合法操作。

而这张图有一个特殊的性质:

  1. 图中的点要么为必胜点(又称P点,即走到这个状态的人必胜)
  2. 要么为必败点(又称N点,即走到这个状态的人必败)

(PS:以下均简写为P点,N点)

P点与N点满足以下性质:

  1. 1所有的终结点都是必败点
  2. 2.从任何P点操作,至少有一种方式进入N
  3. 3.无论如何操作, N点都只能进入P点.

如果我们能将所有必胜点与必败点求出,即可轻易得出答案。

三. 函数

刚才讲到如果我们能确定出必胜点与必败点,即可轻易得出答案。

但有些题模型复杂,操作繁琐,这时便很难直接分析题目得到必胜,必败点。

所以我们必须引进一种非常强大 (恶心) 的东西——函数

先给出两个定义:

  1. Nim和: 所有数的异或和。
  2. mex(minimal excludant)运算:这是一个集合运算,表示最小的不属于当前集合的非负整数。

接下来,我们好好分析以下函数

定义与本质

SG函数的定义:

  1. 对于任意状态 x 定义 SG(x) = mex(S),其中 S x 后继状态的SG函数值的集合。

emmm……是不是感觉有点懵

下面我们来分析一下函数的本质。

刚才讲过了函数是用来求P点,N点的,换句话说,他其实是P点,N点的另一种表示形式

所以说函数会满足P点,N点的所有性质

我们可以注意到关于P点,N点的这两条性质

  1. 1.从任何P点操作,至少有一种方式进入N
  2. 2.无论如何操作, N点都只能进入P点.

再对比一下这句话:

  1. 定义 SG(x) = mex(S),其中 S x 后继状态的SG函数值的集合。

你会发现:

  1. 所谓x 后继状态的SG函数值的集合其实就是节点x所连向的所有出点!
  2. 又由P点,N点性质可得:
  3. SG(x) = mex(S)其实就是从终结点逆推,将所有P点,N点求出的递推式!

时为P点,当时为N点。

求出函数,就能得到原问题的解。

SG函数求法

递推方程:是求函数的通解,在此方程中,最重要的是这个运算如何求解。

由于此递推方程是逆推,我们一般将其转换为顺推求解

即将图反向,且求解顺序颠倒,原来是最终状态--->初始状态,变为初始状态--->最终状态。

那么对于求解每一个时,我们开一个标记数组,标记状态~到状态的转移方式(游戏的操作)。

然后找到第一个没被标记的点即可。
模板代码:

  1. inline void get_SG()
  2. {
  3. // sg[1]必为N点,初值为0,不用考虑
  4. // f[]为标记数组
  5. for (register int i=2;i<=n;++i)
  6. {
  7. memset(f,0,sizeof(f));
  8. for (register int j=1;j<i;++j)
  9. f[j->i的转移操作]=1;
  10. for (register int j=0;;++j)
  11. if (!f[j])
  12. {
  13. sg[i]=j;
  14. break;
  15. }
  16. }
  17. }

SG定理

对于某一些较为复杂,不好确定函数的题目:

  1. 我们可以尝试采用分治思想:
  2. 将游戏分解为多个类似的子游戏
  3. 通过计算子游戏的SG函数,来推出原题的SG函数。

SG定理就是用来解决如何由子游戏的函数推出原题的函数。

定理内容:

  1. 题目所求游戏的SG函数等于各个子游戏SG函数的Nim和(异或和)。

有了这一定理,我们便能将原问题分解为多个子问题,然后分而治之,将原问题简化。

PS:将原问题划分为子问题时,子问题必须要相似且独立

四.例题

P2197 【模板】nim游戏

题面

解法:

一共有n堆石子,不好处理,我们将每一堆石子看作一个子游戏

我们容易得到子游戏的函数:,由此可推出全局函数。

AC代码:

  1. //The code is from dxbdly
  2. #include<bits/stdc++.h>
  3. using namespace std;
  4. inline int read()
  5. {
  6. int x=0;
  7. char c=getchar();
  8. bool f=0;
  9. while (!isdigit(c))
  10. f|=(c=='-'),c=getchar();
  11. while (isdigit(c))
  12. x=(x<<3)+(x<<1)+(c^48),c=getchar();
  13. return f?-x:x;
  14. }
  15. int t,n,ans;
  16. int main(){
  17. t=read();
  18. while (t--)
  19. {
  20. ans=0,n=read();
  21. for (register int i=1;i<=n;++i)
  22. {
  23. int x=read();
  24. ans^=x;
  25. }
  26. ans?printf("Yes\n"):printf("No\n");
  27. }
  28. return 0;
  29. }

P3185 [HNOI2007]分裂游戏

题面

此题显然需要划分子游戏,利用函数处理。

我们从最终状态出发,发现最终状态一定是所有糖果都在最后一个花瓶内

接下来划分子游戏

在划分子游戏时存在一种常见的错误划分方法

  1. 以每个瓶子为一个子游戏。
  2. 错误在哪呢?
  3. 我们会发现,此题的转移方式是在三个瓶子中进行的
  4. 也就是说,当前这个子游戏会受到其他子游戏的影响!
  5. 所以说此划分方法不满足子问题的独立性!!!

正确的划分方法为:每个糖果为一个子游戏。

则题目转化为:

  1. 将这个糖果从第i个位置移动到第n个位置,其实也就是一堆个数为ni的石子等你来取。

则可以使用函数求解。

此题还要求第一步如何走,利用异或的逆运算可知:

  1. 设:全局SG值为ans
  2. 则若三个瓶子i,j,k为第一步
  3. 则满足
  4. ans^sg(n-i+1)^sg(n-j+1)^sg(n-k+1)=0

AC代码:

  1. //The code is from dxbdly
  2. #include<bits/stdc++.h>
  3. using namespace std;
  4. inline int read()
  5. {
  6. int x=0;
  7. char c=getchar();
  8. bool f=0;
  9. while (!isdigit(c))
  10. f|=(c=='-'),c=getchar();
  11. while (isdigit(c))
  12. x=(x<<3)+(x<<1)+(c^48),c=getchar();
  13. return f?-x:x;
  14. }
  15. int t,n;
  16. int a[25],sg[25];
  17. int ans,tot;
  18. bool f[10005];
  19. inline void get_SG()
  20. {
  21. for (register int i=2;i<=21;++i)
  22. {
  23. memset(f,0,sizeof(f));
  24. for (register int j=1;j<i;++j)
  25. for (register int k=j;k<i;++k)
  26. f[sg[j]^sg[k]]=1;
  27. for (register int j=0;;++j)
  28. if (!f[j])
  29. {
  30. sg[i]=j;
  31. break;
  32. }
  33. }
  34. }
  35. int main(){
  36. t=read();
  37. get_SG();
  38. while (t--)
  39. {
  40. n=read();
  41. ans=tot=0;
  42. for (register int i=1;i<=n;++i)
  43. {
  44. a[i]=read();
  45. if (a[i]&1)
  46. ans^=sg[n-i+1];
  47. }
  48. for (register int i=1;i<=n;++i)
  49. for (register int j=i+1;j<=n;++j)
  50. for (register int k=j;k<=n;++k)
  51. if (!(ans^sg[n-i+1]^sg[n-j+1]^sg[n-k+1]))
  52. {
  53. tot++;
  54. if (tot==1)
  55. printf("%d %d %d\n",i-1,j-1,k-1);
  56. }
  57. !tot?printf("-1 -1 -1\n0\n"):printf("%d\n",tot);
  58. }
  59. return 0;
  60. }

P3480 [POI2009]KAM-Pebbles

题面

此题需要先对题目本身进行分析,将原问题转化,并得出一些性质

再根据性质划分子游戏,使用函数解决

题目有如下限制条件。

  1. 每堆石子个数都不少于前一堆的石子个数

可以将其转化为:

  1. 任意一堆石子个数-前一堆石子个数>=0

题目的操作方式为:

  1. 两人轮流操作每次操作可以从一堆石子中移走任意多石子。

思考拿走石子之后的变化会发现:

  1. 拿走x个石子后,当前堆与后一堆石子之差增加x

所以可将操作对象从每一堆石子变为两堆石子的差

即限制与操作可转化为:

  1. 限制:n个差(包含10)均>=0
  2. 操作:将第i个差取出x个,放到第i+1个差中

分析此题性质:

  1. 可以发现必胜策略只跟奇数堆有关,与偶数堆无关(可手推)

则限制与操作进一步转化为:

  1. 限制:所有差均>=0
  2. 操作:将第i个差取出x个石子

所以我们会发现,现在就是我们最开始分析的Nim游戏!!!

所以可以直接用函数解决!

通过对题目的不断转换,以及对性质的挖掘,我们成功将此题变成了我们最熟悉的Nim游戏。

而此题的模型就是Nim游戏的变形:阶梯Nim游戏

更多的Nim游戏变形推荐洛谷日报

AC代码:

  1. //The code is from dxbdly
  2. #include<bits/stdc++.h>
  3. using namespace std;
  4. inline int read()
  5. {
  6. int x=0;
  7. char c=getchar();
  8. bool f=0;
  9. while (!isdigit(c))
  10. f|=(c=='-'),c=getchar();
  11. while (isdigit(c))
  12. x=(x<<3)+(x<<1)+(c^48),c=getchar();
  13. return f?-x:x;
  14. }
  15. int t;
  16. int a[1005],ans,cha[1005];
  17. int main(){
  18. t=read();
  19. while (t--)
  20. {
  21. ans=0;
  22. int n=read();
  23. for (register int i=1;i<=n;++i)
  24. a[i]=read();
  25. for (register int i=1;i<=n;++i)
  26. cha[i]=a[i]-a[i-1];
  27. for (register int i=n;i>=1;i-=2)
  28. ans^=cha[i];
  29. ans?printf("TAK\n"):printf("NIE\n");
  30. }
  31. return 0;
  32. }

五.总结

要想独立的解决函数的博弈论题,需要时刻记住以下几个关键:

  1. 1.SG函数的本质(PN点表达方式)
  2. 2.对于原题目不断进行转化以及推导性质
  3. 3.将原问题正确的划分为子问题(相似性,独立性)
  4. 4.对于所有子问题使用SG函数
  5. 5.将所有SG函数合并,得到原问题的解,并根据题目特殊要求特殊处理。
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注