博弈论专题

知识基础:Nim游戏与SG函数

参考:[学习笔记] (博弈论)Nim游戏和SG函数_A_Comme_Amour的博客-CSDN博客_nim博弈

首先定义P是先手必败,N是先手必胜

有以下性质:

那么终结位置是P

可以转移到P的是N

所有操作都到达N的是P

那么根据以上三点,可以暴力求某一个局面是N还是P。步骤是先使得终结位置是P,然后枚举状态,能到P的是N。然后所有都到达N的P。然后不断迭代。

我做过的一些博弈题就是这个思路。

但是Nim游戏这么做的话时间复杂度很高。

Nim游戏的结论是异或和为0的情况下先手必败。

异或和为0和异或和不为0都可以通过一步操作互相转化。

sg函数:定义mex()表示集合中最小的为出现的非负整数。sg[x] = mex(sg[y] | y是x的后继)

那么终结位置没有后继,sg为0,其他的值可以通过定义算出。

sg = 0是先手必败,否则先手必胜。

多个子游戏的sg值是它们的异或和。

sg的计算:一堆石子,若操作石子数为1~m,那么sg[x] = x % (m + 1)

若为非零任意数,那么sg[x] = x

其他,根据定义暴力计算即可。

例题

比较裸,看作多个子游戏,每个子游戏计算sg值,看异或和即可

阶梯博弈

首先偶数层的石子是没有用的,因为一个人移动了偶数层的石子,另一个人可以模仿把移到奇数层的又移到偶数层。奇数层的操作,石子到了偶数层,相当于扔掉了。所以可以只看奇数层,奇数层的石子组成了一个Nim游戏。对于有必胜策略的那个人,对面移动奇数堆,我就按照Nim游戏移,对面移偶数堆,我就模仿它。

Great Party(Nim拓展+莫队)

在nim游戏的基础上,加了一个可以把剩余石子合并到其他堆的操作

如果是1堆,先手直接拿完,先手必胜

如果是两堆,从简单的开始。1 1,那么每次操作没得选,后手赢。

1 x,x>1  先手可以转化到1 1,先手赢

x x 这时每次操作都不能合并,合并就输了,这时后手可以模仿先手的操作,最后后手拿完,后手赢。

x y ,x!= y 先手可以转化到x x,先手赢

总结,2堆的时候,相同后手赢,不相同先手赢

3堆,发现这时先手可以转化为x x的情况,先手赢

4堆,这时一定不能合并,一合并就变成三堆,对手面对这种情况必赢

所以最后一定是1 1 1 1。那么每个数减去1,就变成了标准的nim游戏了

此时若异或和为0,后手操作完,先手面对1 1 1 1,此时后手赢。反之,异或和不为0,先手1

因此4堆的情况,减去1后,异或和为 0,后手赢,异或和不为0,先手赢。我们可以发现这个结论是包含2堆的情况的。

因此我们可以猜结论,奇数堆是先手,偶数堆,减去1后,异或和不为0是先手,否则后手。

为了简便,我们计算后手赢的情况,也就是长度为偶数且异或和为0

因为异或和不为0不好判断,异或和为0的话转化成前缀也就是前缀异或和相同,这个可以用莫队。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long LL;
const int N = 1e5 + 10;
struct query
{
    int l, r, id, bl;
}q[N];
int s[N], n, m;
LL ans[N], cur;
unordered_map<int, int> cnt[2];

bool cmp(query x, query y)
{
    if(x.bl != y.bl) return x.bl < y.bl;
    if(x.bl % 2 == 1) return x.r < y.r;
    return x.r > y.r;
}

void add(int x)
{
    int id = x % 2;
    x = s[x];
    cur += cnt[id][x];
    cnt[id][x]++;
}

void erase(int x)
{
    int id = x % 2;
    x = s[x];
    cnt[id][x]--;
    cur -= cnt[id][x];
}

int main()
{ 
    scanf("%d%d", &n, &m);
    int block = sqrt(n);

    _for(i, 1, n)
    {
        int x; scanf("%d", &x);
        x--;
        s[i] = s[i - 1] ^ x;
    }
    _for(i, 1, m)
    {
        int l, r;
        scanf("%d%d", &l, &r);
        l--;
        q[i] = {l, r, i, l / block};
    }
    sort(q + 1, q + m + 1, cmp);

    int l = 1, r = 0;
    _for(i, 1, m)
    {
        int ll = q[i].l, rr = q[i].r;
        while(l < ll) erase(l++);
        while(l > ll) add(--l);
        while(r > rr) erase(r--);
        while(r < rr) add(++r);

        LL len = rr - (ll + 1) + 1;
        ans[q[i].id] = len * (len + 1) / 2 - cur;
    }
    _for(i, 1, m) printf("%lld\n", ans[i]);
    
    return 0;
}

Z-Game on grid(dp)

这个不是标准的博弈,但是很像博弈。A可以控制自己的行为,B可以理解为随机走。

从结果逆推,当A操作时,只要有一个操作可以达到即可,B操作时,要全部都达到

有点像暴力算N状态和P状态

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 500 + 10;
int dp[N][N][3], n, m;   //0 A 1 D 2 B
char s[N][N];

int main()
{ 
    int T; scanf("%d", &T);
    while(T--)
    {
        scanf("%d%d", &n, &m);
        _for(i, 1, n) scanf("%s", s[i]);
        _for(i, 1, n + 1)
            _for(j, 1, m + 1)
                rep(k, 0, 3)
                    dp[i][j][k] = 0;
        
        for(int i = n; i >= 1; i--)
            for(int j = m; j >= 1; j--)
            {
                if(s[i][j] == 'A') dp[i][j][0] = 1;
                else if(s[i][j] == 'B') dp[i][j][2] = 1;
                else if(i == n && j == m) dp[i][j][1] = 1;
                else
                {
                    rep(k, 0, 3)
                    {
                        if((i + j) % 2 == 0) dp[i][j][k] = (dp[i + 1][j][k] || dp[i][j + 1][k]);
                        else dp[i][j][k] = ((i + 1 > n || dp[i + 1][j][k]) && (j + 1 > m || dp[i][j + 1][k]));
                    }
                }
            }
        rep(k, 0, 3) printf(dp[1][1][k] ? "yes " : "no ");
        puts("");
    }
   
    return 0;
}

Split Game(暴力计算SG函数)

这题就是暴力计算SG函数

先把边界条件,sg=0先手必败初始化后

