博弈题整理


利用sg函数求解

公平组合游戏都可以转化成一个DAG,每一个点都有一个胜负状态以及sg值。

常见方法的是根据问题找出$sg$函数递推式,然后利用各种方法,在合理的复杂度内得到$sg$函数的解,比如打表猜测、dp、等等

Steins;Game(线性基)

题意:给出n堆石子,后手方可以任意染色,每一步可以从任何白色堆里取走任意石子,或者从最小的黑色堆里取走任意石子,问后手必胜的染色方案数

白色石头即为nim游戏,其sg值可以由异或和直接得到。

黑色石头的sg值可以归纳或者打表推得为:最小的黑色石堆个数 - (相同的最小石堆个数 + [所有黑色石堆石子个数不同])% 2

题目要求先手必败,即白色石头sg值$sgwht$异或黑色石头异或值$sgblk$结果为0。

枚举黑色石头的最小石子数及堆数,再枚举是否有不相同的石子数,再用异或线性基计算方案数。

记插入的数个数为$tot$,被记录在线性基内的数个数为ins,那么只要利用当前线性基能够异或出某个数x,那么异或出这个数的方案数为$2^{tot-ins}$,这一步可以这样理解:对于任何一个插入但没有记录在线性基内的数,无论它包含或不包含在方案数中,总能找到一个方案使异或和为x,且对于线性基内的数来说,异或和为x的方案唯一。

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;

const int N = 1e5 + 5, MOD = 1e9 + 7;

ll ins, tot, n, same[N], num[N], pw[N], ans;

ll a[N], sum[N];

ll sgblk, sgwht;

struct Linear_base {
    ll p[70];
    Linear_base() {
        memset(p, 0, sizeof(p));
    }
    void Insert(ll x) {
        tot++;
        for (int i = 62; i >= 0; i--) {
            if (!(x >> i)) continue;
            if (!p[i]) {
                p[i] = x;
                ins++;
                return;
            }
            x ^= p[i];
        }
    }
    bool check(ll x) {
        for (int i = 62; i >= 0; i--) {
            if (!(x >> i)) continue;
            if ((x >> i) & 1) x ^= p[i];
        }
        return !x;
    }
}lb;

struct Comb {
    int fac[N], ifac[N];
    void init(int n) {
        fac[0] = ifac[0] = 1;
        fac[1] = ifac[1] = 1;
        for (int i = 2; i <= n; i++) fac[i] = 1ll * fac[i - 1] * i % MOD;
        for (int i = 2; i <= n; i++) ifac[i] = 1ll * (MOD - MOD / i) * ifac[MOD % i] % MOD;
        for (int i = 2; i <= n; i++) ifac[i] = 1ll * ifac[i - 1] * ifac[i] % MOD;
    }
    int calc(int n, int m) {
        if (n < m) return 0;
        return 1ll * fac[n] * ifac[m] % MOD * ifac[n - m] % MOD;
    }
}C;

int main() {
    cin >> n;
    C.init(n);
    for (int i = 1; i <= n; i++) {
        scanf("%lld", &a[i]);
    }
    sort(a + 1, a + 1 + n);
    for (int i = 1; i <= n; i++) {
        sum[i] = sum[i - 1] ^ a[i];
        if (a[i] == a[i - 1]) num[i] = num[i - 1] + 1;
        else num[i] = 1;
    }
    for (int i = n; i >= 1; i--) {
        if (a[i] == a[i + 1]) same[i] = same[i + 1];
        else same[i] = num[i];
    }
    pw[0] = 1;
    ans += sum[n] == 0;
    for (int i = 1; i <= n; i++) pw[i] = 1ll * pw[i - 1] * 2 % MOD;
    for (int i = n; i >= 1; i--) {
        ll tmp = 0;
        sgwht = sum[i - num[i]] ^ sum[n] ^ sum[i];
        sgblk = a[i] - (1 - num[i] % 2);
        tmp += (sgblk ^ sgwht) == 0;
        sgblk = a[i] - (num[i] % 2);
        ll q = sgblk ^ sgwht;
        if (lb.check(q)) tmp += pw[tot - ins];
        if (!q) tmp = tmp + MOD - 1;
        tmp %= MOD;
        ans = (ans + 1ll * tmp * C.calc(same[i], num[i])) % MOD;
        if (num[i] == 1) {
            for (int j = 1; j <= same[i]; j++) lb.Insert(a[i]);
        }
    }
    cout << ans;
    return 0;
}

