博弈专题入门总结(Nim 巴什 SG等证明+例题)

前言:近期刚学了博弈论相关的内容,感觉博弈论相比数论还是更形象一点,更好理解(对从0到1开创理论的前辈们表示大大尊敬!!)。特别是SG函数的相关理论,学完后以前很多要扎耳挠腮一两个小时的题都能秒出,这种感觉太妙了!当然博弈论还是很深奥很广泛的东西(报以敬畏),我也只停留在入门水平。本篇博客就总结一下这几天学习的一些知识,以后遇到新的理论慢慢补充吧!


一、巴什博弈

规则:

一堆n个物品,两个人轮流从这堆物品中取物, 规定每次取[1,m]个,最后取光者得胜,问先手必胜还是后手必胜。

分析:

我们先讲一个具体的例子:两个人在一堆n个石子里拿至少1个至多3个石子,轮流拿,问先手必胜还是后手必胜。
很容易可以知道无论先手拿几个,后手都可以拿若干个石子把两次拿的石子个数控制在4个(因为每次只能拿1-3个石子,先手拿1个后手拿3个,先手2个后手2个…必定保证两次为4个),那么可以很好地得出结论:如果n%4 == 0则后手必胜(先手不管怎么拿后手都可以控制数量使石子总数-4,那么如果石子总数是4的倍数,那肯定可以在轮到后手的某次刚好取完所以石子)。
由此,我们有特例推及一般:如果n%(m+1) == 0则后手必胜,否则先手必胜。

例题:

HDU1846 Brave Game
就是粗暴的套模板,代码:

#include<iotream>
using namespace std;
int main()
{
    int t,n,m;
    cin>>t;
    while(t--)
    {
        cin>>n>>m;
        if(n%(m+1) != 0)
            printf("first\n");
        else
            printf("second\n");
    }
    return 0;
}

HDU4764 Stone(巴什博弈拓展——取完石子输)
– 题意:两人在白板上写数字,若一个人写了数字X,则第二个人写的数字Y要满足1 <= Y – X <= k,且第一次写的数字要满足在[1, k]内,先写到数字N的人输。

  • 思路:实际上可以转换成取石子——有N个石子,每次只能取1-k个,先取完的输。注意这题和标准的巴什博弈不一样,这里是取完石子的输,实际上这是巴什博弈的一个变形。我们可以这样考虑:假设我们有n-1个石子,如果后手能取完n-1个石子,那么下一个人必须取走最后一个石子,所以后手必胜,所以取走n个石子,取完胜的后手必胜条件等价于取走n-1个石子,取完输的后手必胜条件——即(n-1)%(m+1) == 0则后手必胜。

  • 代码:

#include<iotream>
using namespace std;
int main()
{
    int t,n,m;
    cin>>t;
    while(t--)
    {
        cin>>n>>m;
        if((n - 1) % (m + 1) != 0)
            printf("first\n");
        else
            printf("second\n");
    }
    return 0;
}

二、对称博弈

没有特定的描述,只能算一种博弈思想吧(感觉用到了零和思维),一般题目数据都是首位相连的一个环,直接放例题感受吧。

HDU – 3951 Coin Game
题意:两个玩家用一圈N个硬币开始游戏。他们轮流从圆圈里取硬币,每次可以取1~k个连续的硬币。(假设10个硬币从1到10,k等于3,因为1和10是连续的,你可以拿走连续的10、1、2,但是如果2被拿走,你不能拿走1、3、4,因为1和3不是连续的)。拿最后一枚硬币的玩家获胜。
思路:当一次能取完时,先手必胜。当k=1,n是奇数时,先手必胜。其他时,后手必胜。在k=1时,双方轮流取一枚,奇数时,先手必胜,偶数时,后手必胜。在k>1时,先手能一次取完,先手必胜,否则后手必胜。开始时,硬币是一圈,先手取完后,变成一列,后手只要取1-2个让这一列变成偶数个即可,然后将这一列从中间分开成两列(镜像),之后先手如何操作,后手就在另一列如何操作,后手必胜。

  • 代码:
#include <stdio.h>
int main()
{
    int t,i,n,k;
    scanf("%d",&t);
    for( i=1;i<=t;++i )
    {
        scanf("%d%d",&n,&k);
        printf("Case %d: ",i);
        if( k==1 )
        {
            if( n&1 )   printf("first\n");
            else    printf("second\n");
        }
        else if( n<=k ) printf("first\n");
        else    printf("second\n");
    }
    return 0;
}

三、Nim博弈

小故事:

中国有一种游戏称为“拈(Nim)”,游戏规则是给出n列珍珠,两人轮流取珍珠,每次在某一列中取至少1颗珍珠,但不能在两列中取。最后拿光珍珠的人输。后经由被贩卖到美洲的奴工们外传,辛苦的工人们,在工作闲暇之余,用石头玩游戏以排遣寂寞。后来流传到高级人士,则用便士(Pennies),在酒吧柜台上玩。最有名的玩法,是把十二枚便士放成3、4、5三列,拿光铜板的人赢。后来,大家发现,先取的人只要在3那列里取走2枚,变成了1、4、5,就能稳操胜券了,游戏也就变得无趣了。于是大家就增加列数,增加铜板的数量,这样就让人们有了毫无规律的感觉,不易于把握。直到本世纪初,哈佛大学数学系副教授查理士•理昂纳德•包顿(Chales Leonard Bouton)提出一篇极详尽的分析和证明,利用数的二进制表示法,解答了这个游戏的一般法则,故Nim游戏的解法也称为Bouton’s Theorem。

规则:

有 n 堆石子,第 i 堆石子有 $a_i$ 个,每次可以从某一堆中取走若干个,先后手轮流取,最后无石子可取的人负。

结论:

将n堆石子每堆石子的数量求异或和(ans = a[1] ^ a[2] ^ … ^a[n]),若ans == 0,先手必败,否则先手必胜。

异或的概念

证明:

首先明确这类博弈的最终状态是每堆石子数量都为0,此时异或和也为0。
我们假设某个时刻$a_1 $^ $a_2$ ^ $a_3$ ^ … ^ $a_n$ $= k ≠ 0$,设k二进制数1的最高位在p位,那么一定存在一个数 $a_t$ 的二进制 1 的最高位也在 p 位。易得 $a_t$ ^ $k$ $< a_t$($a_t$ 最高位和 $k$ 的最高位都为 1 ,异或为 0 则必然小于 $a_t$ )那么从 $a_t$ 中拿掉 $a_t – a_t$ ^ $ k$个石子的操作一定合法(大于 0 且小于 k )那么第 t 堆石子的数量变为 $a_t – (a_t – a_t $^$ k)$ $= a_t$ ^ $k$ ,这个数和其余石子的异或和为$0$($k$^$k=0$)。此时,下一个人无论怎么拿石子,都会使得异或和不为0,因为至少拿一个就会使其中一个数的二进制发生变化使得异或和不为0。
由上面推导我们有两个结论:1.异或和不为0时一定有办法使异或和变为0。2.异或和为0时无论怎么操作都会使异或和不为0。
由于000000….为游戏的最终状态,其异或和也为0,反向往回推——00000为输的状态,异或和为0,前一个状态异或和不确定但保证存在可能异或和不为0的状态(结论1反推),由于决策者绝对聪明故前一种状态一定为异或和不为0(因为此时异或和不为0下一次异或和必为0且由于是最后一次所以下一个人必输),再往前推,前一种状态一定为异或和为0 的状态(结论2),反复往前推就可以得到,若初始状态的异或和不为0,则先手的必胜,否则先手必输。
画图解释(字有点小丑):

一点想法:

上图可以看成一个DAG图(有向无环图),然后XOR=0的点其实就是P点,XOR≠ 0的点为N点,PN点是交替出现的(决策者都很聪明的情况下,毕竟可能有傻子会在必胜的时候选择了必败的状态),这也符合:P点的下一个点必然为N点,P点一定有办法经过合法操作后变为N点,可以好好体会理解PN状态的妙处。

拓展:

若游戏规则变为 有 n 堆石子,第 i 堆石子有 $a_i$ 个,每次可以从最多k堆中取走m个,先后手轮流取,最后无石子可取的人负,如何考虑?

  • 方法:先将每堆石子%(m+1)(原理类似巴什博弈),然后将每堆石子用(k+1)进制求异或和,若为0则先手必败,否则必胜。
  • 对于(k+1)进制求异或和的解释:
    • 首先我们知道异或其实又称半加运算,即只执行加法而不执行进位(每位加起来后%2)。在原始 Nim 游戏中,只允许选取1 堆,所以最终 异或和 的结果是以 2 为进制执行半加运算。现在推广到允许取不超过 m 堆,所以最终 XOR 的结果是以 m+1 为进制执行半加运算。
    • 回忆一下原始的Nim游戏的证明过程,观察异或和的二进制最高位 1,选取其中数的二进制表示对应位也为 1 的数。只要取走使得该位变为 0,并将其他位%2后为0,则XOR变为0,此步骤操作就对应着将其中一堆取走若干个石子。
    • 当可以改变k堆时,我们用k+1进制取模计算XOR:
      可以证明改变最多 m 堆,也一定可以让其 XOR 变为 0。同样,观察 XOR 的 m+1 进制最高位 k,选取其中数的二进制表示对应位也为 1 的数,只要取走使得该位变为 0,且其余的位变成使得 XOR 尽可能的为 0。重复该操作最多 m 次,XOR 一定可以为 0。如果 XOR 为 0 后,你至少得改变其中一堆的一个石子,这时候 XOR 又不为 0 了,这也满足Nim游戏的规律。
  • 例题:Football Game POJ – 2315
  • 代码:
#include <algorithm>
#include <bitset>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <functional>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <vector>
#define LL long long
using namespace std;
const int maxn = 1e5 + 10;
const double PI = acos(-1.0);

int n, m, l, r;
int sg[41];
int ans[41];
int main(int argc, char const *argv[]) {
    while (cin >> n >> m >> l >> r) {
        int Max = l / (2 * PI * r);
        for (int i = 1; i <= n; i++) {
            scanf("%d", sg + i);
            sg[i] = sg[i] / (2 * PI * r) + 1;
            sg[i] %= Max + 1;
        }
        memset(ans, 0, sizeof(ans));
        for (int i = 1; i <= n; i++) {
            int x = sg[i];
            int cnt = 0;
            while (x) {
                ans[cnt++] += x % 2;
                x /= 2;
            }
        }
        int i;
        int x = 32;
        for (i = 0; i < x; i++) {
            printf("i=%d ans=%d x=%d\n", i, ans[i],x);
            if (ans[i] % (m + 1)) break;
        }
        if (i < 32)
            puts("Alice");
        else
            puts("Bob");
    }
    return 0;
}

例题:

poj 2975 Nim(求可行的必胜决策数)

  • 题意:让你输出有几种先手必胜的决策方式,先手必输则输出0。

  • 思路:由上文Nim证明过程可知,先手必胜则一开始XOR不为0,且下一步是要将XOR变为0,只要将其中一堆石子数量变为$a_t$ ^ $k$即可,要保证此操作合法,只要满足$a_t$ ^ $k < a_t$即可(每个符号的含义请翻阅上方关于Nim的证明部分),故只需求出异或和最后遍历一遍判断即可。

  • 代码:
#include <algorithm>
#include <bitset>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <functional>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <vector>
#define LL long long
using namespace std;
const int maxn = 1e5 + 10;

int main(int argc, char const *argv[]) {
    int n;
    int a[1001];
    while (cin >> n && n) {
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            cin >> a[i];
            ans ^= a[i];
        }
        if (!ans)
            cout << '0' << endl;
        else {
            int cnt = 0;
            for (int i = 0; i < n; ++i) {
                if (a[i] >  (ans ^ a[i])) cnt++;
            }
            cout << cnt << endl;
        }
    }
    return 0;
}

四、SG函数