然后遍历每一个状态,对于每一个状态枚举它的所有后继,计算后继的sg值

这道题的操作是分解成两个子游戏,这时的sg值是它们的异或。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
 
const int N = 160;
int sg[N][N], a[N << 1];
 
int check(int i, int j)
{
    return i == 1 && j == 1;
}
 
int main()
{   
    memset(sg, -1, sizeof sg);
    sg[1][2] = sg[2][1] = 0;
    sg[1][3] = sg[3][1] = 0;
    _for(i, 1, 150)
        _for(j, 1, 150)
        {
            if(i == 1 && j == 1 || !sg[i][j]) continue;
            memset(a, 0, sizeof a);
            _for(k, 1, i - 1)
            {
                if(check(k, j) || check(i - k, j)) continue;
                a[sg[k][j] ^ sg[i - k][j]] = 1;
            }
            _for(k, 1, j - 1)
            {
                if(check(i, k) || check(i, j - k)) continue;
                a[sg[i][k] ^ sg[i][j - k]] = 1;
            }
 
            rep(k, 0, N << 1)
                if(!a[k])
                {
                    sg[i][j] = k;
                    break;
                }
        }
    
    int n, m;
    while(~scanf("%d%d", &n, &m))
        puts(sg[n][m] ? "Alice" : "Bob");
 
	return 0;
}

P1290 欧几里德的游戏(SG函数)

N状态和P状态和SG函数的0和1是一致的,多个游戏的时候SG就不只是1,要算异或值。而只有一个游戏的时候可以看作只有0和1,0是先手必败

这题就算SG函数集合

对于n > m

sg(n, m) = mex{sg(n - m, m), sg(n - 2m, m)……sg(m, n % m)}

当sg(m, n% m) = 0时,显然sg(n, m) = 1

当sg(m, n % m) = 1 时,sg(n % m + m,  m) = 0,其他为0

边界情况是m=0时sg为0,这时先手必败

因此递归计算sg函数即可

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

int dfs(int n, int m)
{
    if(m == 0) return 0;
    int t = dfs(m, n % m);
    if(!t) return 1;
    else
    {
        if(n / m == 1) return 0;
        else return 1;
    }
}

int main()
{
    int T; scanf("%d", &T);
    while(T--)
    {
        int n, m; 
        scanf("%d%d", &n, &m);
        if(n < m) swap(n, m);
        puts(dfs(n, m) ? "Stan wins" : "Ollie wins");
    }
    return 0;
}

hdu 1404(打表)

直接根据P态和N态打表,P是先手必败

可以到达P态的是N态。不能N态的是P态

最优策略就是当前是P态,那么不管怎么走都是到N态,当前是N态,那么必然走P态

我开始用string, cin输入然后T了

要用字符数组。

算的时候转化成数字,注意前导零是一个坑,有前导零和没有前导零完全不同,有前导零先手必胜。

我写的打表方式是由之前的状态推现在的状态

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
 
const int N = 1e6 + 10;
int sg[N];

void cal(int x)   //1先手必胜 0先手必败
{
    for(int i = 1; i <= x; i *= 10)
    {
        int cur = x / i % 10;
        if(cur)
        {
            int t = x - cur * i;
            _for(j, 0, cur - 1)
            {
                if(i * 10 > x && j == 0) continue;
                if(!sg[t + j * i])
                {
                    sg[x] = 1;
                    return;
                }
            }
        }
        else if(!sg[x / i / 10])
        {
            sg[x] = 1;
            return;
        }
    }
    sg[x] = 0;
}
 
int main()
{
    sg[0] = 1;
    rep(i, 1, 1e6) cal(i);
    char s[10];
    while(~scanf("%s", s))
    {
        if(s[0] == '0') puts("Yes");
        else puts(sg[stoi(s)] ? "Yes" : "No");
    }   
    return 0;
}

还有一种写法是,开始是0,然后能到它的是1,然后一直往后。如果已经是1了就跳过,如果是0就继续往后拓展。也就是递推

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
 
const int N = 1e6 + 10;
int sg[N];

void deal(int x)   
{
    //第一种操作
    for(int i = 1; i <= x; i *= 10)
        if(x / i % 10 < 9)
        {
            int t = x;
            while(1)
            {
                t += i;
                sg[t] = 1;
                if(t / i % 10 == 9) break;
            }
        }


    // 第二种操作
    x *= 10;
    int t = 1;
    while(1)
    {
        rep(i, 0, t)
        {
            if(x * t + i >= 1e6) return;
            sg[x * t + i] = 1;
        }
        t *= 10;
    }
}
 
int main()
{
    sg[0] = 1;
    rep(i, 1, 1e6)
        if(!sg[i])
            deal(i);

    char s[10];
    while(~scanf("%s", s))
    {
        if(s[0] == '0') puts("Yes");
        else puts(sg[stoi(s)] ? "Yes" : "No");
    }   

    return 0;
}

hdu 1079(打表)

和上一题类似,打表即可

用记忆化搜索

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

unordered_map<int, int> sg;
int m[15];

int check(int year)
{
    return year % 4 == 0 && year % 100 != 0 || year % 400 == 0;
}

struct node
{
    int year, month, day;
    int get()
    {
        return year * 1e4 + month * 1e2 + day;
    }
};

int m_max(int year, int month)
{
    if(check(year) && month == 2) return 29;
    return m[month];
}

node add1(int year, int month, int day)
{
    day++;
    if(day > m_max(year, month))
    {
        day = 1;
        month++;
        if(month == 13)
        {
            month = 1;
            year++;
        }
    }
    return {year, month, day};
}

node add2(int year, int month, int day)
{
    int next = month + 1;
    if(next == 13) next = 1;
    if(day <= m_max(year, next))
    {
        month++;
        if(month == 13)
        {
            month = 1;
            year++;
        }
        return {year, month, day};
    }
    return {-1, month, day};
}

bool pd(node t)
{
    return t.get() <= node{2001, 11, 4}.get();
}

int dfs(node cur)
{
    if(sg[cur.get()]) return sg[cur.get()];
    
    node t = add1(cur.year, cur.month, cur.day);
    if(pd(t) && dfs(t) == -1) return sg[cur.get()] = 1;

    t = add2(cur.year, cur.month, cur.day);
    if(t.year != -1 && pd(t) && dfs(t) == -1) return sg[cur.get()] = 1;

    return sg[cur.get()] = -1;
}