P5363 [SDOI2019]移动金币(阶梯nim,dp求解)

题意很简单,就是一个阶梯nim模型。结论是当且仅当奇数位石堆石子异或和为0时先手必败,否则先手必胜。

  • 因为只要某一方将异或和调整为0后,若某回合对方调整第$i$堆石堆,若$i$为奇数,则按照Nim游戏的规则,对奇数位置的石堆作相应调整维持异或和为0即可;若$i$为偶数,则只需要调整第$i-1$堆,仍然可以维护异或和为0的性质(注意到$i$为偶数时有$i>=2$,则$i-1$必然存在,不能选取偶数堆做异或和的原因就在于此)

难点在于求方案数,即把n-m个球放到m个盒子里,且奇数位的盒子异或和不为0的方案数。

首先套一个容斥,异或和为0肯定比异或和不为0好求,那么只要求出奇数位盒子异或和为0的方案数即先手必败的方案数,再用总方案数$\tbinom{n}{m}$减去即为答案。

而异或和为0的一个很好的性质就是可以进行二进制拆位,限制每一位都为0,也就是每一位上的“1”都必须为偶数个。

这里可以把奇数位和偶数位分开来独立考虑。

先考虑奇数位(共$\frac{m + 1}{2}$位):设计状态$dp[i][j]$为考虑到二进制第j位,和为i,异或和为0的方案数

那么有

再乘上偶数位的方案数,方案数相当于把$n -m-i$个球放进$m - \frac{m + 1}{2}$个盒子里的方案数,用插板法即可解决

令$space=n-m,tot=\frac{m + 1}{2}$

最终先手必败的方案数为

最终用总方案数减去先手必败方案数即可

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;

const int N = 2e5 + 5, M = 31, MOD = 1e9 + 9;

ll ans;

int dp[N][M];
// dp[i][j] 考虑到第j位,和为i,异或和为0的方案数
// dp[i][j] = dp[i - 2^j * k][j - 1] * C(tot, k) (i  - 2^j * k > 0 && k = [0, tot])

int n, m, tot, space, pw[N];

int fac[N], ifac[N];

int init() {
    fac[0] = ifac[0] = fac[1] = ifac[1] = 1;
    for (int i = 2; i < N; i++) fac[i] = 1ll * fac[i - 1] * i % MOD;
    //for (int i = 2; i < N; i++) ifac[i] = 1ll * (MOD - MOD / i) * ifac[MOD % i] % MOD;
    //for (int i = 2; i < N; i++) ifac[i] = 1ll * ifac[i - 1] * ifac[i] % MOD;
}

int C(int n, int m) {
    if (n < m) return 0;
    return 1ll * fac[n] * ifac[m] % MOD * ifac[n - m] % MOD;
}

int main() {
    cin >> n >> m;
    space = n - m;
    tot = (m + 1) / 2;
    init();
    pw[1] = 1;
    for (int i = 2; i <= 30; i++) pw[i] = pw[i - 1] * 2 % MOD;
    dp[0][0] = 1;
    for (int j = 1; j <= 30; j++) {
        for (int i = 0; i <= space; i++) {
            for (int k = 0; k <= tot; k++) {
                if (k % 2 == 1) continue;
                if (i - 1ll * pw[j] * k >= 0) {
                    dp[i][j] = (dp[i][j] + 1ll * dp[i - 1ll * pw[j] * k][j - 1] * C(tot, k) % MOD) % MOD;
                }
            }
        }
    }
    for (int i = 0; i <= space; i++) {
        ans += 1ll * dp[i][30] * C(space - i + m - tot, m - tot) % MOD;
        ans %= MOD;
    }
    ans = C(n, m) - ans;
    ans %= MOD;
    (ans += MOD) %= MOD;
    cout << ans;
    return 0;
}