必要概念:

  • P-positions,N-positions:
    对于一个游戏状态,我们定义它为:

    • P-position,在该状态,上一个移动的玩家(Previous player)能够获胜,也就是后手必胜
    • N-position,在该状态,下一个移动的玩家(Next player)能够获胜,也就是先手必胜

    显然,由定义可知:

    • 对于每一个 P-position,对于任何一个合法的移动下的下一个状态一定是一个 N-position
    • 对于每一个 N-position,一定存在一个合法移动,使得下一个状态是 P-position
      > 对于Nim游戏,P就是XOR为0的点,N就是XOR不为0的点,可以结合上方Nim的证明体会一下
  • 现在形式化地定义一个组合游戏:
    1.两个玩家构成的游戏
    2.游戏有一个状态集合,表示游戏过程中所有可能状态,通常是有穷的
    3.游戏的规则描述了在某个状态下玩家移动到下一个状态的合法移动,如果规则对两个玩家是相同的,就是 impartial (公平的),否则是 partizan (不公平的)
    4.玩家交替移动
    5.当玩家移动到一个状态,下一个移动的玩家没有可行的移动时,游戏结束;
    游戏无论怎么进行,始终能在有限步内结束,且游戏假设两个玩家都是足够聪明的玩家,不允许任何随机移动的存在。
    博弈题基本都是组合游戏(至少我目前碰到的)

  • 组合游戏的形式有两种,我们讨论的是Impartial Combinatorial Games ,以下简称 ICG。ICG 是指在游戏中,两个玩家所能进行的移动是完全相同的。对应的另一种形式 Partizan Combinatorial Games 就是指两个玩家分别有不同的移动,比如说我们熟知的象棋。
  • 我们定义一个操作叫做minimal excludant, 简写为mex,给出不出现在一个非负整数集合中的第一个非负整数,如mex(1,2,3)=0,mex(0,1,3,4)=2。

定义:

  • 首先我们将游戏转化成一个DAG图(有向无环图),每个节点表示一个游戏的状态,用一根有向线段连接AB两节点表示可以从A状态经过合法操作到达B状态。数学定义如下:对于一个有向图 G(X,F) ,其中 X 是游戏中所有状态的集合, F 表示状态之间的移动关系,令x 表示一个状态,那么 F(x)表示在 x 状态下的下一个合法状态,也就代表了移动的集合。对于节点x,如果F(x)为空集,那么x为终止节点(代表游戏结束)。
  • 对于图中任意一个节点,我们定义它的SG函数为g(x) = mex {g (y) : y ∈ F (x)},所以SG函数是递归定义的,我们令终止结点x,g(x)=0。
  • SG函数的性质:
    1 x是终止结点,则g(x) = 0
    2 g(x) = 0,对于x的每个后继节点y, g(y)≠0
    3 g(x) ≠ 0,存在一个x的后继节点y,g(y) = 0
    上述三条均可通过sg的定义得出,由此发现,对于任意的g(x) = 0,x对应的状态为P,对应任意的g(x) ≠0,x对应的状态为N

举例:

假设有7个石子 ,每次只能取1,3,4个石子,先取完石子的获胜。下面给出每个状态的SG值:

1

所以可以得出此情况先手必输。

程序化求sg函数

  • 由于sg函数是递归定义的,所以我们也可以dfs递归求解,在上图可以发现,很多的sg值都是由已知的sg值得来,所以可以用dp的思想记录所有的sg值,减少多余的运算。需要注意的有sg初始化为-1,因为sg可能有0值。给出一个模板加以理解吧,实际博弈题模板没有什么作用,因为比较灵活,没有特定模板。
int sg[maxn];
int a[maxn];//存的是每回合可选的操作
int get_sg(int x) {
    if(sg[x] != -1) return sg[x];//记忆化优化时间复杂度
    bool vis[2100];//用于记录当前状态的可达状态的sg值
    memset(vis, 0, sizeof(vis));
    for(int i = 1; i <= n; i++) {//n为可选操作的个数
        if(x-a[i] >=0)//将所有当前状态可达状态记录下来
        vis[get_sg(x-a[i])] = 1;
    }
    for(int i = 0;;i++) if(!vis[i]) return sg[x] = i;//记忆化存下每个状态的sg,这里的循环实现mex函数
}