void init()
{
    _for(i, 1, 12) m[i] = 31;
    m[4] = m[6] = m[9] = m[11] = 30;
    m[2] = 28;
    sg[node{2001, 11, 4}.get()] = -1;
}

int main()
{
    init();

    int T; scanf("%d", &T);
    while(T--)
    {
        int year, month, day;
        scanf("%d%d%d", &year, &month, &day);
        puts(dfs({year, month, day}) == 1 ? "YES" : "NO");
    }

    return 0;
}

小牛再战(打表+猜结论)

三个步骤

1.打表:手算或程度

2.从表中猜结论,找规律

3.证明结论

这题其实程序打表很复杂,因为状态转移非常多,所以手算

n = 0 P

n = 1 N

n = 2  相等时,先手一定破坏相等,后手一定可以操作到相等。最后是后手拿完,P

不相等时,先手可以操作到相等,N

n = 3   设x1 >= x2 >= x3  显然可以操作x1使得有x2 x2  即操作到P态, 那么当前是N态

n = 4 这时就比较困难了,根据前面的状态猜结论

n为奇数N态,n为偶数,两两相等则P否则N。也可以猜n为偶数全部相等则P,但可以举出反例,入2 2 1 1 

证明用P态和N态的性质证明

1.首先终止状态全0满足两两相等,即P态。

2.其次证明P态一定走到N态

对于两两相等的,不管怎么操作一定不会再两两相等。

首先不能放石子给其他堆,否则破坏其他堆的配对。不能放时,一定破坏当前的配对

3.最后证明N态可以走到P态

当N为奇数时,即x1 >= x2 ……xn

我们假设可以配对成x2 x2 x4 x4……xn xn

需要的石子数为x2 - x3 + x4 - x5 ……xn-1 - xn

写为 x2 +(x4 - x3) + ……(xn-1 - xn-2) - xn

括号里都是小于等于0的,最后是小于0的,所以整个式子是小于x2的

因此x1是大于上式的,所以一定可以提供这么多。

当N为偶数时,x1 x2……xn

那么假设x1拿走一部分,使得为

xn x2 x2 x4 x4……xn

那么即证明x1 - xn > x2 -x3 + x4 - x5 ……xn-2  -  xn-1

即x1 > x2 +(x4 - x3) + ……(xn-1 - xn)

括号里都是小于等于,只要有一个是小于,那就成立

不成立的情况只有两两配对的情况,这个情况不是N态。

因此得证。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 15;
int a[N], n;

int main()
{
    while(scanf("%d", &n) && n)
    {
        _for(i, 1, n) scanf("%d", &a[i]);
        if(n % 2 == 1) puts("Win");
        else
        {
            sort(a + 1, a + n + 1);
            int flag = 0;
            for(int i = 1; i <= n; i += 2)
                if(a[i] != a[i + 1])
                {
                    flag = 1;
                    break;
                }
            puts(flag ? "Win" : "Lose");
        }
    }

    return 0;
}

cf 1537D(打表+猜结论)

打个表,发现0和1交替,但有一些反例

于是就猜偶数是1,奇数是0

打出反例发现是2 8 32……

特判一下反例即可。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
map<ll, int> mp;

int main()
{
    ll cur = 2;
    while(1)
    {
        mp[cur] = 1;
        cur *= 4;
        if(cur > 1e9) break;
    }    

    int T; scanf("%d", &T);
    while(T--)
    {
        int x; scanf("%d", &x);
        int cur = (x % 2 == 0) ? 1 : 0;
        if(mp[x]) cur ^= 1;
        puts(cur ? "Alice" : "Bob");
    }

    return 0;
}

反常Nim游戏

nim游戏结论是异或和为0是P态,否则N态

而反常nim游戏就是终态是反的,即拿了最后一个石子的人输

这个时候分两种情况,全1的话,奇数是P态,偶数是N态。不全为1的话,和nim游戏一样

hdu 1730(Nim游戏或SG函数)

这是一个不平等游戏,即两个人可操作的集合是不同的。这题黑子和白子显然可操作的是不同的

这道题有两种理解方式

Nim游戏的话,用每一行中间的差值看作一堆石子即可

证明:

1.当全部为0,即全部挨在一起时,是P态,因为后手可以一直移动到挨着。

2.P态一定走到N态:一个异或值为0的序列,改变其中一个数,不管变大或者变小,一定改变后异或值不为0

3.N态可以走到P态。一定可以通过减少某个xi达到异或值为0(证明Nim游戏时已证)

因此就可以看作Nim游戏。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

int main()
{
    int n, m;
    while(~scanf("%d%d", &n, &m))
    {
        int cur = 0;
        _for(i, 1, n)
        {
            int x, y;
            scanf("%d%d", &x, &y);
            cur ^= abs(x - y) - 1;
        }
        puts(cur ? "I WIN!" : "BAD LUCK!");
    }

    return 0;
}

栗酱的异或和(Nim游戏扩展)

Nim游戏基础上加了一个限制,即从第k堆开始取

其实就看从第k堆开始取能否使异或和为0即可

此时要求a[k] > x ^ a[k]

x是异或和

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e5 + 10;
int a[N], n, k;

int main()
{
    int T; scanf("%d", &T);
    while(T--)
    {
        scanf("%d%d", &n, &k);
        int x = 0;
        _for(i, 1, n)
        {
            scanf("%d", &a[i]);
            x ^= a[i];
        }
        puts((x && a[k] > (x ^ a[k])) ? "Yes" : "No");  //这里注意要加一些括号 因为这个运算优先级WA了一发
    }

    return 0;
}

Georgia and Bob(阶梯Nim)

阶梯博弈算法详解(尼姆博弈进阶)_我爱AI_AI爱我的博客-CSDN博客_尼姆博弈 算法

阶梯nim可以看作很多个石子堆,每次操作是将第i堆的石子移动到第i-1堆,第1堆可以任意减少。不能操作的人输

这时把奇数堆提取出来,然后看作nim游戏即可。因为先手可以按照nim操作,后手如果移动偶数堆的,先手模仿它即可,不影响结果,因此可以排除掉偶数堆的影响。

对于这题而言,把每个棋子可以移动的空格算出,发现一移动,相邻的两个一加一减,相当于把石子移动到相邻一堆,于是就刚好符合阶梯Nim的规则

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e3 + 10;
int a[N], n;