网格博弈

E - Candy Piles

题意:n堆石子,要么把最多的那堆全拿走,要么每堆拿一个

根据糖果数从大到小排序后可以转化成一个网格模型。

例如

5
12 9 4 3 12

可以转化成

000
0000
000000000
000000000000
000000000000

相当于以左下角为原点,每次选择向右(每堆都拿一个),或者向上(拿走最大堆)走一步,走最后一步的人输。

对于这种只能向右或向上走一步的模型,结论是其某一条对角线上的状态是相同的。那么原点状态$(1,1)$可以通过任意的$(x,x)$来判断。我们取到最大的$x$,可知此状态只能一直向右或一直向上走,再根据必败态的条件,令$(x,x)$可以向上走$a$步,可以向右走$y$步,则$(x,x)$为必胜态当且仅当$a$为奇数或$y$为奇数,否则$(x,x)$为必胜态。

#include <bits/stdc++.h>
#define debug(x) cerr << #x << " = " << x << ' '
#define debugl(x) cerr << #x << " = " << x << '\n'

using namespace std;
typedef long long ll;

const int N = 1e5 + 5;

int n, a[N], m;

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    sort(a + 1, a + 1 + n, [&](int u, int v) {
        return u > v;
    });
    for (int i = 1; i <= n; i++) {
        if (a[i] >= i) m = i;
    }
    int num1 = 0;
    while(a[m + num1] >= m) num1++;
    num1--;
    int num2 = a[m] - m;
    if (num1 % 2 || num2 % 2) printf("First\n");
    else printf("Second\n");
    return 0;
}

状态设计博弈

即设计两种状态A、B,如果说A至少有一种操作能转化到B,而B状态下的任何操作都只能转化到A,且必败态为B,那么持有A状态的一方必胜。

这种状态设计有时候需要一点想象力。

Nullify The Matrix

题意:给一个n*m的矩阵,每次玩家选择两个点$(r1,c1),(r2,c2),r1\le r2, c1\le c2$(即$(r1,c1)$一定在$(r2,c2)$的左下),然后选择一条两点之间的简单路径,给$(r1,c1)$减去一个非0值,把路径上其他点修改为任意值,矩阵全0时无法操作,无法操作者败,问先手胜负情况

令$d(x)$为所有满足$i + j == x$的$a_{ij}$的异或和,即所有对角线上的数的异或和

设计状态:

$S:\forall x\in d(x), d(x)=0$

$S’:\exists x\in d(x), d(x)\neq0$

只要还能操作,$S$状态的任何操作一定会到达$S’$,$S’$状态一定有办法能到达$S$,且必败态属于$S$。

于是谁能把局面维护在$S$状态谁获胜。

显然,若起始状态为$S$,则后手必胜,若起始状态为$S’$则先手必胜。

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;

const int N = 1e2 + 5;

int T, n, m, a[N][N];

void solve() {
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            scanf("%d", &a[i][j]);
        }
    }
    for (int i = 2; i <= m + n; i++) {
        int now = 0;
        for (int j = 1; j <= i - 1; j++) {
            int k = i - j;
            if (j <= n && k >= 1 && k <= m)
                now ^= a[j][k];
        }
        if (now != 0) {
            printf("Ashish\n");
            return;
        }
    }
    printf("Jeel\n");
}

int main() {
    cin >> T;
    while(T--) {
        solve();
    }
    return 0;
}

对称博弈

