博弈论学习笔记
蒟蒻最近学习了一下博弈论,想着就发了一篇学习笔记。
什么是博弈论
博弈论,又称为对策论(Game Theory)、赛局理论等,既是现代数学的一个新分支,也是运筹学的一个重要学科。
(百度百科)
当然,以上说的十分正确,但在OI里没什么用。
简单来说,就是两个人玩一个小游戏的过程。
此类问题经常让我们求必胜决策或者必败决策。
几个性质
1所有的终止位置都是必败点P 我们认为这个是公理,即所有推导都在这个性质成立的基础上进行。
2 从任何一个必胜点N 操作,至少有一种方法可以达到一个
必败点P。
3 从一个必败点P 出发,只能够到达必胜点N。
举几个栗子
1.洛谷唯一一道入门博弈论
通过这一道题,大家初步了解一下什么是博弈论,这里就不讲了
#include<bits/stdc++.h>
using namespace std;
int t , n;
int read()
{
long long X = 0 , w = 1;
char c = getchar();
while (c < '0' || c > '9')
{
if (c == '-')
w = -1;
c = getchar();
}
while (c >= '0' && c <= '9')
X = X * 10 + c - '0' , c = getchar();
return X * w;
}
int main()
{
t = read();
while(t--)
{
n = read();
if(n % 2 == 0)
cout << "pb wins" << endl;
else
cout << "zs wins" << endl;
}
return 0;
}
2. 巴什博奕Bash Game(你可以认为取石子游戏)
有一堆石子,总个数是n,两名玩家轮流在石子堆中
拿石子,每次至少取1 个,至多取m 个。取走最后一个石子的
玩家为胜者。判定先手和后手谁胜
博弈解题思路:
假设(m + 1)|n,那么假设先手拿走了x 个,那么后手必定可以拿走(m + 1) − x 个,这样子无论怎么拿,剩下的石头个数都将是m + 1 的倍数。
那么最后一次取的时候石头个数必定还剩下m + 1 个,无论先手拿多少个,都会剩下石头,此时后手必定可以将剩下的所有石头取光从而获胜。
否则的话,先手可以取走模m + 1 余数个数个石头,此时模型转换为了先手面对(m + 1)|n 个石头的情况,也就是后手必败,即先手必胜。(参考奆佬yyb的课件)
答案:
如果\((m + 1)|n\)则先手必败,否则先手必胜。
是不是感觉有一点复杂。
那么,就可以引出我们的第二个知识:
3. SG函数
基本定义
先讲一下SG函数中十分重要的mex函数
mex(a)表示最小的不属于这个集合的非负整数
例如:\(\operatorname{mex}({1,2})=0,\operatorname{mex}({0,1,2})=3,\operatorname{mex}({4})=0\)
而SG函数的定义是
\(\operatorname{sg}(x)=\operatorname{mex}(\operatorname{sg}(y))\) 其中,y是x的后继
也就是说,一个点的SG函数为在它所有后继中未出现的最小的值。
基本性质
-
对于已经没有出边的一个点(即出度为0),它的SG函数值为0.
-
对于一个有后继的点,若它的后继的SG值都不为0,则当前点的SG值为0。
-
对于一个有后继的点,若它的SG值不为0,则当前点的后继的SG值必有一个为0。
有没有发现,这个性质与我们最开始讲的性质很像。
是的,顶点x所代表局面是必败局面当且仅当\(\operatorname{sg}(x)=0\)
4.再探巴什博奕
由题我们可以知道,当场面上石子数量为0时,先手必败。
不妨设当前\(m=5\)
即,\(\operatorname{sg}(0)=0\)
那么 \(\operatorname{sg}(1) = \operatorname{mex}(\operatorname{sg}(0))=1\)
方便理解,我们多举几个例子
\(\operatorname{sg}(2) = \operatorname{mex}(\operatorname{sg}(0) , \operatorname{sg}(1))=2\)
\(\operatorname{sg}(3) = \operatorname{mex}(\operatorname{sg}(0) , \operatorname{sg}(1) , \operatorname{sg}(0))=3\)
\(\operatorname{sg}(4) = \operatorname{mex}(\operatorname{sg}(0) , \operatorname{sg}(1) , \operatorname{sg}(2) , \operatorname{sg}(3))=4\)
\(\operatorname{sg}(5) = \operatorname{mex}(\operatorname{sg}(0) , \operatorname{sg}(1) , \operatorname{sg}(2) , \operatorname{sg}(3) , \operatorname{sg}(4))=5\)
\(\operatorname{sg}(6) = \operatorname{mex}(\operatorname{sg}(1) , \operatorname{sg}(2) , \operatorname{sg}(3) , \operatorname{sg}(4) , \operatorname{sg}(5))=0\)
发现规律了吗?
同样,我们可以发现答案:
如果\((m + 1)|n\)则先手必败,否则先手必胜。
因此,用SG函数打表找规律也是不错的解法
5. 关于打表题
这里给一道SG函数打表找规律的题
各位可以自己去尝试一下,多用一用SG函数
这里就直接给AC代码
#include<bits/stdc++.h>
using namespace std;
int t , n;
int read()
{
long long X = 0 , w = 1;
char c = getchar();
while (c < '0' || c > '9')
{
if (c == '-')
w = -1;
c = getchar();
}
while (c >= '0' && c <= '9')
X = X * 10 + c - '0' , c = getchar();
return X * w;
}
int main()
{
t = read();
while(t--)
{
n = read();
puts(n % 4?"October wins!" : "Roy wins!");
}
return 0;
}
6.尼姆博弈Nim Game(这里只讲带SG函数的做法)
大致题意:
地上有 n 堆石子,每人每次可从任意一堆石子里取出任意多枚石子扔掉,可以取完,不能不取。每次只能从一堆里取。最后没石子可取的人就输了。询问是否存在先手必胜的策略
分析:
我们可以把这道题分开来看,
因为一个显而易见的道理:若有奇数个必胜的局面,那么此盘游戏就是必胜。
所以我们最后仅要把每堆石子的SG值的异或和。
考虑分析每堆石子的SG值,
到了这一步就与我们上面讲的那道题很像,道理就不过多赘述了
直接上代码
#include<bits/stdc++.h>
using namespace std;
int vis[100010] , pw[100010] , sg[100010];
int read()
{
long long X = 0 , w = 1;
char c = getchar();
while (c < '0' || c > '9')
{
if (c == '-')
w = -1;
c = getchar();
}
while (c >= '0' && c <= '9')
X = X * 10 + c - '0' , c = getchar();
return X * w;
}
void getsg()
{
for(int i = 1;i <= 10000;++i)
{
for(int j = 0;pw[j] <= i;++j) vis[sg[i - pw[j]]] = true; //标记出后继
for(int j = 0;;++j) if(!vis[j]){sg[i] = j;break;} //求出SG值
}
}
int main()
{
for(int i = 1;i <= 10001;i++) pw[i] = i; //获得步数
getsg(); //求SG值
int t = read();
while(t--)
{
int n = read();
int ans = 0;
for(int i = 1;i <= n;i++)
{
int a = read();
ans ^= sg[a]; //异或得答案
}
if(ans)
cout << "Yes" << endl;
else
cout << "No" << endl;
}
return 0;
}
这里各位可以感性理解一下。
另外,还有大佬告诉我无需SG函数做法,这里也贴上来
#include<bits/stdc++.h>
using namespace std;
int read()
{
long long X = 0 , w = 1;
char c = getchar();
while (c < '0' || c > '9')
{
if (c == '-')
w = -1;
c = getchar();
}
while (c >= '0' && c <= '9')
X = X * 10 + c - '0' , c = getchar();
return X * w;
}
int main()
{
int t = read();
while(t--)
{
int n = read();
int ans = 0;
for(int i = 1;i <= n;i++)
{
int a = read();
ans ^= a;
}
if(ans)
cout << "Yes" << endl;
else
cout << "No" << endl;
}
return 0;
}
7.小约翰的游戏(反nim游戏)
题意各位随便看看
这里参考了题解区第一位大佬的题解(手动点赞)
-
:只有一堆石子,且石子数量为一,那么后手胜利
-
:每一堆都是1,那么只需要判断奇偶性,奇数则先手败,偶数则后手败
-
:只有一堆不是1,其余堆都是1,那么可以根据就行,先手可以选择是拿完或是那得只剩一个
-
:一般情况,思考怎么转化成Case1-3
这里直接打上无需SG函数的做法(带了会玄学RE)
代码附上
#include<bits/stdc++.h>
using namespace std;
int read()
{
long long X = 0 , w = 1;
char c = getchar();
while (c < '0' || c > '9')
{
if (c == '-')
w = -1;
c = getchar();
}
while (c >= '0' && c <= '9')
X = X * 10 + c - '0' , c = getchar();
return X * w;
}
int main()
{
int t = read();
while(t--)
{
int n = read();
int ans = 0 , flag = 1;
for(int i = 1;i <= n;i++)
{
int a = read();
if(a != 1) flag = 0;
ans ^= a;
}
if(flag)
{
if(n & 1)
cout << "Brother" << endl;
else
cout << "John" << endl;
}
else
{
if(ans)
cout << "John" << endl;
else
cout << "Brother" << endl;
}
}
return 0;
}
8.威佐夫博弈
大致题意:
有两堆石子,数量任意,可以不同。每次有两种不同的取法,一是可以在任意的一堆中取走任意多的石子;二是可以在两堆中同时取走相同数量的石子。最后把石子全部取完者为胜者,询问是否先手必胜。
至于思路的话,个人感觉十分奇妙(像我绝对想不到),可以参考题解区第一位大佬的题解,这里就不赘述了
代码还是要给的
#include<bits/stdc++.h>
using namespace std;
const double l = (sqrt(5.0) + 1.0) / 2.0;
int n, m;
int read()
{
long long X = 0 , w = 1;
char c = getchar();
while (c < '0' || c > '9')
{
if (c == '-')
w = -1;
c = getchar();
}
while (c >= '0' && c <= '9')
X = X * 10 + c - '0' , c = getchar();
return X * w;
}
int main()
{
n = read() , m = read();
if(n < m) swap(n, m);
int ans = int(l * (double)(n - m));
if(m == ans)
cout << 0 << endl;
else
cout << 1 << endl;
}
9.P3185 [HNOI2007]分裂游戏(Multi-SG)
蒟蒻决定不水字数了,好好讲几道题。
来看这道题,蒟蒻想了一个多小时,最后还去请同机房的大佬解答才做了出来。
这里就不讲题意了,各位可以自己去看看。
思路
与前几题相同,我们先来思考SG函数。
对于这道题的一个难点来说,
我们需要感性理解一下,以一粒豆子作为子游戏。
也就相当于一个取石子游戏的过程。
这里,蒟蒻也不是特别的理解,写的不好,勿喷
如果理解了这一点以后
不难看出,当前i节点的后继,应该是j , k
那么,一个板子就诞生了
void getsg()
{
for(int i = n - 1;i;i--)
{
memset(vis , 0 , sizeof(vis)); //初始化
for(int j = i + 1;j <= n;j++)
{
for(int k = j;k <= n;k++)
{
vis[sg[j] ^ sg[k]] = 1; //确定后继
}
}
for(int j = 0;;j++)
if(vis[j] == 0) //mex操作
{
sg[i] = j;
break;
}
}
}
当然,肯定有人要问了,为什么后继是 xor 操作呢
这里蒟蒻也不是很清楚,是在看题解的时候,每篇都这么写,若是有神犇知道,请私信发给蒟蒻。
接着,SG函数都出来了,主函数就十分流畅了
注意到,对于每个瓶子里的巧克力豆,是可以在模2的意义下去考虑的,因为后手可以模仿先手的操作,所以就将巧克力豆个数转化为了0或1。(参考大佬题解)
对于输出字典序最小的一组解,因为
\(2≤n≤21\)所以直接\(O(n^3)\)遍历即可
AC代码:
#include<bits/stdc++.h>
using namespace std;
int t , n , ans , cnt , flag , vis[100] , sg[100] , a[100];
int read()
{
long long X = 0 , w = 1;
char c = getchar();
while (c < '0' || c > '9')
{
if (c == '-')
w = -1;
c = getchar();
}
while (c >= '0' && c <= '9')
X = X * 10 + c - '0' , c = getchar();
return X * w;
}
void getsg()
{
for(int i = n - 1;i;i--)
{
memset(vis , 0 , sizeof(vis));
for(int j = i + 1;j <= n;j++)
{
for(int k = j;k <= n;k++)
{
vis[sg[j] ^ sg[k]] = 1;
}
}
for(int j = 0;;j++)
if(vis[j] == 0)
{
sg[i] = j;
break;
}
}
}
int main()
{
t = read();
while(t--)
{
n = read() , sg[n] = 0 , ans = 0 , cnt = 0 , flag = 0;
for(int i= 1;i <= n;i++) a[i] = read();
getsg();
for(int i = 1;i <= n;i++)
if(a[i] % 2) ans ^= sg[i];
for(int i = 1;i < n;i++)
{
if(!a[i]) continue;
for(int j = i + 1;j <= n;j++)
{
for(int k = j;k <= n;k++)
{
if((ans ^ sg[i] ^ sg[j] ^ sg[k]) == 0)
{
cnt++;
if(!flag)
{
flag = 1;
cout << i - 1 << " " << j - 1 << " " << k - 1 << endl;
}
}
}
}
}
if(!flag) cout << "-1 -1 -1" << endl;
cout << cnt << endl;
}
return 0;
}
10.P3235 [HNOI2014]江南乐(Multi-SG)
这是今天的最后一道题目,已经是蒟蒻的极限了,讲得可能不好,勿喷
可以看见这道题,题解区大多都是记忆化搜索的SG,既然我们讲了这么久的预处理,我们就继续这么做。
首先,看见这道题,就有了一个朴实的做法
一个\(O(n^2)\)的直接处理即可,
很遗憾,你将会拿到爆TLE的成绩
再看看数据范围 \(每堆石子数量<100000\)
wow!好开心啊
正经思路
分析题意,我们可以发现分成的 mm 堆的石子数最多有两种取值,分别是: \(i/m\) , \((i/m)+1\)。这两种取值的个数分别是: \(i\%m\) , \(m-i\%m\)。
这里应该可以理解
又因为SG函数维护的是异或和
所以只有奇数的个数,才能有贡献
分析一下奇偶性
对于分析奇偶性,各位可以看看题解区第一位大佬的题解,个人认为十分详细,我因为不会就不赘述了
有始有终,上代码
#include<bits/stdc++.h>
using namespace std;
int t , f , n , sg[100010] , vis[100010];
int read()
{
long long X = 0 , w = 1;
char c = getchar();
while (c < '0' || c > '9')
{
if (c == '-')
w = -1;
c = getchar();
}
while (c >= '0' && c <= '9')
X = X * 10 + c - '0' , c = getchar();
return X * w;
}
void getsg()
{
for(int i = f;i <= 100000;i++)
{
int a;
for(int j = 2;j <= i;j = a + 1)
{
int b = i / j , tot = 0, cnt = 0 , flag;
a = i / (i / j);
if(j == a) flag = 1;
else flag = 2;
while(tot < flag)
{
tot++;
cnt = 0;
if((i % j) & 1)
cnt ^= sg[b + 1];
if((j - i % j) & 1)
cnt ^= sg[b];
j++;
vis[cnt] = i;
}
}
for(int j = 0; ; j++)
if(vis[j] != i)
{
sg[i] = j;
break;
}
}
}
int main()
{
t = read() , f = read();
getsg();
while(t--)
{
n = read();
int ans = 0;
for(int i = 1;i <= n;i++)
{
int a = read();
ans ^= sg[a];
}
if(ans == 0) cout << 0 << " ";
else cout << 1 << " ";
}
return 0;
}