int main()
{
    int T; scanf("%d", &T);
    while(T--)
    {
        scanf("%d", &n);
        vector<int> ve;
        _for(i, 1, n) scanf("%d", &a[i]);
        sort(a + 1, a + n + 1);
        for(int i = n; i >= 1; i--)
            ve.push_back(a[i] - a[i - 1] - 1);
        
        int x = 0;
        for(int i = 0; i < ve.size(); i += 2) x ^= ve[i];
        puts(!x ? "Bob will win" : "Georgia will win");
    }

    return 0;
}

Rake It In(minmax搜索)

学校课程学过这个方法,搜索即可

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

struct node{ int a[5][5]; };

node make(node x, int i, int j)
{
    int t = x.a[i][j];
    x.a[i][j] = x.a[i][j + 1];
    x.a[i][j + 1] = x.a[i + 1][j + 1];
    x.a[i + 1][j + 1] = x.a[i + 1][j];
    x.a[i + 1][j] = t;
    return x;
}

int dfs_max(node x, int k);

int dfs_min(node x, int k)
{
    int res = 1e9;
    _for(i, 1, 3)
        _for(j, 1, 3)
        {
            int get = x.a[i][j] + x.a[i + 1][j] + x.a[i][j + 1] + x.a[i + 1][j + 1];
            res = min(res, get + dfs_max(make(x, i, j), k - 1));
        }
    return res;
}

int dfs_max(node x, int k)
{
    if(!k) return 0;
    int res = 0;
    _for(i, 1, 3)
        _for(j, 1, 3)
        {
            int get = x.a[i][j] + x.a[i + 1][j] + x.a[i][j + 1] + x.a[i + 1][j + 1];
            res = max(res, get + dfs_min(make(x, i, j), k));
        }
    return res;
}

int main()
{
    int T; scanf("%d", &T);
    while(T--)
    {
        int k; scanf("%d", &k);
        node x;
        _for(i, 1, 4)
            _for(j, 1, 4)
                scanf("%d", &x.a[i][j]);
        printf("%d\n", dfs_max(x, k));
    }

    return 0;
}

Palindrome Game (hard version)(dp)

这题类似博弈,但不是博弈。用dp解决。当前状态可以由00 01 中间是否有0 上一次是否翻转来表示,一个状态的值为自己的代价减去对方的代价。

然后分类讨论dp即可

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e3 + 10;
int dp[N][N][2][2], a[N], n;

int main()
{
    memset(dp, 0x3f, sizeof dp);
    dp[0][0][0][0] = dp[0][0][0][1] = 0;
    _for(i, 0, 1e3)
        _for(j, 0, 1e3)
            _for(p, 0, 1)       
                for(int r = 1; r >= 0; r--)   //这里注意顺序 因为0是从1转移过来的
                {
                    if(i > 0) dp[i][j][p][r] = min(dp[i][j][p][r], 1 - dp[i - 1][j + 1][p][0]);
                    if(j > 0) dp[i][j][p][r] = min(dp[i][j][p][r], 1 - dp[i][j - 1][p][0]);
                    if(p == 1) dp[i][j][p][r] = min(dp[i][j][p][r], 1 - dp[i][j][0][0]);
                    if(r == 0 && j > 0) dp[i][j][p][r] = min(dp[i][j][p][r], -dp[i][j][p][1]);   //记住不为回文串才可以反转
                }
    
    int T; scanf("%d", &T);
    while(T--)
    {
        scanf("%d", &n);
        _for(i, 1, n) scanf("%1d", &a[i]);

        int i = 0, j = 0, p = 0, r = 0;
        _for(l, 1, n)
        {
            int r = n - l + 1;
            if(l >= r) break;
            if(a[l] + a[r] == 0) i++;
            if(a[l] + a[r] == 1) j++;
        }
        if(n % 2 == 1 && a[(n + 1) / 2] == 0) p = 1;
        if(dp[i][j][p][r] < 0) puts("Alice");
        else if(dp[i][j][p][r] > 0) puts("BOB");
        else puts("DRAW");
    }
    
    return 0;
}

A Chess Game(SG函数+拓扑排序)

SG函数裸题,拓扑排序使得求SG函数的顺序正确

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e3 + 10;
vector<int> g[N], topo;
unordered_map<int, bool> vis;
int d[N], sg[N], n, m;

int main()
{
    scanf("%d", &n);
    rep(u, 0, n)
    {
        int k; scanf("%d", &k);
        while(k--)
        {
            int v; scanf("%d", &v);
            g[u].push_back(v);
            d[v]++;
        }
    }

    queue<int> q;
    rep(i, 0, n)
        if(!d[i])
            q.push(i);
    while(!q.empty())
    {
        int u = q.front(); q.pop();
        topo.push_back(u);
        for(int v: g[u])
            if(--d[v] == 0)
                q.push(v);
    }
    
    for(int i = n - 1; i >= 0; i--)
    {
        vis.clear();
        int u = topo[i];
        for(int v: g[u]) vis[sg[v]] = 1;
        rep(j, 0, n)
            if(!vis[j])
            {
                sg[u] = j;
                break;
            }
    }

    while(scanf("%d", &m) && m)
    {
        int cur = 0;
        _for(i, 1, m)
        {
            int x; scanf("%d", &x);
            cur ^= sg[x];
        }
        puts(cur ? "WIN" : "LOSE");
    }
    
    return 0;
}

S-Nim(SG函数)

暴力计算每个子游戏SG函数,异或起来即可

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e4 + 10;
int sg[N], s[N], n, k, m;
unordered_map<int, bool> vis;

int main()
{
    scanf("%d", &k);
    _for(i, 1, k) scanf("%d", &s[i]);

    _for(i, 0, 1e4)
    {
        vis.clear();
        _for(j, 1, k)
            if(i - s[j] >= 0)
                vis[sg[i - s[j]]] = 1;
        rep(j, 0, 1e4)
            if(!vis[j])
            {
                sg[i] = j;
                break;
            }
    }

    scanf("%d", &m);
    while(m--)
    {
        int l, x, cur = 0; 
        scanf("%d", &l);
        _for(i, 1, l)
        {
            scanf("%d", &x);
            cur ^= sg[x];
        }
        printf(cur ? "W" : "L");
    }
    
    return 0;
}

Stone Game(SG函数+找规律)

先手算SG函数,注意计算SG函数时从游戏结束开始倒推,这道题游戏结束都是箱子满了的时候,从这里开始反过来推一下。

数据范围不允许暴力,那就找一下规律即可

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