题目很多也不难,关键是要有这个思路就是:如果A操作后B一定也能操作,且B不是先手或B一定可以完成第一部操作,则B必胜

博弈相关

先用博弈的知识将问题转化之后解其他问题,其实有点脱离博弈的范畴

Remove the Prime

题意:每次可以选择一个区间$[l,r]$并移除它们的一个质因子,不能操作的人失败

博弈部分:对于每个质因子,连续的一段有这个质因子的区间可以看成一堆石子,每次操作相当于把一整段区间从中间拿掉一部分之后分成两段(两段都可能为0),如果中间拿掉的个数为k(k>1),对应sg函数的可以写成$sg(x)=MEX\{sg(i)\oplus sg(x-k-i) \}$

但是这个sg函数值其实就是$sg(x)=x$,因为如果$a+b<x$,那么有$sg(a)\oplus sg(b)<x$,这说明$sg(x)$最大值取到x,和最基本的nim问题一样

所以只要对于每个素数把这样的区间长度求出来然后异或(每个区间长度就相当于nim问题中每堆石子的个数),需要用到Miller-rabin之类的判素数方法

#include <bits/stdc++.h>
#define debug(x) cerr << #x << " = " << x << ' '
#define debugl(x) cerr << #x << " = " << x << '\n'
#define rep(x,y,z) for (int x=(y);(x)<=(z);(x)++)

using namespace std;
typedef long long ll;
typedef long long LL;

const int N = 1e6 + 5;

LL power(__int128 a,LL b,LL p)
{
    __int128 x=1;
    while (b) {if (b&1) x=x*a%p;b>>=1;a=a*a%p;}
    return x%p;
}
bool Test(LL a,LL n)
{
    LL m=n-1,t=0;
    while (!(m&1)) {t++;m>>=1;}
    __int128 x=power(a,m,n);
    if (x==1) return false;
    rep(i,1,t)
    {
        if (x!=n-1&&x!=1&&x*x%n==1) return true;
        x=x*x%n;
    }
    return x!=1;
}
bool MillerRabbin(LL n)
{
    const int times=4;
    if (n==2) return true;
    if (n<2||!(n&1)) return false;
    srand(time(0));
    rep(i,1,times) if (Test(rand()%(n-1)+1,n)) {return false;}
    int temp[]={2, 325, 9375, 28178, 450775, 9780504, 1795265022};
    rep(i,0,6) if (Test(temp[i]%(n-1)+1,n)) {return false;}
    // cout<<n<<"T"<<endl;
    return true;
}

int vis[N],prime[N],countPrime;
void getPrime() {
    for (int i=2;i<N;i++) {
        if (!vis[i]) prime[++countPrime]=i;
        for (int j=1;j<=countPrime;j++) {
            if (i*prime[j]>=N) break;
            vis[i*prime[j]]=1;
            if (i%prime[j]==0) break;
        }
    }
}

struct status {
    ll num, len;
    bool avil;
}p[3];

int last[N], st[N], n;

ll a[1005];

int ans;

ll gcd(ll x, ll y) {
    if (y == 0) return x;
    return gcd(y, x % y);
}

bool isSquared(ll x) {
    ll s = sqrt(x);
    return (1ll * s * s) == x;
}

