记录一点结论性的东西,推导见百度吧。
首先博弈的前提是双方“绝对理智”。
一般的胜负博弈游戏来说,有以下几点:(注意必胜必败是针对这回合操作的人)
所有终结状态为必败点(比如五子棋a下出5连珠后,轮到b走,而5连珠为终结状态,所以b败,叫做必败点)。
所有1步操作能进入必败点的点为必胜点(比如该a走时a有4连珠,a只要能走出5连珠就进入了(b的)必败点,此时a必胜)。
某点的所有操作都走向必胜点,则该点为必败点。(和上一条对称)
Hdu 1517 上述定理的应用 本质还是搜索(有O(1)的数学方法)
Hdu 1079 日期模拟+博弈搜索 考验代码功底..(也有O(1)的数学方法)
可以发现,博弈的本质还是搜索,由一个局面到下一个局面,形成一颗树(或图),树的叶子节点是最终局面。
也有一些最终结果不是胜负的博弈,以https://nanti.jisuanke.com/t/19975 “Rake It In”为例(icpc现场赛题目,比赛的时候突然领悟..):
给4*4的矩阵,a,b分别进行操作,每人每次可以选择一个2*2的矩阵把他们的和加到最终分数里并逆时针旋转选中的矩阵(2人共用1个分数)。但a,b的目标不同:分别是让最终分数最大(最小)。求k轮操作最终的分数。
从第一步开始想很难,贪心显然是不行的,因为涉及矩阵的旋转,会改变整个矩阵。
但倒着想,在最后1步(最后1步是b走),b一定会选总和最小的矩阵。a走倒数第二步时知道对于每个局势b将怎么走,所以a会走最化大最小矩阵的步。
这是一个递归的过程。绝对理智走出第一步时,就把所有情节遍历了一遍,知道了最终答案。(其实并没博弈...开局就决定了胜负)
所以做法是:先遍历所有状态的分数,再递归求解绝对理智的第一步是怎么走的,就有了最终答案。(卡常数比较厉害,只能dfs,还需要一点树的知识)
或者用ab剪枝优化博弈树..
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
int a[4][4], k, sum;
short num[600005], ans[66666];
void fx(int x, int y)
{
int temp = a[x][y];
a[x][y] = a[x][y + 1], a[x][y + 1] = a[x + 1][y + 1];
a[x + 1][y + 1] = a[x + 1][y], a[x + 1][y] = temp;
}
void cofx(int x, int y)
{
int temp = a[x][y];
a[x][y] = a[x + 1][y], a[x + 1][y] = a[x + 1][y + 1];
a[x + 1][y + 1] = a[x][y + 1], a[x][y + 1] = temp;
}
void dfs(int tim, int x)
{
num[x] = sum;
int i, j;
/*下一步是常数优化,注释这样写更容易懂但会tle..大概效率差了1倍
if (tim == k)
return;
*/
if (tim == k - 1)
{
for (i = 0; i < 3; i++)
for (j = 0; j < 3; j++)
num[x * 9 + i * 3 + j + 1] =
sum + a[i][j] + a[i][j + 1] + a[i + 1][j] + a[i + 1][j + 1];
return;
}
for (i = 0; i < 3; i++)
for (j = 0; j < 3; j++)
{
fx(i, j);
sum += a[i][j] + a[i][j + 1] + a[i + 1][j] + a[i + 1][j + 1];
dfs(tim + 1, x * 9 + i * 3 + j + 1);
cofx(i, j);
sum -= a[i][j] + a[i][j + 1] + a[i + 1][j] + a[i + 1][j + 1];
}
}
int viia(int tim, int x)
{
if (tim == k)
return num[x];
if (ans[x] != -1)
return ans[x];
if ((tim & 1) == 0)
{
int te = 0;
for (int i = 1; i <= 9; i++)
te = max(te, viia(tim + 1, x * 9 + i));
return ans[x] = te;
}
else
{
int te = 999999;
for (int i = 1; i <= 9; i++)
te = min(te, viia(tim + 1, x * 9 + i));
return ans[x] = te;
}
}
int main()
{
int t, i, j;
scanf("%d", &t);
while (t--&&scanf("%d", &k))
{
sum = 0;
k <<= 1;
for (i = 0; i < 4; i++)
for (j = 0; j < 4; j++)
scanf("%d", a[i] + j);
memset(num, 0, sizeof(num));
memset(ans, -1, sizeof(ans));
dfs(0, 0);
printf("%d\n", viia(0, 0));
}
return 0;
}
博弈的搜索求解的本质是树的遍历,但博弈论(sg函数相关)是从数学角度快速求解特定类博弈问题(直接上最终结论,但墙裂推荐认真学一遍推导过程)。
针对这类问题,应用sg函数可以快速解决:
“给定模型给定一个有向无环图和一个起始顶点上的一枚棋子,两名选手交替的将这枚棋子沿有向边进行移动,无法移动者判负。”
比如给定1堆石子, 2人轮流操作,每次操作可以将某堆石子取出b[i]个,2人绝对理智,求最终谁胜。
定义对于自然数集合的mex运算,mex(A)表示A中最小的未出现的自然数。
定义对于节点的sg函数,x的子节点的sg函数构成的集合为A,则sg(x)=mex(A)。(对于叶子结点x,A为空集,mex(x)=0)
则有:对于状态x,先手必败当且仅当sg(x)=0。
这样理论上可以解决所有上述类型问题,可如果有n堆石子(每一步可以在n个图中选一个图操作)当n很大是时候,节点也非常多,算sg函数基于递归,计算量仍然很大。
另一个结论是:n个有向图游戏构成的游戏其sg函数为所有子游戏sg函数的异或。
理论部分就这些,推导有点麻烦(再次墙裂推荐看一遍推导)。
以取石子https://nanti.jisuanke.com/t/25083为例:
3堆石子,每次可以取1,3,7,9个,求最终谁胜。
分解为3个1堆石子的游戏,预处理出x个石子时的sg(x),判断sg(x)^sg(y)^sg(z)是否为0即可。
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
//f[N]:可改变当前状态的方式,N为方式的种类,f[N]要在getSG之前先预处理
//SG[]:0~n的SG函数值
//S[]:为x后继状态的集合
int f[] = { 1,3,7,9 }, SG[1005], S[1005];
void getSG(int n)
{
int i, j;
memset(SG, 0, sizeof(SG));
//因为SG[0]始终等于0,所以i从1开始
for (i = 1; i <= n; i++)
{
//每一次都要将上一状态 的 后继集合 重置
memset(S, 0, sizeof(S));
for (j = 0; f[j] <= i && j <= 3; j++)
S[SG[i - f[j]]] = 1; //将后继状态的SG函数值进行标记
for (j = 0;; j++)
if (!S[j])
{ //查询当前后继状态SG值中最小的非零值
SG[i] = j;
break;
}
}
}
int main()
{
int n, m, k;
getSG(1000);
while (~scanf("%d%d%d", &n, &m, &k))
puts(SG[n] ^ SG[m] ^ SG[k] ? "win" : "lose");
return 0;
}
对于不好打表(不是取石子这种)的模型,可以直接在图上dfs出每个节点的sg函数。
Hdu 1847 sg函数模板题目 可以发现步数是2的幂时sg(x)=0当且仅当x%3=0(所以直接判%3也行)
Hdu 1848 同上
Poj 2234 同上,可以发现对于一堆石子取任意数目时sg(x)=x。
Hdu 2509 注意胜负条件和取石子相反,先取完的输。用必胜点的方法可以发现规律。特判所有堆都是1的情况就好。
Hdu 1907 同上