int find(int x)
{
    _for(t, 0, 1e4)
        if(t + t * t < x && (t + 1) + (t + 1) * (t + 1) >= x)
            return t;
    return 0;
}

int main()
{
    int n, cur = 0; 
    scanf("%d", &n);
    _for(i, 1, n)
    {
        int s, c; 
        scanf("%d%d", &s, &c);
        if(c == 0) continue;
        vector<int> ve;
        while(s)
        {
            ve.push_back(s);
            s = find(s);
        }
        sort(ve.begin(), ve.end());
        rep(i, 0, (int)ve.size())
            if(ve[i] <= c && c <= ve[i + 1])
            {
                if(ve[i] == c) cur ^= 0;
                else cur ^= ve[i + 1] - c;
                break;
            }
    }
    puts(cur ? "Yes" : "No");
    
    return 0;
}

Be the Winner(反常Nim)

这题不太一样,最后取的人输,注意这样不能用SG函数,SG函数默认是正常游戏

这是经典的反常Nim,只要全为1特判一下,其他的和Nim游戏一样。

注意这题还可以把石子堆拿掉一些,然后把剩余的分两堆。可以分析一下,对于全1的不能分开,不为全1的,从异或值不为0到为0,不需要这个操作,从异或值为0到异或值不为0,有了这个操作也没用。所以这个操作对结果没用影响。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

int main()
{
    int n, cur = 0, flag = 1; 
    scanf("%d", &n);
    _for(i, 1, n)
    {
        int x; scanf("%d", &x);
        if(x > 1) flag = 0;
        cur ^= x;
    }

    if(flag) puts((n % 2 == 0) ? "Yes" : "No");
    else puts(cur ? "Yes" : "No");
    
    return 0;
}

[HNOI2007]分裂游戏(隐藏nim)

有很多隐藏的nim游戏

1.1个1xn的棋盘,有很多棋子,每个人可以把棋子往左移动任意步数,一个格子可以有多个棋子

实际上,每个棋子可以走的步数看作一个石子堆,就成了nim游戏

2.有n堆石子,一个人可以把左边堆的一个石子放到右边的堆中。把这个石子堆看作棋盘,发现就和上面的一样了。只不过算sg值的时候,相同的会异或完,所以一个格子的棋子数是奇数才对最终的sg值起作用。另一个角度,后手可以模仿前手的操作,石子两两抵消。

3.也就是这道题,上面两个懂了,这个就很容易了,也就是看作棋盘。其实就是将1个石子堆分裂成两个大小小于它的石子堆,暴力sg函数求即可。

第一次操作需要将异或和不为0变成为0,此时注意不考虑棋子数的奇偶,只要满足这个操作即可,棋子的奇偶是对于最后的sg值来说的。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 30;
int sg[N], p[N], n, ans1, ans2, ans3;
unordered_map<int, bool> vis;

int main()
{
    _for(x, 1, 25)
    {
        vis.clear();
        _for(i, 0, x - 1)
            _for(j, 0, x - 1)
                vis[sg[i] ^ sg[j]] = 1;
        int t = 0;
        while(vis[t]) t++;
        sg[x] = t;
    }

    int T; scanf("%d", &T);
    while(T--)
    {
        scanf("%d", &n);
        _for(i, 1, n) scanf("%d", &p[i]), p[i] %= 2;

        int x = 0;
        _for(i, 1, n)
            if(p[i])
                x ^= sg[n - i];
        if(!x)
        {
            puts("-1 -1 -1\n0");
            continue;
        }

        int cnt = 0, fi = 1;
        _for(i, 1, n)
            _for(j, i + 1, n)
                _for(k, j, n)
                    if((sg[n - i] ^ sg[n - j] ^ sg[n - k]) == x)
                    {
                        cnt++;
                        if(fi)
                        {
                            fi = 0;
                            ans1 = i, ans2 = j, ans3 = k;
                        }
                    }
        printf("%d %d %d\n%d\n", ans1 - 1, ans2 - 1, ans3 - 1, cnt);
    }
    
    return 0;
}

A tree game(Hackenbush模板题)

一颗树,在上面删边,删完后它的子树也全没了,谁不能操作谁输。

结论是多一条边sg值+1,两条链的sg值看作两个游戏,异或起来

用sg[u]表示u为跟的子树的sg值即可

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e5 + 10;
vector<int> g[N];
int sg[N], n;

void dfs(int u, int fa)
{
    sg[u] = 0;
    for(int v: g[u])
    {
        if(v == fa) continue;
        dfs(v, u);
        sg[u] ^= sg[v] + 1;
    }
}

int main()
{
    int T; scanf("%d", &T);
    while(T--)
    {
        scanf("%d", &n);
        _for(i, 1, n) g[i].clear();
        _for(i, 1, n - 1)
        {
            int u, v;
            scanf("%d%d", &u, &v);
            g[u].push_back(v);
            g[v].push_back(u);
        }
        dfs(1, 0);
        puts(sg[1] ? "Alice" : "Bob");
    }
   
    return 0;
}

Christmas Game(树上博弈+阶梯nim+换根dp)

这题好秀啊

自己想的时候,先思考一条链的情况,发现就是阶梯nim,但是不知道怎么转化到树上

我是以合并的思路来考虑的,实际上并不能看作多个子游戏

实际上不是合并,而是直接思考,其实阶梯nim的时候,奇数堆就是一堆石子,那么树上也一样,奇数深度是石子,把它们全部异或起来即可。深度定义为除以k向下取整

那么接下来怎么换根dp呢

因为又要除以k,又要奇数,所以换一种等价的更容易考虑的方式,即模2k为k~2k-1的,这个用换根比较好维护。

于是先预处理dp[u][j]表示以u为根的子树,距离u模2k为j的节点的异或和,这个用一个dp维护一下即可。

然后在换根dp的时候,要用到dp[u]和dp[v],dp[u]要先减去v部分的贡献,然后再把u加到v上,使得dp[v]变成以v为根节点。然后注意恢复现场

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e5 + 10;
int dp[N][45], sg[N], a[N], n, k;
vector<int> g[N];

void dfs(int u, int fa)
{
    dp[u][0] = a[u];
    for(int v: g[u])
    {
        if(v == fa) continue;
        dfs(v, u);
        rep(j, 0, k) dp[u][(j + 1) % k] ^= dp[v][j];
    }
}