int main() {
    getPrime();
    cin >> n;
    for (int i = 1; i <= n; i++) {
        scanf("%lld", &a[i]);
        for (int j = 1; j <= countPrime; j++) {
            if (a[i] % prime[j] != 0) continue;
            if (last[j] == 0) {
                st[j] = last[j] = i;
            } else if (last[j] != i - 1) {
                ans ^= last[j] - st[j] + 1;
                st[j] = last[j] = i;
            } else last[j] = i;
            while(a[i] % prime[j] == 0) a[i] /= prime[j];
        }
    }
    for (int j = 1; j <= countPrime; j++) {
        if (last[j] != 0) ans ^= last[j] - st[j] + 1;
    }
    if (isSquared(a[1])) a[1] = sqrt(a[1]);
    if (a[1] == 1) {
        p[1].avil = p[2].avil = 0;
        p[1].len = p[2].len = 0;
        p[1].num = p[2].num = -1;
    } else if (MillerRabbin(a[1])) {
        p[1].avil = 1, p[1].num = a[1], p[1].len = 1;
        p[2].avil = 0, p[2].num = -1, p[2].len = 0;
    } else {
        p[1].avil = p[2].avil = 1;
        p[1].num = p[2].num = -1;
        p[1].len = p[2].len = 1;
    }
    for (int i = 2; i <= n; i++) {
        if (isSquared(a[i])) a[i] = sqrt(a[i]);

        ll GCD = gcd(a[i], a[i - 1]);

        if (GCD == 1) {
            if (p[1].avil) ans ^= p[1].len;
            if (p[2].avil) ans ^= p[2].len;
            if (a[i] == 1) {
                p[1].avil = p[2].avil = 0;
                p[1].len = p[2].len = 0;
                p[1].num = p[2].num = -1;
            } else if (MillerRabbin(a[i])) {
                p[1].avil = 1, p[1].num = a[i], p[1].len = 1;
                p[2].avil = 0, p[2].num = -1, p[2].len = 0;
            } else {
                p[1].avil = p[2].avil = 1;
                p[1].num = p[2].num = -1;
                p[1].len = p[2].len = 1;
            }
        } else {
            if (a[i] == a[i - 1]) {
                p[1].len++, p[2].len++;
            } else {
                if (p[1].avil && p[1].num == GCD) {
                    p[1].len++;
                    if (p[2].avil) {
                        ans ^= p[2].len;
                        p[2].avil = 0;
                        p[2].len = 0;
                        p[2].num = -1;
                    }
                } else
                if (p[2].avil && p[2].num == GCD) {
                    p[2].len++;
                    if (p[1].avil) {
                        ans ^= p[1].len;
                        p[1].avil = 0;
                        p[1].len = 0;
                        p[1].num = -1;
                    }
                } else
                if (p[1].avil && p[1].num == -1) {
                    p[1].num = GCD;
                    p[1].len++;
                    if (p[2].avil) {
                        ans ^= p[2].len;
                        p[2].avil = 0;
                        p[2].len = 0;
                        p[2].num = -1;
                    }
                } else
                if (p[2].avil && p[2].num == -1) {
                    p[2].num = GCD;
                    p[2].len++;
                    if (p[1].avil) {
                        ans ^= p[1].len;
                        p[1].avil = 0;
                        p[1].len = 0;
                        p[1].num = -1;
                    }
                }
                if (GCD != a[i]) {
                    if (!p[1].avil) {
                        p[1].avil = 1, p[1].num = a[i] / GCD, p[1].len = 1;
                    } else {
                        p[2].avil = 1, p[2].num = a[i] / GCD, p[2].len = 1;
                    }
                }
            }
        }
    }
    if (p[1].avil) ans ^= p[1].len;
    if (p[2].avil) ans ^= p[2].len;
    if (ans) cout << "First\n";
    else cout << "Second\n";
    return 0;
}

Down We Dig

披着博弈皮的状压dp,实际上题目就是利用博弈自然转化后得到的DAG,在DAG图上进行dp。

可以发现第一行的胜负情况是由最后八行的胜负情况可以确定的,用$dp[i][j]$表示考虑前$i$行,后八行胜负状态为$j$时,第一行的胜负情况

而$dp[i][j]$的求解是可以由$dp[i-1]$推导出的,$j$已知的情况下,唯一要确定的是第$i-8$行的胜负情况,而第$i-8$行又可以由$j$导出,得到上一层状态$prev$,所以$dp[i][j]=dp[i-1][prev]$

可以发现总状态数$2^8O(n)$,转移为$O(1)$,可以用记忆化搜索的方式剪枝。

