博弈论与SG函数

  • 基础博弈
      • BZOJ2463: [中山市选2009]谁能赢呢?
    • 环取点
      • POJ2484A Funny Game
  • nim博弈——SG函数
      • BZOJ1299
      • POJ2975
    • 有向图
      • POJ2425
    • SG函数典型应用
      • POJ2960
    • 必胜状态向必败状态的转移,方法数,以及操作
      • BZOJ1874
    • 阶梯博弈
      • BZOJ1115
      • POJ1704
      • HDU4315
    • Multi——SG
      • HDU3032
    • SG函数打表
      • POJ2311
      • POJ3537

基础博弈

BZOJ2463: [中山市选2009]谁能赢呢?

小明和小红经常玩一个博弈游戏。给定一个n×n的棋盘,一个石头被放在棋盘的左上角。他们轮流移动石头。每一回合,选手只能把石头向上,下,左,右四个方向移动一格,并且要求移动到的格子之前不能被访问过。谁不能移动石头了就算输。假如小明先移动石头,而且两个选手都以最优策略走步,问最后谁能赢?

[分析]

首先对于n是偶数,一定能被1*2的骨牌覆盖!所以从起点开始,先手走是骨牌的另一端,后手只能走新的骨牌,因此无论何时,先手总是可以走。因此先手必胜。

如果n是奇数,那么去掉一格后一定能被1*2的骨牌覆盖,这样先手只能走到新的骨牌,因此先手必败。

环取点

POJ2484A Funny Game

[描述]

将n枚硬币排成环状,每次可以移除一个或两个相邻的硬币,无法操作者输。

[分析]

如果n<=2,先手必胜;不然先手必败,因为无论,先手怎样操作后手都可以将剩下的硬币分成数目相等且不相邻的两部分,这样后手必然取走最后一个。

nim博弈——SG函数

BZOJ1299

[描述]

TBL和X用巧克力棒玩游戏。每次一人可以从盒子里取出若干条巧克力棒,或是将一根取出的巧克力棒吃掉正整数长度。TBL先手两人轮流,无法操作的人输。 他们以最佳策略一共进行了10轮(每次一盒)。你能预测胜负吗?

[分析]

这里说一下,取出的巧克力棒大家一起吃。

这样就可以转化为石子游戏了,

两个操作:(1).拿掉某堆中的任意数量石子,(2).添加一堆或多堆石子

先手必胜策略:令对手面临的SG值为0,对方先手必败即可。

首先拿出SG值异或和为0的巧克力棒,对手面临必败局面,且对手不能通过添加巧克力棒,是其SG值为0,也就是先手取出最大长度且异或和为0的序列即可。无论对手怎么做,只需维护使其面临SG值为0即可。