void dfs2(int u, int fa)
{
    _for(i, k / 2, k - 1) sg[u] ^= dp[u][i];
    for(int v: g[u])
    {
        if(v == fa) continue;
        rep(j, 0, k) dp[u][(j + 1) % k] ^= dp[v][j];
        rep(j, 0, k) dp[v][(j + 1) % k] ^= dp[u][j];
        dfs2(v, u);
        rep(j, 0, k) dp[v][(j + 1) % k] ^= dp[u][j];
        rep(j, 0, k) dp[u][(j + 1) % k] ^= dp[v][j];
    }
}

int main()
{
    scanf("%d%d", &n, &k);
    k *= 2;
    _for(i, 1, n - 1)
    {
        int u, v;
        scanf("%d%d", &u, &v);
        g[u].push_back(v);
        g[v].push_back(u);
    }
    _for(i, 1, n) scanf("%d", &a[i]);

    dfs(1, 0);
    dfs2(1, 0);
    _for(i, 1, n) printf("%d ", sg[i] > 0);

    return 0;
}

二分图博弈

在一个二分图上,从一个起点开始,每次可以沿着边走到另外一个点,走过的点不能再走,不能移动的人输。

结论是如果起点一定在最大匹配边上,则先手必胜,否则先手必败。

可以去掉起点跑最大流,然后加入起点在残量网络上再跑,如果最大流增加则一定在最大匹配边上。

斐波那契Nim

有一堆n个石子,先手可以取1~1-n,之后每个人可1到取前一个人取石子的两倍

可以手算,发现为斐波那契数的时候的P态。

此外,有一个结论,任意一个数都可以分解为一组斐波那契数,这些数在斐波那契数列上不相邻,且分解方式唯一

下棋(SG函数+线段树)

首先计算sg值,根据题目的特点,后继非常多,所以我们用线段树来优化这个过程,建立一个权值线段树。注意sg值会为0所以左边界是0,线段树左端点为0是可以的,树状数组最小值不能为0,要为1。在权值线段树可以找到第一个为0的地方,也就是mex。

对于询问,可以理解为一个新点,向图上连了很多边,于是就要计算新的sg值。所以就是区间mex的问题,可以用主席树高效解决。

#include <bits/stdc++.h>
#define l(k) (k << 1)
#define r(k) (k << 1 | 1)
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e5 + 10;
const int mx = 1e5 + 5;
vector<int> g[N], topo;
int t[N << 2], sg[N], n, m, k;

void up(int k)
{
    t[k] = (t[l(k)] > 0) && (t[r(k)] > 0);
}

void add(int k, int l, int r, int x, int p)
{
    if(l == r)
    {
        t[k] += p;
        return;
    }
    int m = l + r >> 1;
    if(x <= m) add(l(k), l, m, x, p);
    else add(r(k), m + 1, r, x, p);
    up(k);
}

int find(int k, int l, int r)
{
    if(l == r) return l;
    int m = l + r >> 1;
    if(!t[l(k)]) return find(l(k), l, m);
    return find(r(k), m + 1, r);
}

//主席树
int s[N << 5], root[N << 5], ls[N << 5], rs[N << 5], cnt;

void build(int& k, int pre, int l, int r, int x)
{
    k = ++cnt;
    ls[k] = ls[pre]; rs[k] = rs[pre]; s[k] = s[pre] + 1;
    if(l == r) return;
    int m = l + r >> 1;
    if(x <= m) build(ls[k], ls[pre], l, m, x);
    else build(rs[k], rs[pre], m + 1, r, x);
}

int ask(int k, int pre, int l, int r)
{
    if(l == r) return l;
    int m = l + r >> 1, x = s[ls[k]] - s[ls[pre]];
    if((m - l + 1) > x) return ask(ls[k], ls[pre], l, m);
    return ask(rs[k], rs[pre], m + 1, r);
}

int main()
{
    scanf("%d%d%d", &n, &m, &k);
    while(m--)
    {
        int u, v;
        scanf("%d%d", &u, &v);
        g[u].push_back(v);
    }

    _for(u, 1, n)
    {
        for(int v: g[u]) add(1, 0, mx, sg[v], -1);
        sg[u] = find(1, 0, mx);
        add(1, 0, mx, sg[u], 1);
        for(int v: g[u]) add(1, 0, mx, sg[v], 1);
    }

    int ans = 0;
    _for(i, 1, n) build(root[i], root[i - 1], 0, mx, sg[i]);
    while(k--)
    {
        int l, r; scanf("%d%d", &l, &r);
        ans ^= ask(root[r], root[l - 1], 0, mx);
    }
    puts(ans ? "Alice" : "Bob");

    return 0;
}

A New Tetris Game(SG函数+dfs)

做法非常明显了,暴力dfs竟然能过……不过其实数据范围也只有几十

注意要记忆化一下,一个棋盘可以拉成一行变成stirng,这样就可以用map来实现记忆化。

寻找是否存在时不用0,用find函数。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

int n, m, num, ans;
unordered_map<string, int> mp;

int id(int i, int j)
{
    return i * m + j;
}

int dfs(string s)
{
    if(mp.find(s) != mp.end()) return mp[s];
    unordered_map<int, bool> vis;
    rep(i, 0, n - 1)
        rep(j, 0, m - 1)
        {
            if(s[id(i, j)] == '1' || s[id(i + 1, j)] == '1'  || s[id(i, j + 1)] == '1' || s[id(i + 1, j + 1)] == '1') continue;
            string t = s;
            t[id(i, j)] = t[id(i + 1, j)] = t[id(i, j + 1)] = t[id(i + 1, j + 1)] = '1';
            vis[dfs(t)] = 1;
        }

    int res = 0;
    while(vis[res]) res++;
    return mp[s] = res;
}

int main()
{
    scanf("%d", &num);
    while(num--)
    {
        string s = "", x;
        scanf("%d%d", &n, &m);
        _for(i, 1, n)
        {
            cin >> x;
            s += x;
        }
        ans ^= dfs(s);
    }
    puts(ans ? "Yes" : "No");

    return 0;
}

P8369 [POI2000]条纹(暴力计算SG函数)

SG函数很好用。简单题直接暴力计算,有些题需要一些数据结构来优化计算

这题直接暴力

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e3 + 10;
unordered_map<int, bool> vis;
int sg[N];
 