#include <bits/stdc++.h>
#define all(x) x.begin(), x.end()
#define mem(x, y) memset(x, y, sizeof(x))
#define debug(x) cerr << #x << " = " << x << ' '
#define debugl(x) cerr << #x << " = " << x << '\n'

using namespace std;
typedef long long ll;

const int N = 3e5 + 5, M = (1 << 8) - 1;

int n, status[N], path[N][10];

int dp[N][M + 5];

char s[10];

int bitcount(int x) {
    int res = 0;
    while(x) {
        res += x & 1;
        x >>= 1;
    }
    return res;
}

int dfs(int x, int st) {
    if (dp[x][st] != -1) return dp[x][st];
    int &res = dp[x][st];
    if (x <= 8) return res = (st >> (x - 1)) & 1;
    int prev = x - 8, pst = st >> 1;
    for (int i = 1; i <= 8; i++) {
        if (path[prev][i] && !((st >> (8 - i)) & 1)) {
            pst |= (1 << 7);
            break;
        }
    }
    return res = dfs(x - 1, pst);
}

int main() {
    scanf("%d\n", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%s", s);
        int temp = 0;
        for (int j = 0; j <= 7; j++) {
            temp = temp * 2 + (s[j] == 'W' ? 0 : 1);
        }
        status[i] = temp;
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= 8; j++) {
            if (8 - bitcount(status[i] ^ status[i + j]) >= j) path[i][j] = 1;
        }
    }
    mem(dp, -1);
    int now = 0;
    for (int i = 1; i <= n; i++) {
        int now = 0;
        for (int j = 1; j <= 7; j++) {
            int target = i - j;
            for (int k = 1; k <= j; k++) {
                if (path[target][k] && !((now >> (j - k)) & 1)) {
                    now |= (1 << j);
                    break;
                }
            }
        }
        printf("%d", 2 - dfs(i, now));
    }
    printf("\n");
    return 0;
}

Gems Fight!(状压dp)

题意:给出B个背包,每个背包里由n个宝石,每种宝石一种颜色。两人轮流选一个背包加入公共池,如果加入后相同颜色的宝石超过S个,则得分并继续选背包,否则由对手选择,问采取最优策略下最终先后手得分之差的最大值

背包总数21,状压表示背包的选择状态然后dp就可以了,dp[i]表示先手者当前状态为i时,先手得分-后手得分的最大值

对于每一种状态,枚举最后一个选择的背包(或者说枚举前驱状态),可知是否得分,即是否先后手易位,如果得分,$dp[prev]=dp[i]+increment$,否则$dp[prev]=-dp[i]$

#include <bits/stdc++.h>

using namespace std;

const int N = (1 << 21) + 5;

int dp[N];

int col[10], col2[10], up, g, b, s;

vector<int> bag[25];

int main() {
    while(scanf("%d %d %d", &g, &b, &s), g != 0 && b != 0 && s != 0) {
        for (int i = 1; i <= b; i++) {
            int x; scanf("%d", &x);
            bag[i].clear();
            while(x--) {
                int y; scanf("%d", &y);
                bag[i].push_back(y);
            }
        }
        up = (1 << b) - 1;
        memset(dp, -0x3f, sizeof(dp));
        dp[up] = 0;
        for (int i = up; i >= 0; i--) {
            memset(col, 0, sizeof(col));
            for (int j = 1; j <= b; j++) {
                if (i & (1 << (j - 1))) {
                    for (int x : bag[j])
                        col[x]++;
                }
            }
            for (int j = 1; j <= b; j++) {
                if ((i & (1 << (j - 1))) == 0) continue;
                memset(col2, 0, sizeof(col2));
                for (int x : bag[j]) col2[x]++;
                int incre = 0;
                for (int k = 1; k <= g; k++) {
                    incre += col[k] / s - (col[k] - col2[k]) / s;
                }
                if (incre) dp[i ^ (1 << (j - 1))] = max(dp[i ^ (1 << (j - 1))], dp[i] + incre);
                else dp[i ^ (1 << (j - 1))] = max(dp[i ^ (1 << (j - 1))], -dp[i]);
            }
        }
        /*for (int i = 1; i <= up; i++)
            cout << dp[i] <<endl;
        */
        cout << dp[0] << endl;
    }
    return 0;
}

