博弈论与sg函数入门

记录一点结论性的东西,推导见百度吧。

 

首先博弈的前提是双方“绝对理智”。

 

一般的胜负博弈游戏来说,有以下几点:(注意必胜必败是针对这回合操作的人)

所有终结状态为必败点(比如五子棋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 同上

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值