#include
#include
using namespace std;
int arr[20];
int n;
int solve() {   //判断是否可以找到异或和为0的子序列(不必连续)
    for(int mask = 1; mask < (1 << n); mask++) {
        int a = 0;
        for(int i = 0; i < n; i++) {
            if((1<if(a == 0)  return 1;
            }
        }
    }
    return 0;
}
int main() {
    for(int i = 0; i < 10; i++) {
        scanf("%d", &n);
        for(int i = 0; i < n; i++)
            scanf("%d", &arr[i]);
        if(solve()) cout << "NO" << endl;
        else cout << "YES" << endl;
    }
    return 0;
}

POJ2975

[题意]

给定一种Nim状态(相当于含N堆石头),求能有几种方法能通过调整某一堆石头的状态(只准取出),使新的Nim状态为必败态。(或者说求出所给的Nim游戏状态有多少种方法能够赢)

[分析]

在证明SG定理时,我们证明由必胜态一定存在某种操作,转移到必败态。

M=SG(x1)SG(x2)SG(x3)... M = S G ( x 1 ) ⊕ S G ( x 2 ) ⊕ S G ( x 3 ) ⊕ . . . , 必胜态 M0 M ≠ 0 , 且设其最高位为 1 ‘ 1 ′ 的位为第 k k 位,因此必然存在 xi x i 使得其 SG S G 值第 k k 位为 1 ‘ 1 ′ ,令 MSG(xi)<SG(xi) M ⊕ S G ( x i ) < S G ( x i ) . 可以通过操作使其变为 xi x i ′ SG(xi)=MSG(xi) S G ( x i ′ ) = M ⊕ S G ( x i )

找到满足 MSG(xi)<SG(xi) M ⊕ S G ( x i ) < S G ( x i ) xi x i 即可

但需要注意的是,并不是所有的nim游戏都是找到 MSG(xi)<SG(xi) M ⊕ S G ( x i ) < S G ( x i ) xi x i ;详情见后面的BZOJ1874.

只是本题,是可以任取的nim游戏,由平凡的SG函数性质知 SG(x)=x S G ( x ) = x , 不存在 y<xSG(y)=MSG(x)>SG(x) y < x 且 S G ( y ) = M ⊕ S G ( x ) > S G ( x )

#include
#include
using namespace std;
int x[1005];
int main()
{
    int n;
    while(scanf("%d", &n) && n) {
        int res = 0;
        for (int i = 0; i < n; ++i) {
            scanf("%d", &x[i]);
            res ^= x[i];
        }
        int ans = 0;
        for(int i = 0; i < n; i++)
            if((res ^ x[i]) < x[i]) ans++;
        cout << ans << endl;
    }
    return 0;
}

有向图

POJ2425

[描述]

由m个棋子在一个有向无环图上,每次双方只能移动一枚棋子,无法在移动者输。

[分析]

典型的组合nim博弈,将有向图的一个子图抽象为石子堆,每次对一堆石子操作,可以将该石子减少任意数量(沿着有向边移动)。

重点是SG函数~,将有向无环图看作有根树,深搜找到孩子节点的SG值作为,根节点的后继状态即可。叶子节点的SG值必为0.

#include
#include
#include
#include
using namespace std;
vector<int>edges[1005];
int sg[1005];
int getsg(int x)
{
    if(sg[x]!=-1) return sg[x];
    int v[1005]={0};
    for(int i=0;iint a=getsg(edges[x][i]);               //标记后继出现的状态
        v[a]=1;                                 
    }
    int a=0;
    while(v[a]!=0) a++;                         //实现mex函数
    return sg[x]=a;
}
int main(int argc, char const *argv[])
{
    int n;int m;
    while(scanf("%d",&n)==1)
    {
        memset(sg,-1,sizeof(int)*n);
        for(int i=0;ifor(int i=0;iint a,b;
            scanf("%d",&a);
            if(a==0) sg[i]=0;
            while(a--) {scanf("%d",&b);edges[i].push_back(b);}
        }

        for(int i=0;iwhile(scanf("%d",&m)==1&&m)
        {
            int a;int ans=0;
            for(int i=1;i<=m;i++)
            {
                scanf("%d",&a);
                ans=ans^sg[a];  
            }
            if(ans==0) printf("LOSE\n");
            else printf("WIN\n");
        }

    }
    return 0;
}

SG函数典型应用

POJ2960

[描述]

n堆石子,给定一个整数集合S,每次只能从一堆石子中取出s个石子,且 sS s ∈ S .

[分析]

后继状态有所限制,灵活写出SG函数即可。

#include
#include
#include
using namespace std;
const int maxn = 10005;
int f[105], sg[maxn];
bool    vis[maxn]; //f是可改变的数目,vis标记后继出现的状态
int arr[500][105];
int m;              //m是可改变的数的总数
void getsg(int n)  //sg(x)=mex{sg(y)|y是x的后继}
{
    //求1~n的sg值
    memset(sg, 0, sizeof(sg));
    for(int i = 1; i <= n; i++) {
        memset(vis, 0, sizeof(bool)*n);
        for(int j = 0;  j < m; j++)
            if(f[j] <= i)
                vis[sg[i - f[j]]] = 1;
        for(int j = 0;; j++) {
            if(!vis[j]) {
                sg[i] = j;
                break;
            }
        }
    }
}
int main(int argc, char const *argv[])
{
    //int n;
    while(scanf("%d", &m) == 1 && m) {
        for(int i = 0; i < m; i++) scanf("%d", &f[i]);
        int n;  scanf("%d", &n);
        int maxx = -1;
        //本题可以离线处理
        for(int t = 1; t <= n; t++) {
            int c;
            scanf("%d",&c);
            arr[t][0]=c;
            for (int i = 1; i <= c; ++i) {
                scanf("%d", & arr[t][i]);
                maxx = max(maxx, arr[t][i]);
            }
        }
        getsg(maxx);
        for(int t = 1; t <= n; t++) {
            int ans = 0;
            for(int i = 1; i <= arr[t][0]; i++)
                ans ^= sg[arr[t][i]];
            if(ans == 0)cout << "L";
            else cout << "W";

        }
        cout << endl;
    }
    return 0;
}

必胜状态向必败状态的转移,方法数,以及操作

BZOJ1874

[描述]

小H和小Z正在玩一个取石子游戏。 取石子游戏的规则是这样的,每个人每次可以从一堆石子中取出若干个石子,每次取石子的个数有限制,谁不能取石子时就会输掉游戏。 小H先进行操作,他想问你他是否有必胜策略,如果有,第一步如何取石子。

若结果为“YES”,则第二行包含两个数,第一个数表示从哪堆石子取,第二个数表示取多少个石子,若有多种答案,取第一个数最小的答案,若仍有多种答案,取第二个数最小的答案

[分析]

取石子的个数有所限制,套用SG函数的模板求出每个石子堆的SG值,再求异或和即可,若为0则为必败,否则必胜。

注意,如果必胜需要输出字典序最小的答案。一开始我只考虑了 SG(xi)A<SG(xi) S G ( x i ) ⊕ A < S G ( x i ) 的石子堆。但事实上这是有可能,但不是必须满足的,【结论】我们只须满足 xi+f[j]=xi : x i ′ + f [ j ] = x i ,且 SG(xi)A=SG(xi) S G ( x i ′ ) ⊕ A = S G ( x i ) 就能保证 SG(xi)A=SG(xi)othersA=0 S G ( x i ′ ) ⊕ A = S G ( x i ) ⊕ o t h e r s ⊕ A = 0 ,使得后手必败。

由于这里所取石子数有限制,则 SG(xi) S G ( x i ′ ) 不一定小于 SG(xi) S G ( x i )

例如 每次只能去2个或1石子,初始有3个石子,则

SG(3)=mex{SG(1)SG(2)},SG(2)=mex{SG(0),(1)},SG(1)=mex{SG(0)},SG(0)=0SG(1)=1,SG(2)=2,SG(3)=0 S G ( 3 ) = m e x { S G ( 1 ) , S G ( 2 ) } , S G ( 2 ) = m e x { S G ( 0 ) , ( 1 ) } , S G ( 1 ) = m e x { S G ( 0 ) } , S G ( 0 ) = 0 即 S G ( 1 ) = 1 , S G ( 2 ) = 2 , S G ( 3 ) = 0

#include
#include
#include
using namespace std;
const int maxn = 1005;
int f[maxn], v[maxn], sg[maxn];
int arr[maxn];
int m;
void getsg(int N) {
    memset(sg, 0, sizeof(sg));
    for(int i = 1; i <= N; i++) {
        memset(v, 0, sizeof(v));
        for(int j = 0; j < m; j++)
            if(f[j] <= i)
                v[sg[i - f[j]]] = 1;

        for (int j = 0;; ++j) {
            if(!v[j]) {
                sg[i] = j;
                break;
            }
        }
    }
}
int main(int argc, char const *argv[]) {
    int n;
    scanf("%d", &n);
    for(int i = 0; i < n; i++) scanf("%d", &arr[i]);
    scanf("%d", &m); for(int i = 0; i < m; i++) scanf("%d", &f[i]);
    getsg(1000);

    int ans = 0;
    for(int i = 0; i < n; i++) ans ^= sg[arr[i]];
    if(ans == 0) printf("NO\n");
    else printf("YES\n");
    //找到可行操作
    if(ans != 0) {  for(int i = 0; i < n; i++) {
                for(int j = 0; j < m; j++) {
                    if(sg[arr[i] - f[j]] == (ans ^ sg[arr[i]])) {
                        cout << i + 1 << ' ' << f[j] << endl;
                        return 0;
                    }
                }
            }
    }

}

阶梯博弈

详解见 【理论篇】

BZOJ1115

神题,没做过人根本想不到

[描述]

有N堆石子,除了第一堆外,每堆石子个数都不少于前一堆的石子个数。两人轮流操作每次操作可以从一堆石子中移走任意多石子,但是要保证操作后仍然满足初始时的条件谁没有石子可移时输掉游戏。问先手是否必胜。

[思路]

每次操作都会增加当前堆与右堆的差值,减小与左堆的差值。自左向右的石堆,看作下楼梯,每层楼梯放置的石子数为当前石堆与左堆的差值则减小某堆的石子,相当于从当前层楼梯移动石子到下一层。 这就转变成了阶梯博弈,胜利当前仅当第一层楼梯石子(最右堆)拿完,且上层所有楼梯(左边所有石堆)差值为零。

注意一点,最左边的石堆为最高层楼梯,最右边石堆为第一层阶梯。求奇数层的nim和即可。

# include
using namespace std;
int arr[1005], b[1005];
int main(int argc, char const *argv[]) {
    int u;
    cin >> u;
    while(u--) {
        int n;
        cin >> n;
        for (int i = 1; i <= n; ++i)
            cin >> arr[i];
        for(int i = n; i >= 1 ; i--)
            arr[i] -= arr[i - 1];
        int ans = 0;
        for(int i = n; i >= 1; i -= 2)ans ^= arr[i];

        if(ans == 0) cout << "NIE" << endl;
        else cout << "TAK" << endl;
    }
    return 0;
}

POJ1704

[描述]

1535812029294

如图所示的网格,放有若干象棋,每次一人可以移动一枚象棋,只能向左移动任意格数,但不能穿过左边的象棋,以及最左边的边界。无法移动象棋者输。、

[分析]

阶梯博弈最主要的是找到,那些隐形的阶梯。“阶梯”上可以没有石子,但是必须一直“存在”。可以将两者之间的一个“属性”当作一个阶梯,属性的值作为阶梯上的石子,当属性值的改变看作移动石子,且最终状态为,将石子移动到第0层,从而确定阶梯第一次的位置!

例如上题的两个整数之间的差值这个属性为阶梯,差值的大小就是在阶梯上的石子。

注意阶梯博弈,只能将石子从一个阶梯移动到下一层阶梯。

本题一个较明显的阶梯是:两个象棋之间的间隔,阶梯的石子是两象棋之间可移动的格数,显然最终结果是,自左到右象棋之间没有间隔,可以看作是将“石子”移动到最右边,因此右边是阶梯的第一层。

#include
#include
using namespace std;
int arr[1005], s[1005];
int main()
{
    int t;
    cin >> t;
    while(t--) {
        int n;
        cin >> n;
        for(int i = 1; i <= n; i++)
            cin >> arr[i];
        sort(arr + 1, arr + n + 1);
        int co = 1;
        arr[0] = 0;
        for (int i = n; i; --i) {
            int a = arr[i] - arr[i - 1] - 1;
            if(a <= 0) s[co++] = 0;
            else s[co++] = a;
        }

        int x = 0;
        for(int i = 1; i < co; i++)
            if(i & 1) x ^= s[i];
        if(x == 0) cout << "Bob will win" << endl;
        else cout << "Georgia will win" << endl;
    }
}

HDU4315

[描述]

n个人爬山,山是分层的,每个人都要爬到山顶。第k个人是特殊的人(将军),谁将将军移动到山顶谁获胜。

1535859934747

灰色是山顶,红色是将军。

[分析]

  1. 经过这道题我终于明白了,博弈的内涵。当我们将博弈划分为多个阶段时,单独处理某个阶段(先后手状态可能改变),最终的博弈结果不变。因此我们可以确定当前博弈阶段的终态,在利用熟知的nim博弈处理。
  2. 阶梯博弈需要“阶梯”——某个属性,状态的改变可能导致“阶梯”上的
    “石子”——属性值为0,但是这个“阶梯要存在”。

上题那个阶梯博弈的应用,以两象棋的可移动距离为“阶梯”,最终结果,两象棋的可移动距离为0,但是两象棋不会放置在一起,也就是可移动距离这个属性,“依然存在”。

通过1 的分析,本题可以转化为两个阶段,第一个阶段将棋子状态移动到

1535860290312

也就是上题的结果状态——n个棋子自山顶依次排列,再然后就是基础的小游戏:对于n个依次排列的象棋,我们将其中第k个棋子移动出界。

给出这个移动棋子的结论:

  1. k=1 k = 1 ,先手必胜,如果 k=2 k = 2 ,后手必胜。否则
  2. 如果 n n 为偶数,后手必胜
  3. 如果 n n 为奇数,先手必胜。

证明略

移动策略:如果 n n 为偶数,k为奇数那么后手就一次只将目前第一个棋子移动一格; 若k为偶数,那么后手需要直接将k棋子之前的所有棋子直接移出去。

#include
using namespace std;
int arr[1005], s[1005];
int main()
{
    int n, k;
    while(cin >> n >> k) {
        for(int i = 1; i <= n; i++) cin >> arr[i];

        if(k == 1) {
            cout << "Alice" << endl;
            continue;
        }

        int co = 1; arr[0] = 0;
        for(int i = n; i; i--) s[co++] = arr[i] - arr[i - 1] - 1;
        int x = 0;
        for(int i = 1; i < co; i++) if(i & 1)
                x ^= s[i];

        //分类先后手
         if(x == 0) {//后阶段,Alice先手
            if(k == 2) {cout << "Bob" << endl;continue;}
            if(n % 2 == 0) 
                cout << "Bob" << endl;
            else    
                cout << "Alice" << endl;
         }
         else {//后阶段,Alice后手
            if(k == 2) {
                cout << "Alice" << endl;
                continue;
            }
            else if(n % 2 == 0) cout << "Alice" << endl;
            else cout << "Bob" << endl;
         }
    }
    return 0;
}

Multi——SG

HDU3032

[描述]

一堆石子,可以任意在一堆上取出多枚石子丢弃,也可以取出多枚石子分成两堆石子,multi-nim的模板题

#include 
using namespace std;
const int maxn = 1e6 + 5;
int arr[maxn], sg[maxn];
int getsg(int x)
{
    if(x % 4 == 0) return x - 1;
    else if (x % 4 == 3)    return x + 1;
    else    return x ;
}
int main(int argc, char const *argv[])
{
    int t;
    cin >> t;
    while(t--) {
        int n; cin >> n;
        int x = 0;
        for (int i = 0; i < n; ++i) {
            int a; cin >> a;
            sg[i] = getsg(a);
            x ^= sg[i];
        }
        if(x) cout << "Alice" << endl;
        else cout << "Bob" << endl;
    }
    return 0;
}

SG函数打表

POJ2311

[描述]

一个n\times m的网格,每次可以沿着网格线,水平或垂直剪开。每次操作只会增加一块网格,第n次操作后有n+1块。问谁先检出1\times 1的网格谁胜。

[分析]

看着很像典型的 MultiSG M u l t i — — S G ,采用 Multi M u l t i — — SG S G 博弈思想进行求出 SG S G 函数即可。

要求 SG S G 函数我们就要找出边界条件——其 SG S G 值为 0 0 也就是必败态,经过分析我们可知, SG[2][2]=SG[2][3]=SG[3][2]=0 S G [ 2 ] [ 2 ] = S G [ 2 ] [ 3 ] = S G [ 3 ] [ 2 ] = 0 , 当我们面临了 2×2 2 × 2 2×3 2 × 3 3×2 3 × 2 的网格时,我们操作后必然会出现 1×m 1 × m 的网格,而对手就会直接胜利。

#include 
#include 
using namespace std;
int n, m;
int sg[210][210];
int getsg(int x, int y)
{//二维回溯求sg函数,不能任取,只能分成两堆
    bool vis[250] = {0};
    if(sg[x][y] != -1) return sg[x][y];
    for(int i = 2; i <= x - i; i++)     vis[getsg(x - i, y)^getsg(i, y)] = 1;   //后继状态
    for(int j = 2; j <= y - j; j++)     vis[getsg(x, y - j)^getsg(x, j)] = 1;

    for(int i = 0;; i++) {
        if(vis[i] == 0) {
            sg[x][y] = i;
            return i;
        }
    }
}
int main(int argc, char const *argv[])
{
    memset(sg,-1,sizeof(sg));
    sg[2][2]=sg[2][3]=sg[3][2]=0;
    while(scanf("%d%d",&n,&m)==2)
    {
        if(getsg(n,m)==0) cout<<"LOSE"<else cout<<"WIN"<return 0;
}

POJ3537

[描述]

一个 1×n 1 × n 的棋盘,每次在一个格子里画 x x ,首先连成三个 x x 的胜。

[分析]

典型的multi-sg,打表求 sg s g 函数。但是我们怎样定义这个状态呢?搜索每次画x的位置?那么必败状态又是什么?。。。麻烦

通过分析我们可知对于长度为 x x 的棋盘,如果我们在 i i 处画下记号,则对手肯定不会在 i2,i1,i+1,i+2 i − 2 , i − 1 , i + 1 , i + 2 画记号。

我们将问题转化一下:在 1×n 1 × n 的棋盘上两人轮流画 x x ,每次在 i i 处做下标记后就要将 i2,i2,i+1,i+2 i − 2 , i − 2 , i + 1 , i + 2 丢弃,那么就转化成了在长度为 i3xi2 i − 3 、 x − i − 2 的两个棋盘做标记,谁没有位置做标记者输。

转化一下思路,问题就容易解决了

#include
#include 
#include
#include
using namespace std;
const int maxn = 2e3 + 5;
int sg[maxn];

int getsg(int x)
{
    if(x <=0) return 0;
    if(sg[x] != -1) return sg[x];
    bool v[maxn]={0};
    for(int i = 1; i <= x; i++)             //将棋盘分为长度为i-3、x-(i+2)的两块棋盘
        v[getsg(i - 3)^getsg(x - i - 2)] = 1;
    for(int i = 0;; i++) {
        if(!v[i]) {
            sg[x] = i;
            break;
        }
    }
    return sg[x];
}
int main(int argc, char const *argv[])
{
    int n;
    memset(sg, -1, sizeof(sg));
    while(scanf("%d", &n)==1) {
        printf("%d\n", getsg(n) ? 1 : 2);
    }
    return 0;
}

你可能感兴趣的:(acm解题报告)