int main()
{
    int a[5];
    _for(i, 1, 3) scanf("%d", &a[i]);

    _for(p, 1, 1000)
    {
        vis.clear();
        _for(id, 1, 3)
        {
            int k = a[id];
            _for(i, 0, p - k) vis[sg[i] ^ sg[p - k - i]] = 1;
        }
        int t = 0;
        while(vis[t]) t++;
        sg[p] = t;
     }

     int q; scanf("%d", &q);
     while(q--)
     {
         int x; scanf("%d", &x);
       //  puts("!@#");
         puts(sg[x] ? "1" : "2");
     }

    return 0;
}

巧合力棒(Nim拓展)

就是在Nim基础上,多了一个选择石子堆的操作

其实从样例可以反推,如果存在异或和为0且剩余石子堆不存在异或和为0的子集则先手必胜

因为先手取出来后,异或和变为0,后手无论是操作还是取出新的石子堆,异或和肯定不是0,然后先手又使其异或和为0,后手同样使得异或和不为0.

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 20;
int a[N], n;
 
int main()
{
    _for(_, 1, 10)
    {
        scanf("%d", &n);
        rep(i, 0, n) scanf("%d", &a[i]);

        int ans = 1;
        rep(S, 1, 1 << n)
        {
            int cur = 0;
            rep(j, 0, n)
                if(S & (1 << j))
                    cur ^= a[j];
            if(!cur) ans = 0;
        }
        puts(ans ? "YES" : "NO");
    }

    return 0;
}

取石子(优化状态+记忆化搜索)

这题在不考虑1的情况下很容易

不考虑1的话,操作次数是固定的,合并n-1次,减少为石子总数,加起来就是操作总数,看操作总数奇偶即可。

但是有1的话,取了同时使得合并次数-1,就很难搞。

所以我们分开来看,一堆是非1的,一堆是1的

而对于非1的,可以直接用操作总数来代表它们所有

所以这样就可以设计出一个状态,sg[a][b]表示有a堆1的,非1堆有b的操作次数。

那么就用记忆化搜索计算sg函数即可。

有必胜策略的那一方在操作时会保证在操作非1堆时,让另一方没有方法拿掉一个1的石子堆。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e3 + 10;
int sg[N][N * 50], n;

int dfs(int a, int b)
{
    if(sg[a][b] != -1) return sg[a][b];
    if(b == 1) return dfs(a + 1, 0);
    if(a == 0) return sg[a][b] = b % 2;

    if(b && !dfs(a, b - 1)) return sg[a][b] = 1;
    if(!dfs(a - 1, b)) return sg[a][b] = 1;
    if(b && !dfs(a - 1, b + 1)) return sg[a][b] = 1;
    if(a > 1 && !dfs(a - 2, b + 2 + (b > 0))) return sg[a][b] = 1;
    return sg[a][b] = 0;
}
 
int main()
{
    memset(sg, -1, sizeof sg);
    int T; scanf("%d", &T);
    while(T--)
    {
        scanf("%d", &n);
        int a = 0, b = 0;
        _for(i, 1, n)
        {
            int x; scanf("%d", &x);
            if(x == 1) a++;
            else b += x + 1;
        }
        if(b) b--;
        puts(dfs(a, b) ? "YES" : "NO");
    }
    return 0;
}

[CQOI2013] 新Nim游戏(nim+线性基)

这道题nim只是提供一个媒介,主要是线性基

等价于删除石子总数最小的堆,使得剩下的堆不存在子集的异或和为0

考虑如何求一个集合,它的子集异或和不为0。

注意异或和为0意味着可以分割成两个集合异或和相等。

因此用线性基实现,遍历一遍,如果当前数可以由前面的数表示,那么就可以异或和为0,所以此时这个数不能加入。

为了使得这个集合的和最大,先从大到小排序,然后这样子加入即可。

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int M = 40;
const int N = 110;
int a[N], d[M], n;

void add(int x)
{
    for(int i = 30; i >= 0; i--)
        if(x & (1 << i))              
        {
            if(d[i]) x ^= d[i];
            else { d[i] = x; return; }   
        }                   
}
 
int check(int x)                        
{
    for(int i = 30; i >= 0; i--)
        if(x & (1 << i))
        {
            if(d[i]) x ^= d[i];
            else return false;
        }
    return true;
}
 
int main()
{
    scanf("%d", &n);
    _for(i, 1, n) scanf("%d", &a[i]);
    sort(a + 1, a + n + 1, greater<int>());

    ll ans = 0;
    _for(i, 1, n)
    {
        if(check(a[i])) ans += a[i];
        else add(a[i]);
    }
    printf("%lld\n", ans);
 
	return 0;
}

焦糖布丁(树上阶梯Nim+线性基)

这道题简直是前面两道题拼凑起来,秒切

首先在这个树上,石子数往上移,就是阶梯nim,根节点深度为0,那么只有奇数深度的石子是有用的,把它们全部异或起来即可。

因此可以发现只要存在子集异或和为0即可,那么这个可以由线性基来做。

注意开long long

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 70;
ll d[N];
int n;

void add(ll x)
{
    for(int i = 62; i >= 0; i--)
        if(x & (1LL << i))              
        {
            if(d[i]) x ^= d[i];
            else { d[i] = x; return; }   
        }                   
}
 
int check(ll x)                        
{
    for(int i = 62; i >= 0; i--)
        if(x & (1LL << i))
        {
            if(d[i]) x ^= d[i];
            else return false;
        }
    return true;
}
 
int main()
{
    int T; scanf("%d", &T);
    while(T--)
    {
        int ans = 0;
        memset(d, 0, sizeof d);
        scanf("%d", &n);
        _for(i, 1, n)
        {
            ll x; scanf("%lld", &x);
            if(check(x)) ans = 1;
            else add(x);
        }
        puts(ans ? "Yes" : "No");
    }
	return 0;
}

树链博弈(模仿策略)

这题的核心是模仿策略。

先手将哪些层的祖先翻转了,后手就翻哪些。

这样子的话,当前层的黑色节点数少二,其他层的黑色节点数不变。

那么如果每一层黑色节点数都是偶数的话,后手赢,P态。

如果不是,先手可以一步操作达到这个P态,即N态。

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e3 + 10;
vector<int> g[N];
int a[N], d[N], n;

void dfs(int u, int fa, int dep)
{
    if(a[u]) d[dep]++;
    for(int v: g[u])
    {
        if(v == fa) continue;
        dfs(v, u, dep + 1);
    }
}
 
