You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

5.3 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

##AcWing 1321. 取石子

一、题目描述

AliceBob 两个好朋友又开始玩取石子了。

游戏开始时,有 N 堆石子排成一排,然后他们轮流操作(Alice 先手),每次操作时从下面的规则中任选一个:

  • 从某堆石子中取走一个;
  • 合并任意两堆石子。

不能操作的人输。

Alice 想知道,她是否能有必胜策略。

输入格式 第一行输入 T,表示数据组数。

对于每组测试数据,第一行读入 N

接下来 N 个正整数 a_1,a_2,⋯,a_N ,表示每堆石子的数量。

输出格式 对于每组测试数据,输出一行。

输出 YES 表示 Alice 有必胜策略,输出 NO 表示 Alice 没有必胜策略。

数据范围 1≤T≤100,1≤N≤50,1≤a_i≤1000

输入样例:

3
3
1 1 2
2
3 4
3
2 3 5

输出样例:

YES
NO
NO

二、博弈论总结

必胜态 \Rightarrow 选择合适方案 \Rightarrow 必败态 必败态 \Rightarrow 选择任何路线 \Rightarrow 必胜态

三、本题题解

先来考虑 简单情况:所有堆的石子个数 >1

b = 堆数 + 石子总数  1

可以推出: 先手必胜 \Leftrightarrow b是奇数

证明:考虑以下结论:

α. 任意一个是奇数的情况 可以 被转换成一个偶数的情况 (一定存在一个偶数后继)

β. 任意一个是偶数的情况 一定 会被转换成一个奇数的情况 (所有后继都是奇数)。

先来考虑结论 α.

  • 如果堆数 >1,可以合并其中两堆石子,这样 b 就会变成偶数
  • 如果堆数 =1,就可以拿掉 1 个石子,这样 b 也会变成偶数

再来考虑结论 β.

  • 合并其中的两堆石子:显然,b 会变成奇数
  • 取了一个石子: (1) 取得堆中石子个数 >2:石子个数  1b 会变成奇数 (2) 取得堆中石子个数 =2:石子个数变成了 1(继续分情况讨论)
    1. 只有一堆:b 是奇数,对手必胜
    2. 多于一堆:b 是奇数,对手可以把剩下的一个石子放到其他的堆里面去,对手同样必胜

这样,我们成功的证明除了: 先手必胜 \Leftrightarrow (b 是奇数)

接下来考虑一般情况:有些堆的石子个数可能 =1 假设石子个数 =1 的堆数为 ab 仍然表示剩余石子的 操作数,即 (堆数 + 石子总数  1) 把所有石子分成两个区域,一区域的数量 =1 (对应 a),二区域的数量都 >1 (对应 b).

那么可以写出以下转移:

f(a,b):

① 从 a中取 1→f(a1,b) ② 从 b中取 1→f(a,b1) ③ 合并 b中的 2→f(a,b1) ④ 合并 a中 2个 (b堆石子数 +2,堆数 +1) →f(a2,b+3) ⑤ 合并 a中 1个 b中 1个 (b堆石子数 +1a个数 1)→f(a1,b+1)

Code

#include <bits/stdc++.h>
using namespace std;
const int N = 55, M = 50050; // M 包括了 50 * 1000 + 50个石子数量为1的堆数
int f[N][M];

int dfs(int a, int b) {
    int &v = f[a][b];
    if (~v) return v;
    // 简单情况: 即所有堆的石子个数都是严格大于1此时a是0
    if (!a) return v = b % 2; // 奇数为先手必胜,偶数为先手必败

    // 一般5个情况 + 1个特殊情况
    
    // 特殊情况: 如果操作后出现b中只有一堆且堆中石子个数为1
    // 那么应该归入到a中并且b为0
    // 以下所有情况,如果能进入必败态,先手则必胜!
    if (b == 1) return dfs(a + 1, 0);

    // 情况1有a从a中取一个
    if (a && !dfs(a - 1, b)) return v = 1;

    // 情况2, 4有b从b中取1个(石子总数 - 1) or 合并b中两堆(堆数 - 1),
    if (b && !dfs(a, b - 1)) return v = 1;

    // 情况3有a >= 2 合并a中两个
    // 如果b的堆数不为0 a - 2,  b + 1堆 + 2个石子只需要加delta  ====> b + 3
    // 如果b的堆数为0 a - 2,  0 + 2个石子 + 1堆 - 1  ====> b + 2
    if (a >= 2 && !dfs(a - 2, b + (b ? 3 : 2))) return v = 1;

    // 情况5有a有b 合并a中1个b中1个, a - 1, b的堆数无变化 + 1个石子只加delta
    if (a && b && !dfs(a - 1, b + 1)) return v = 1;

    // 其他情况,则先手处于必败状态
    return v = 0;
}

int main() {
    memset(f, -1, sizeof f);
    int T, n;
    cin >> T;
    while (T--) {
        cin >> n;
        int a = 0, b = 0;
        for (int i = 0; i < n; i++) {
            int x;
            cin >> x;
            if (x == 1) a++;
            // b != 0时 加1堆 + 加x石子 = 原来的 + x + 1 (其实就是区别一开始的时候)
            // 当b != 0时, 我们往后加的delta
            // b == 0时 加1堆 + 加x石子 = 0 + 1 + x - 1 = x
            // 注意操作符优先级
            else
                b += b ? x + 1 : x;
        }

        // 1 为先手必胜, 0为先手必败
        if (dfs(a, b))
            puts("YES");
        else
            puts("NO");
    }
    return 0;
}