multi-SG

对于多个ICG游戏 ,我们可以将其合并为一个更大的ICG游戏,合并后的g(x)为每个小游戏sg的异或和。这里可以考虑Nim游戏,把每个小游戏看成一堆石子,多个小游戏的sg值就是将其异或起来,这里其实也能看出,Nim游戏就是一种特殊的SG,每堆石子的sg值就是石子个数。

例题

POJ 3537 Crosses and Crosses

  • 题意:一条1*n的格子,可以任意在上面画‘×’,先连成三个×的赢。
  • 分析:对于一条带子················,随意画一个×后······×········,下一个人一定不会下在这个x的左右两格内,如·····×·×····或······××····因为如果这样下一个人就赢了,所以假设在第i个格子画了×那么该游戏就可以看成两个子游戏——在长度为i-3的带子上画x和在n-i-2的带子上画×两个游戏,则sg[i] = sg[i-3] ^ sg[n-i-2]。
  • 代码:
#include <algorithm>
#include <bitset>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <functional>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <vector>
#define LL long long
using namespace std;
const int maxn = 1e5+10;

int sg[2010];

int get_sg(int x) {
    if(x <= 0) return 0;
    if(sg[x] != -1) return sg[x];
    bool vis[2100];
    memset(vis, 0, sizeof(vis));
    for(int i = 1; i <= x; i++) {
        vis[get_sg(i-3)^get_sg(x-i-2)] = 1;
    }
    for(int i =0;;i++) if(!vis[i]) return sg[x] = i;
}

int main(int argc, char const *argv[]) {
    int n;
    cin >> n;
    memset(sg, -1, sizeof(sg));
    if(get_sg(n)) puts("1");
    else puts("2");
    return 0;
}

POJ 2311 Cutting Game

  • 题意:有一张WH的纸,每个人每回合可以横着剪一刀或者竖着剪一刀,每次只能剪整数长度,谁先剪出11的格子就赢
  • 思路:首先该题的P状态是剪出1 * n或者n * 1的纸条,由于SG的定义最终态为P状态,故这里可以把1 * n和n * 1当作最终状态。每次可以横着切或者竖着切,用两次循环遍历所有可以的切法(注意这里要从2个单位长度开始切,因为如果切开1个单位长度,下一个人就赢了),将一张纸切成两张,就可以看成两个子游戏,那么就可以用异或和来求大游戏的SG值,具体看代码。
  • 代码:
#include <algorithm>
#include <bitset>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <functional>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <vector>
#define LL long long
using namespace std;
const int maxn = 1e5 + 10;

int sg[210][210];
int vis[310];
int n, m;

int get_sg(int x, int y) {
    if (sg[x][y] != -1) return sg[x][y];
    for (int i = 2; i <= x - i; i++) {
        vis[get_sg(i, y) ^ get_sg(x - i, y)] = 1;
    }
    for (int i = 2; i <= y - i; i++) {
        vis[get_sg(x, i) ^ get_sg(x, y - i)] = 1;
    }
    for (int i = 0;; i++) {
        if (!vis[i]) return sg[x][y] = i;
    }
}

int main(int argc, char const *argv[]) {
    memset(sg, -1, sizeof(sg));
    while (cin >> n >> m) {
        memset(vis, 0, sizeof(vis));
        if (get_sg(n, m))
            cout << "WIN" << endl;
        else
            cout << "LOSE" << endl;
    }
    return 0;
}

CodeForces 138D World of Darkraft
题意:有一个 n × m 的棋盘,每个点上标记了 L; R; X 中的一个每次能选择一个没有被攻击过的点 (i; j),从这个点开始发射线,射线形状为: 若字符是 L,向左下角和右上角发,若字符是 R,向左上角和右下角发,若字符是 X,向左下左上右下右上发,遇到被攻击过的点停下来,如果轮到的人无法操作(全部点都被破坏)则输。
分析:
– 1.由于激光的特性,单数和双数的格子相互不受影响,所以将棋盘分成单双两部分,即两个子问题,如图:

在这里插入图片描述

如果不划分的话黑线激光会影响到白线激光,实际是不影响的。
– 2 激光的方向是与坐标轴成45°的,为了方便处理,将棋盘顺时针旋转45°。坐标(x,y)被映射为(x+y, x-y+W),于是射线分割的结果就是矩形而不是三角形或不规则的多边形了。
代码:

#include <algorithm>
#include <bitset>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <functional>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <vector>
#define LL long long
using namespace std;

const int maxn = 50;
int n, m;
int sg[maxn][maxn][maxn][maxn][2];
char mp[maxn][maxn];

int get_sg(int x1, int y1, int x2, int y2, int op) {
    if (sg[x1][y1][x2][y2][op] != -1) return sg[x1][y1][x2][y2][op];
    int vis[100];
    memset(vis, 0, sizeof(vis));
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if (((i + j) & 1) != op) continue;
            int x = i - j + m, y = i + j;
            if (x < x1 || x > x2 || y < y1 || y > y2) continue;
            int sgg = 0;
            if (mp[i][j] == 'L') {
                sgg = get_sg(x1, y1, x2, y - 1, op) ^
                      get_sg(x1, y + 1, x2, y2, op);
            } else if (mp[i][j] == 'R') {
                sgg = get_sg(x1, y1, x - 1, y2, op) ^
                      get_sg(x + 1, y1, x2, y2, op);
            } else if (mp[i][j] == 'X') {
                sgg = get_sg(x1, y1, x - 1, y - 1, op) ^
                      get_sg(x1, y + 1, x - 1, y2, op) ^
                      get_sg(x + 1, y1, x2, y - 1, op) ^
                      get_sg(x + 1, y + 1, x2, y2, op);
            }
            vis[sgg] = 1;
        }
    }
    for (int i = 0;; i++) {
        if (vis[i] == 0) {
            sg[x1][y1][x2][y2][op] = i;
            return i;
        }
    }
}

int main() {
    scanf("%d%d", &n, &m);
    memset(sg, -1, sizeof(sg));
    for (int i = 0; i < n; i++) {
        scanf("%s", mp[i]);
    }
    int ans = get_sg(0, 0, n + m - 1, n + m - 1, 0) ^
              get_sg(0, 0, n + m - 1, n + m - 1, 1);
    if (ans)
        printf("WIN\n");
    else
        printf("LOSE\n");

    return 0;
}

此题难度较大,思考了好久才有一点感觉,附上参考博客:博客1 博客2

五、暴力博弈

描述:

部分题目数据量较小,可以用dp思想直接枚举所有的可能然后暴力求解,这里有时候可能会用到状态压缩,或者还有一种题型为打表找规律,下面附上几道例题。

Permutation game(暴力枚举+状压)

  • 题意:给出一个数组,每轮拿走一个数字,先让数组升序或者只剩下一个数字的胜。
  • 分析:数组大小n的范围为[1,15],数据量小可以直接枚举所有情况,用状态压缩的方法存状态,开一个1<<16的dp数组,dp数组的下标可以转换成一个16位的二进制数,每位二进制数1代表数组对应的该位数没被取走,0代表被取走了。
  • 代码:
#include <algorithm>
#include <bitset>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <functional>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <vector>
#define LL long long
using namespace std;
const int maxn = 1e5+10;

int dp[1<<16];
int a[20], n;

bool check(int x) {//检查数字x所代表的状态下数组是否递增了
    int p = 0;
    for(int i = 0;i < n; i++) {
        if(x&(1<<i)) {//遍历x每一位二进制数
            if(p >= a[i])
            return 0;
            p =a[i];
        }
    }
    return 1;
}

bool dfs(int x) {
    if (check(x)) return dp[x] = 0;
    if (dp[x] != -1) return dp[x];
    for (int i = 0; i < n; i++) {//依次去掉每一个还存在于数组中的数字
        if (x&(1<<i)) {
            if(!x&(1<<i-1)) continue;
            if(!dfs(x^(1<<i))) return dp[x] = 1;
        }
    }
    return dp[x] = 0;
}