int main()
{
    scanf("%d", &n);
    _for(i, 1, n) scanf("%d", &a[i]);
    _for(i, 1, n - 1)
    {
        int u, v;
        scanf("%d%d", &u, &v);
        g[u].push_back(v);
        g[v].push_back(u);
    }
    dfs(1, 0, 1);

    int flag = 1;
    _for(i, 1, n)
        if(d[i] % 2 == 1)
        {
            flag = 0;
            break;
        }
    puts(flag ? "Second" : "First");
    
	return 0;
}

AT2307 [AGC010F] Tree Game(分析方法)

首先数据范围暗示是n方的算法,由于要判断每个点,所以可以想到以每个点为根

然后从最简单的情况分析必胜策略,一点点拓展情况,得到一个综合的策略,适合于所有情况的策略。关键的是从最简单的开始分析起,找规律猜结论

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 3e3 + 10;
vector<int> g[N];
int sg[N], a[N], n;

void dfs(int u, int fa)
{
    sg[u] = 0;
    for(int v: g[u])
    {
        if(v == fa) continue;
        dfs(v, u);
        if(!sg[v] && a[u] > a[v]) sg[u] = 1;
    }
}

int main()
{
    scanf("%d", &n);
    _for(i, 1, n) scanf("%d", &a[i]);
    _for(i, 1, n - 1)
    {
        int u, v;
        scanf("%d%d", &u, &v);
        g[u].push_back(v);
        g[v].push_back(u);
    }
    _for(i, 1, n)
    {
        dfs(i, 0);
        if(sg[i]) printf("%d ", i);
    }

    return 0;
}

游戏(拓展Nim+第一步方案)

算了一下暴力sg不会T,因此就暴力算sg

在第一步方案上,注意对于sg值,比其小的的sg值都可以达到,大于它的也有部分达到,所以要枚举所有情况。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e5 + 10;
unordered_map<int, int> vis;
int sg[N], a[N], n;

void init()
{
    _for(i, 1, 1e5)
    {
        vis.clear();
        for(int j = 1; j * j <= i; j++)
            if(i % j == 0)
                vis[sg[i - j]] = vis[sg[i - i / j]] = 1;
        int t = 0;
        while(vis[t]) t++;
        sg[i] = t;
    }
}

int main()
{
    init();

    int x = 0;
    scanf("%d", &n);
    _for(i, 1, n) scanf("%d", &a[i]), x ^= sg[a[i]];
    if(!x) 
    {
        puts("0");
        return 0;
    }

    int cnt = 0;
    _for(i, 1, n)
        for(int j = 1; j * j <= a[i]; j++)
            if(a[i] % j == 0)
            {
                if(sg[a[i] - j] == (x ^ sg[a[i]])) cnt++;
                if(j * j != a[i] && sg[a[i] - a[i] / j] == (x ^ sg[a[i]])) cnt++;
            }
    printf("%d\n", cnt);

    return 0;
}

[SDOI2011]黑白棋(k-nim + dp)

这题是k-nim,也就是说一次可以操作1~k堆石子

结论是将每个数变成二进制表示,ri为第i位二进制1的个数模(k+1)

如果所有所有ri都为0,那么先手必败。

知道这个后,用dp求方案数,这里结合了组合数

因为为0好计算,所以算为0的方案数,最后用总数减去即可

dp[i][j]表示1~i位都是r为0,当前有j个石子的方案数。那么枚举第i+1位的1有x*(d+1)个即可

最后统计答案的适合还要枚举堆的位置,n减去石子数减去终点,在剩下的位置里面选k/2个起点。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 1e4 + 10;
const int mod = 1e9 + 7;
ll dp[20][N], C[N][110];

int main()
{
    int n, k, d;
    scanf("%d%d%d", &n, &k, &d);

    _for(i, 0, 1e4) C[i][0] = 1;
    _for(i, 1, 1e4)
        _for(j, 1, 100)
            C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;

    dp[0][0] = 1;
    _for(i, 0, 13)
        _for(j, 0, n - k)
            for(int x = 0; j + x * (d + 1) * (1 << i) <= n - k && x * (d + 1) <= k / 2; x++)
                dp[i + 1][j + x * (d + 1) * (1 << i)] =  (dp[i + 1][j + x * (d + 1) * (1 << i)] + dp[i][j] * C[k / 2][x * (d + 1)] % mod) % mod;
    
    ll ans = 0;
    _for(j, 0, n - k) ans = (ans + dp[14][j] * C[n - k / 2 - j][k / 2] % mod) % mod;
    printf("%lld\n", (C[n][k] - ans + mod) % mod);

    return 0;
}

筱玛爱游戏(线性基+博弈)

选一些集合不存在一些数异或和为0,这就是线性基的基

线性基的个数是一定的,所以操作数是一定的,那么就看操作数的奇偶即可。线性基注意long long

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
 
typedef long long ll;
const int N = 70;
ll d[N];
int n;
 
void add(ll x)
{
    for(int i = 62; i >= 0; i--)
        if(x & (1LL << i))              
        {
            if(d[i]) x ^= d[i];
            else { d[i] = x; return; }   
        }                   
}
 
int check(ll x)                        
{
    for(int i = 62; i >= 0; i--)
        if(x & (1LL << i))
        {
            if(d[i]) x ^= d[i];
            else return false;
        }
    return true;
}
 
int main()
{
    int cnt = 0;
    scanf("%d", &n);
    _for(i, 1, n)
    {
        ll x; scanf("%lld", &x);
        if(!check(x))
        {
            cnt++;
            add(x);
        }
    }
    puts((cnt % 2 == 1) ? "First" : "Second");
   
	return 0;
}

魔法珠(暴击计算SG)

发现暴力计算sg不会T,然后就过了

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e3 + 10;
int sg[N], n;
unordered_map<int, int> vis;
 
int main()
{
    sg[1] = 0;
    _for(i, 2, 1e3)
    {
        vector<int> ve;
        for(int j = 1; j * j <= i; j++)
            if(i % j == 0)
            {
                ve.push_back(j);
                if(j * j != i && j != 1) ve.push_back(i / j);
            }
        
        vis.clear();
        int cur = 0, t = 0;
        for(int x: ve) cur ^= sg[x];
        for(int x: ve) vis[cur ^ sg[x]] = 1;
        while(vis[t]) t++;
        sg[i] = t;
    }

    while(~scanf("%d", &n))
    {
        int cur = 0;
        _for(i, 1, n)
        {
            int x; scanf("%d", &x);
            cur ^= sg[x];
        }
        puts(cur ? "freda" : "rainbow");
    }

	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值