二分图博弈

如果状态可以分成两类,且任意操作之后状态一定发生转换,那么这张DAG可以用二分图来表示。这类博弈先手必胜的条件是起始点存在于任何一种完美匹配中。

如果起始点不在某一个完美匹配中,那么后手方就只看这种起始点不在完美匹配的匹配方案,先手方走的任意一步一定走到一个完美匹配中的点(否则说明又发现了一对匹配,与完美匹配的假设矛盾),而后手方只需要走匹配边,也就是说只要先手方能走,后手方一定能走,则后手必胜

起始点在任何一种完美匹配中,先手方选择一种完美匹配,然后一直走匹配边。

判断起始点是否在任何一种完美匹配中,可以先不加起始点跑一次最大匹配,然后把起始点加进去再跑一次,如果两次的结果相同,说明起始点可以不在某一种完美匹配中。实际上不用跑两次完整的,第二次单独加一个点之后不需要重新建图,在原来的残余网络上跑,判断是否产生增量即可。

Combination Lock

题意:给一个最多五位的锁,每次可以拨一位,使该位置上的数+1或-1,0-9循环,有若干个数不能拨到,也不能重复。已知起始状态,两个人轮流拨,不能拨的人失败

根据各位和的奇偶性建二分图,用二分图博弈的理论解决即可

#include <bits/stdc++.h>
#define fi first
#define se second
#define all(x) x.begin(), x.end()
#define mem(x, y) memset(x, y, sizeof(x))
#define debug(x) if (DEBUG) cerr << #x << " = " << x << ' ';
#define debugl(x) if (DEBUG) cerr << #x << " = " << x << '\n';

using namespace std;
typedef long long ll;
typedef long long LL;

//const int DEBUG = 1;
const int DEBUG = 1;
const int N = 1e5 + 5, INF = 0x3f3f3f3f, MOD = 1e9 + 7;

struct Dinic{
    struct Edge{
        int to;LL v;int rev;
    };
    int n,s,t,dep[N],_cur[N];
    bool used[N];
    vector<Edge>G[N];
    queue<int>L;
    void init(int n_,int s_,int t_) {
        n=n_,s=s_,t=t_;
        for (int i=0;i<=n;i++) G[i].clear();
    }
    void addEdge(int x,int y,LL v) {
        G[x].push_back({y,v,G[y].size()});
        G[y].push_back({x,0,G[x].size()-1});
        //G[y].push_back({x,v,G[].size()});
        //G[y].push_back({x,0,G[x].size()-1});
    }
    bool bfs() {
        for (int i = 0; i <= n; i++) used[i] = 0;
        //memset(used,0,sizeof(used));
        dep[s]=1,used[s]=1;
        L.push(s);
        while(!L.empty()) {
            int cur=L.front();
            L.pop();
            for (int i=0;i<G[cur].size();i++) {
                Edge &e=G[cur][i];
                if (!used[e.to] && e.v>0) {
                    dep[e.to]=dep[cur]+1;
                    used[e.to]=1;
                    L.push(e.to);
                }
            }
        }
        return used[t];
    }
    LL dfs(int c,LL flow) {
        if (c==t ||flow==0) return flow;
        LL ret=0,f;
        for (int &i=_cur[c];i<G[c].size();i++) {
            Edge &e=G[c][i];
            if (dep[e.to]==dep[c]+1 && (f=dfs(e.to,min(flow,e.v)))>0) {
                ret+=f,flow-=f,e.v-=f;
                G[e.to][e.rev].v+=f;
                if (!flow) break;
            }
        }
        return ret;
    }
    LL go() {
        LL ans=0;
        while(bfs()) {
            for (int i = 0; i <= n; i++) _cur[i] = 0;
            // memset(_cur,0,sizeof(_cur));
            ans+=dfs(s,1LL*100*INT_MAX);
        }
        return ans;
    }
}dinic;