int main(int argc, char const *argv[]) {
    int t;
    cin >> t;
    while(t--) {
        memset(dp, -1, sizeof(dp));
        cin >> n;
        for(int i = 0; i < n ;i++) {
            cin >> a[i];
        }
        if(dfs((1<<n)-1))//1<<n的二进制为10000...(n个0),-1则为1111...(n个1)表示所有数都没被取的状态
            cout << "Alice\n";
        else
            cout << "Bob\n";
    }
    return 0;
}

URAL 2104 Game with a Strip(暴力)

  • 题意:给你一张纸带两面都印有字符A和B(题目给出),每次操作可以将纸带向任意一面折叠,被盖住的那一面字符则消失,当存在的字符全为A则Alice获胜,如果全为B则Bob获胜,否则平局,注意如果纸带长度为奇数则不能继续折叠,如果字符没有全一样则平局。
  • 思路:可以知道只要判断当纸带长度从偶数变成奇数的那一次纸带上的图片是怎么样的就能判断谁赢了。我们用-1表示Bob赢,1表示Alice赢,0表示平局,每次可以向两面折叠,那么就有两种可能。但是每次操作后,Bob都希望数字尽量小(最好-1实在不行就0,最坏是1),Alice则希望数字尽量大。所以我们要用一个flag记录当前回合是谁操作,如果是Bob操作则选取两次中数字小的那次操作,Alice则选择较大的那个,用递归的方式求值。
  • 代码:
#include <algorithm>
#include <bitset>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <functional>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <vector>
#define LL long long
using namespace std;
const int maxn = 5e5+10;
char s[maxn];

int dfs(int l, int r, int n, int flag) {

    if(n % 2 == 0 && ((n/2)&1)) {
        int a = 0, b = 0;
        for(int i = l;i <= r;i++) {
            if(s[i] == 'A') a++;
            else b++;
        }
        if(a == n) return 1;
        else if(b == n) return -1;
        else return 0;
    }
    int mid = (l+r)/2;
    int t1 = dfs(l, mid, n/2, flag^1);
    int t2 = dfs(mid+1, r, n/2, flag^1);
    if(flag) return max(t1, t2);
    return min(t1, t2);
}

int main(int argc, char const *argv[]) {
    int n;
    cin >> n;
    if(n&1) {
        int a = 0, b = 0;
        cin >> s;
        for(int i = 0;i < n;i++) {
            if(s[i] == 'A') a++;
            else b++;
        }
        cin >> s;
        for(int i = 0;i < n;i++) {
            if(s[i] == 'A') a++;
            else b++;
        }
        if(a == 2*n) puts("Alice");
        else if(b == 2*n) puts("Bob");
        else puts("Draw");
    }
    else {
        int t1, t2;
        cin >> s;
        t1 = dfs(0, n-1, n, 0);
        cin >> s;
        t2 = dfs(0, n-1, n, 0);
        t1 = max(t1, t2);
        if(t1>0) puts("Alice");
        else if(t1 == -1) puts("Bob");
        else puts("Draw");
    }
    return 0;
}

POJ 1082 Calendar Game(找规律)

  • 题意:给出一个年份,每次操作可以让月份+1或者日期+1,最先使得日期变为2001-11-4获胜。
  • 分析:找规律可以发现,day+month最终到达的是奇数,每次走的都是要不是奇数点要不是偶数点,所以让后者每次走的都是偶数点,那么先者一定能赢,然后还有两个特例9.30与11.30为奇数开局,但是先手必胜。
  • 代码:
#include<iostream>
#include<cstring>
#include <cstdio>
using namespace std;
int main()
{
    int y,m,d , t;
    scanf("%d",&t);
    while(t--)
    {
         scanf("%d%d%d",&y,&m,&d);
         if((m+d)%2==0 || m==9 && d==30 || m==11 && d==30)  printf("YES\n");
         else printf("NO\n");
    }
    return 0;
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