一、心得体会
1.ACM博弈题,不会的时候觉得难于上青天,会的时候觉得没有比博弈更水的题了;
博弈题看到的第一眼觉得是难题,代码敲完顿觉水题。你可能花半个小时去找规律,然后仅花2分钟敲代码。
2.博弈是单人游戏,也可以说是自己跟自己玩,因为“双方都做出最优决策”这一点限制了,最后的结果不取决
于你是谁,不取决于你的智商,只取决于你面对的局面;
3.局面,这是博弈里面最最最重要的东西!!!(所谓SG也是指这一局面的SG),博弈是一种不公平的游戏
因为游戏开始的时候已经结束了,影响你胜负的就是你所面对的局面,因为双方采取最优策略
故而局面必然会以双方当前对自己最优的路径走下去,所以结局已经确定了
4.当你面对一个局面的时候如何做出最优的决策呢?你一定是走到了最后一步才确定了胜负,所以当前的局面
往往需要从最终的局面逆推而来(也就是从一个已知胜负的局面一步步推导其他的局面,有了这样的思想,SG
也就不那么难理解了)
5.关于SG:
入门了博弈的人都知道,博弈里面常常用到一个重要的概念 -- SG。但是SG是什么?你去百度的话会有非常专业的解答,
但是那些所谓的专业绝对让人看的头疼。这里说说我所理解的博弈里面的SG(仅限博弈)
挑程里是这样解释SG值的:
除 任意一步所能转移到的子局面的SG值以外的最小非负整数。
仔细体会一下这句话,你会发现,这里对SG值的定义是递归定义的!
当前局面的SG是什么呢?请先去找当前局面的子局面的SG值。
显然,递归是有一个边界的,SG是一种递归,那么它也是有边界的,
不难发现,它的边界是没有子局面的局面(也就无法再转移的局面)
什么样的局面没有子局面呢,也就是胜负已定的局面。在第4点说到,
当前局面的最优策略是从胜负已定的最终局面逆推来的,这里的SG其实也是
说了这些,那么SG到底是什么呢?
联想当年学习递归的一个例子:
f(n) = 1 , n = 1
f(n-1) +1 ,n > 1
这样一个函数是我们学习递归时的经典例子,你说这里的F到底是什么?其实它不过是一个函数而已。
SG也是一样,它只是一个函数而已,函数这个词翻译成英语再翻译成中文,就成了“功能、作用”
那么SG的作用是什么呢?
举一个最简单的例子:
有一堆石头数量为n,两个人轮流从石堆拿{a1,a2,a3,......,ak}个石头,先取完所有石头者胜。
根据前面说的,首先找胜负已定的局面,当n=0的时候,石头被拿完了,败态
那么sg[0] = 0表示面对0个石头的局面者败,然后根据sg的定义,我们可以求出其他局面的sg值
(为了使每种局面确保有可以转移的子局面,我们假设{a1,a2,a3......,ak}里面一定有1,例如假设没有1的话
,假设为{5,6,7}那么局面4没有可以转移的子局面,这样会出现平局的情况,我们后面再说平局)
这样可以求出所有局面的sg值,然后sg的作用出来了~
我们发现,若sg[x] = 0,那么x是败态,这其中很神奇,鶸也说不清楚,只说一下胜态败态的转移
(其实光理解的话可能还是不知道什么是SG,但是看了后面的题目就能理解了并知道怎么用SG找到游戏的胜态败态了)
6.胜态与败态:
之前说了,博弈里面,游戏开始的时候已经结束了,影响你胜负的就是你所面对的局面。
也就是说,这个局面觉得了你的胜负,我们称能让你走向胜利的局面称为胜态,也是必胜态,专业术语也叫P态(积极的英语单词怎么写?)
称让你走向失败的状态称为败态,也是必败态,专业也叫N态(消极的英语单词鶸也不会拼。。。)
有一个很显然的规律:
只要当前状态可以转移到的状态中有一个是败态,那么当前状态就是胜态。
如果当前状态可以转移到的所有状态都是胜态,那么当前状态就是败态。
这两句话互为逆否命题,一眼就看出是对的就不解释了。
可以胜态败态的角度去理解下SG。
7.Nim游戏:
关于这个Nim游戏,百度的话又是一大堆乱七八糟看不下去的东西,
它的最原始的版本大概是说有N堆石头,{a1,a2,a3......,an}表示每堆的数量,两个人轮流选一个石堆拿若干石头(不能不拿),
如果轮到某个人时所有的石子堆都已经被拿空了,则判负。
这个游戏有个非常完美的结论:
令 s = a1^a2^a3....^an(^符号表示异或运算)
若 s = 0,则此局面为败态,否则为胜态
对于上面的式子,我们不难发现,当你从一个石堆拿走一些石头(即改变一个ai),一定会发生胜态和败态的转变
胜态一定会转移成败态,败态也一定有策略转移成胜态
当这个结论与SG结合,神奇的事发生
我们发现sg异或和为0的状态也是败态,否则胜态。
另外,很多游戏都可以转变为Nim的形式,例如POJ 1704(挑程上有讲解)
8.关于平局:
我们发现,一个必胜态的获得,必然是因为它可以转移到一个败态,那么是不是说相比于平局我们更倾向于败态呢?
如果有更多的败态,理论上可以转移出更多的胜态,但是孩子别太天真了啊~
博弈将“对敌人的仁慈就是对自己的残忍”这句话发挥的淋漓尽致,当你选择败态的时候,对方却不会傻傻按照你的想法给你转移胜态的
该你输的时候你还是得输,所以,在博弈里的决策,一定要是对自己最有利对对手最不利的策略才是最优策略,、
也就是说,如果实在不能赢,你一定宁可平局,也不要选择败态。例如今年HDU 多校题5754 里面马的情况
9.当初关于博弈看了很多但是都只是似懂非懂,只有做多了题才有更多的·体会
二、博弈做题技巧
做了个专题:点击打开博弈专题
题目其实好多都是做过的原题,不过以前都是自己找规律的,这次就是用SG打表找规律,通过这些题目也算是知道怎么使用SG找规律了
其中的题目大多都是打表找规律,不过也有一些有趣的题目
PS:题目选自kuangbin 的博弈分类:点击打开链接(难度的话,后面的题都蛮简单,前面的题稍难)
1.打表找规律题:
W - A multiplication game
输入n,从1开始,每次乘以2~9的数,谁最先达到n谁胜
直接上代码,其中solve()函数是打表的过程,找完规律之后直接解决不需要solve,不过为了记录自己的思路,打表的代码也保留了
#include
#include
#include
#include
#define mem(a,x) memset(a,x,sizeof(a))
using namespace std;
typedef long long ll;
/*
败态:
10 - 18
163 - 324
2917 - 5832
综上:
败态:
(9*18^i,18*18^i]
i从0开始
*/
const int N = 100000;
int sg[N+4];
void solve()
{
sg[1] = 0;
for (int i = 2;i <= N;++i)
{
sets;
for (int j = 2;j <= 9;++j)
{
int to = i/j;
if (i%j)to++;
s.insert(sg[to]);
}
int g = 0;
while (s.count(g)) ++g;
sg[i] = g;
}
for (int i = 2000;i <= 9000;++i)
{
cout<l[i]&&x<=r[i]) return 1;
}
return 0;
}
int main()
{
// solve();
ll n;init();
while (~scanf("%I64d",&n))
{
if (loser(n)) puts("Ollie wins.");
else puts("Stan wins.");
}
return 0;
}
S - A Multiplication Game
S题和W题一样的,不说了
R - 悼念512汶川大地震遇难同胞——选拔志愿者
和S、W的意思也差不多,不过操作从乘法变成了加法,由于数据小,于是也没有找规律,直接打完所有表把规律存在表里就好
#define mem(a,x) memset(a,x,sizeof(a))
#include
#include
#include
#include
#include
#include
#include
#include
#include
O - Calendar Game
同样从终态逆推,不过逆推的过程有点麻烦,导致看起来都像模拟了。。。
#define mem(a,x) memset(a,x,sizeof(a))
#include
#include
#include
#include
#include
#include
#include
#include
#include
V - Digital Deletions
题意是对于一个数字形式的字符串,可以把每一位的数字变小(包括0,不为负),可以删去一个0以及0右边的所有数一起删除,两人轮流操作
谁移除最后一个数胜
同样逆推局面推出胜态败态
逆推的时候操作变成将数字变大,或者在后面补0及其他数字,因为长度不超过6,所以还是很简单的
#include
#include
#include
#include
#include
#include
#include
M - Play a game
大胆猜测,小心求证,自己随便玩几种局面就会发现奇败偶胜(代码略)
int n;
while (cin>>n)
{
if (!n) break;
if (n&1) puts("ailyanlu");
else puts("8600");
}
L - Good Luck in CET-4 Everybody!
依旧简单打表找规律,自己手动找规律也可以,不过为了练习下SG的运用,还是用SG打表(也比手动找规律更快更准)
具体用SG打表找规律的方法代码中见:
#define mem(a,x) memset(a,x,sizeof(a))
#include
#include
#include
#include
#include
#include
#include
#include
#include
K - kiki's game
打表找规律,发现当n和m都是奇数的时候必败,打表代码注释了没删除以供参考
#define mem(a,x) memset(a,x,sizeof(a))
#include
#include
#include
#include
#include
#include
#include
#include
#include
J - 取石子游戏
斐波那契博弈哦,必败态是斐波那契数
#include
#include
using namespace std;
typedef long long ll;
bool check(ll x)
{
ll f1 = 1,f2 = 1;
ll f = 2;
while (f <= x)
{
f = f1 + f2;
if (f == x) return 1;
f1 = f2;
f2 = f;
}
return 0;
}
int main()
{
ll n;
while (cin>>n)
{
if (!n) break;
if (check(n)) puts("Second win");
else puts("First win");
}
return 0;
}
I - 邂逅明下
三个变量,找规律的时候不是那么容易,然后说到博弈还有一个特点就是,大胆猜测~
最后发现1~p必败,p+1~p+q必胜
#define mem(a,x) memset(a,x,sizeof(a))
#include
#include
#include
#include
#include
#include
#include
#include
#include
E - Fliping game
找规律,发现当右下角是1的时候必胜
插一句,这个游戏公平吗?是公平的,因为右下角是1的概率是1/2,而其他的石头怎么样不需要考虑^_^
#define mem(a,x) memset(a,x,sizeof(a))
#include
#include
#include
#include
#include
#include
#include
#include
#include
2.Nim 游戏变形:
U - John
Nim游戏的简单变形,特判全部是1的情况:如果全部是1,奇败偶胜,否则就按Nim游戏的异或和为0的是败态
#define mem(a,x) memset(a,x,sizeof(a))
#include
#include
#include
#include
#include
#include
#include
#include
#include
T - Be the Winner
和上面一题一样的规律,完全不一样的游戏却有完全一样的规律
#define mem(a,x) memset(a,x,sizeof(a))
#include
#include
#include
#include
#include
#include
#include
#include
#include
H - Nim or not Nim?
和今年多校里面的一道博弈题基本一样,规律基本都是一样的,这里是可以把石头分两堆,今年多校的那题( HDU 5795)分三堆一样的原理
直接打表找规律,打表的过程注释以供参考
#define mem(a,x) memset(a,x,sizeof(a))
#include
#include
#include
#include
#include
#include
#include
#include
#include
PS:另附HDU 5795代码对比:(表打出来了规律就很简单了)
#define mem(a,x) memset(a,x,sizeof(a))
#include
#include
#include
#include
#include
#include
#include
#include
#include
F - Daizhenyang's Coin
我以为算是找规律的题,不过找的不是十进制数的规律,而是二进制数的规律,本来博弈就和二进制有着密不可分的关系
所以找规律的时候也要记得考虑一下二进制(这一点不仅是博弈,记得很多其他地方也用到找二进制数的规律)
不过有文章专门讲解了这一类型的游戏的策略:博弈-翻硬币游戏
这里的规律是如果x的二进制里面1个数为奇数,sg[x]就是2x,否则是2x+1
关于unique去重函数:点击打开链接
#define mem(a,x) memset(a,x,sizeof(a))
#include
#include
#include
#include
#include
#include
#include
#include
#include
3.状态转移:
Q - Being a Good Boy in Spring Festival
一般博弈都是问当前的局面是胜态还是败态,这个问如果是胜态,第一步有几种走法
真正理解博弈的会明白,博弈双方对局面做出的转移
当某人面对胜态的时候,他会将胜态转移成败态,
而面对败态的人不管怎么操作,只能将局面由败态转为胜态(不包含平局)
这是因为,如果异或和为0(败态)不管怎么操作都将使异或和变为非0(胜态)
而异或和不为0(胜态),一定有策略将异或和变为0(败态)
所以这题就是找,如果面对的是异或和不为0的胜态,有多少种方案将其变成异或和为0的败态
关于异或,有个很有用的性质:a^a^b = b (即相同的数异或为0),具体操作看代码
#define mem(a,x) memset(a,x,sizeof(a))
#include
#include
#include
#include
#include
#include
#include
#include
#include
P - Public Sale
一样的水题打表,不过问的是第一次的选择有哪些,那么枚举第一次的选择,判断子局面是不是败态即可
(也就是只能将败态留给对手)
#define mem(a,x) memset(a,x,sizeof(a))
#include
#include
#include
#include
#include
#include
#include
#include
#include
4.思维王道
N - Euclid's Game
这题的选择稍多,假设a
这里涉及到一个自由度的概念,有些局面是固定的,比如(4,7),它只能按(4,7)-(4,3)-(1,3)的情况走下去
像这样的局面就是没有自由度,操作者只有唯一的选择
对于形如b-a
对于形如b-a>a的局面,其实这是必胜的局面
(不要问b-a==a的局面,b是a的倍数显然必胜态)
综合上述规律,直接模拟即可(详解参考挑程310面):
#define mem(a,x) memset(a,x,sizeof(a))
#include
#include
#include
#include
#include
#include
#include
#include
#include
G - Game
非常神奇,和二分图也联系起来了,想清楚了就是Nim游戏变形
#define mem(a,x) memset(a,x,sizeof(a))
#include
#include
#include
#include
#include
#include
#include
#include
#include
B - Gems Fight!
局面的描述比较复杂,使用状态压缩博弈,一样的博弈原理,从终态去逆推当前面对的局面
另写了详细题解: HDU 4778 Gems Fight!(博弈+状压)
C - Mine
这个题才真正让人看到SG的作用,前面说当SG和Nim游戏的异或和的结论结合的时候可能并没有什么感觉
这题就很好的应用了这点,整个棋盘的sg就是每个格子的sg的异或和
另写了详细题解: HDU 4678 Mine (博弈SG+自由度原理)