int T, m, n, st, mx, prev[N][6], succ[N][6];

bool ban[N], even[N];

vector<int> sv;

int getprev(int x, int pos) {
    int res = 0, p = 1;
    for (int i = 1; i <= m; i++) {
        if (i != pos) res = res + p * (x % 10);
        else res = res + p * (x % 10 == 0 ? 9 : (x % 10) - 1);
        x /= 10;
        p *= 10;
    }
    return res;
}

int getsucc(int x, int pos) {
    int res = 0, p = 1;
    for (int i = 1; i <= m; i++) {
        if (i != pos) res = res + p * (x % 10);
        else res = res + p * (x % 10 == 9 ? 0 : (x % 10) + 1);
        x /= 10;
        p *= 10;
    }
    return res;
}

void addpoint(int i) {
    if (even[i]) dinic.addEdge(0, i + 1, 1);
    else dinic.addEdge(i + 1, mx + 2, 1);
    for (int j = 1; j <= m; j++) {
        int target = getprev(i, j);
        if (!ban[target]) {
            if (even[i])
                dinic.addEdge(i + 1, target + 1, 1);
            else dinic.addEdge(target + 1, i + 1, 1);
        }
        target = getsucc(i, j);
        if (!ban[target]) {
            if (even[i])
                dinic.addEdge(i + 1, target + 1, 1);
            else dinic.addEdge(target + 1, i + 1, 1);
        }
    }
}

void solve() {
    scanf("%d %d %d", &m, &n, &st);
    for (auto x : sv) ban[x] = 0;
    sv.clear();
    for (int i = 1; i <= n; i++) {
        int x; scanf("%d", &x);
        ban[x] = 1;
        sv.push_back(x);
    }
    mx = 1;
    for (int i = 1; i <= m; i++) mx *= 10;
    mx -= 1;
    dinic.init(mx + 2, 0, mx + 2);
    for (int i = 0; i <= mx; i++) {
        if (ban[i]) continue;
        if (i == st) continue;
        addpoint(i);
    }
    int res1 = dinic.go();
    addpoint(st);
    int res2 = dinic.go();
    if (res2 == 0) printf("Bob\n");
    else printf("Alice\n");
}

int main() {
    cin >> T;
    for (int i = 0; i <= 99999; i++) {
        int x = i, sum = 0;
        while(x) {
            sum += x % 10;
            x /= 10;
        }
        if (sum % 2 == 0) even[i] = 1;
    }
    while(T--) {
        solve();
    }
    return 0;
}

一些结论题

斐波那契博弈

1堆石子有n个,两人轮流取.先取者第1次可以取任意多个,但不能全部取完.以后每次取的石子数不能超过上次取子数的2倍。取完者胜.

n为斐波那契数时先手必败,否则先手必胜

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;
const int N = 2e5 + 5;

int n;

ll fib[100];
int main() {
    fib[1] = 2;
    fib[2] = 3;
    for (int i = 3; i <= 50; i++) fib[i] = fib[i - 1] + fib[i - 2];
    while(scanf("%d", &n), n != 0) {
        int i;
        for (i = 1; i <= 50; i++) {
            if (fib[i] == n) break;
        }
        if (i == 51) printf("First win\n");
        else printf("Second win\n");
    }
    return 0;
}

威佐夫博弈

有两堆各若干个物品,两个人轮流从任一堆取至少一个或同时从两堆中取同样多的物品,规定每次至少取一个,多者不限,最后取光者得胜。

ak =[k(1+√5)/2],bk= ak + k (k=0,1,2,…,n 方括号表示取整函数)为“奇异局势”,奇异局势必败。