题目链接

【模板】Anti-Nim游戏

题目描述

给定 堆石子,数量分别为 。两位玩家轮流操作,Alice 先手。

操作规则如下:

  1. 每次任选一堆石子,取走正整数个。
  2. 拿到最后一个石子的一方判负

如果双方均采用最优策略,判断先手是否必胜。

解题思路

本题是 Anti-Nim 游戏,也称为反 Nim 游戏或 Misere Play Nim。它的规则与标准 Nim 游戏几乎完全相同,唯一的区别在于胜负的判定:在 Anti-Nim 游戏中,取走最后一个石子的玩家失败,而不是胜利。

虽然规则只有微小差别,但其最优策略与标准 Nim 游戏有所不同,需要分情况讨论。标准 Nim 游戏的胜负手由所有石子堆数量的异或和(Nim-sum)决定。Anti-Nim 游戏的策略也基于 Nim-sum,但存在一个特殊情况。

设 Nim-sum 为

先手(Alice)必胜的条件,需要根据石子堆的状态分为两种情况:

情况一:所有石子堆的数量均为 1

在这种情况下,游戏变为一个简单的计数问题。总共有 堆,每堆 1 个石子。

  • 如果 偶数,Alice 取一堆后剩下奇数堆。Bob 再取一堆,又剩下偶数堆。如此交替,最后一定是 Bob 取走倒数第二堆,留给 Alice 最后一堆。Alice 被迫取走最后一堆而失败。因此,当 为偶数时,Alice 必败
  • 如果 奇数,Alice 取一堆后剩下偶数堆。同理,最后一定是 Alice 取走倒数第二堆,留给 Bob 最后一堆,Bob 失败。因此,当 为奇数时,Alice 必胜

注意:此处的胜负结论与常规思维相反,因为拿走最后一个算输。如果 是偶数,Alice 拿第 1, 3, 5, ..., (n-1) 个,Bob 拿第 2, 4, ..., n 个。Bob 拿最后一个,Bob 输,Alice 胜。如果 是奇数,Alice 拿第 1, 3, ..., n 个。Alice 拿最后一个,Alice 输,Bob 胜。

修正后的结论:

  • 若所有堆均为 1,当 偶数时,Alice 必胜
  • 若所有堆均为 1,当 奇数时,Alice 必败

情况二:至少有一堆石子的数量大于 1

在这种情况下,游戏的终局离得还远,玩家有足够的策略空间。其胜负手与标准 Nim 游戏完全一致:

  • 如果 Nim-sum (非零),则先手(Alice)必胜
    • 策略: Alice 总能找到一堆石子,取走一部分后使得新的 Nim-sum 变为 0。只要游戏没有进入“所有堆都为 1”的临界状态,Alice 就可以一直将 Nim-sum 为 0 的局面留给 Bob。Bob 无论如何操作,都会使得 Nim-sum 再次变为非零。最终 Alice 会将 Bob 引入一个必败局面。
  • 如果 Nim-sum (为零),则先手(Alice)必败
    • 策略: 无论 Alice 如何操作,新的 Nim-sum 必定不为 0。接下来 Bob 总能通过一次操作,使得 Nim-sum 重新变为 0。Alice 始终面对 Nim-sum 为 0 的局面,而 Bob 始终面对 Nim-sum 非 0 的局面,因此 Bob 掌握了主动权,Alice 必败。

总结

综合以上两种情况,我们可以得出 Alice (先手) 的必胜条件:

  1. 所有堆石子数都大于 1:此时 Alice 必胜当且仅当 Nim-sum 不为 0。
  2. 所有堆石子数均为 1:此时 Alice 必胜当且仅当石子堆数 为偶数。

代码

#include <iostream>
#include <vector>
#include <numeric>

void solve() {
    using namespace std;
    int n;
    cin >> n;
    vector<int> a(n);
    int nim_sum = 0;
    bool all_ones = true;
    for (int i = 0; i < n; ++i) {
        cin >> a[i];
        if (a[i] > 1) {
            all_ones = false;
        }
        nim_sum ^= a[i];
    }

    if (all_ones) {
        // 如果所有堆都是 1, 堆数为偶数则先手胜
        if (n % 2 == 0) {
            cout << "YES" << endl;
        } else {
            cout << "NO" << endl;
        }
    } else {
        // 如果至少有一堆大于 1, Nim-sum 不为 0 则先手胜
        if (nim_sum != 0) {
            cout << "YES" << endl;
        } else {
            cout << "NO" << endl;
        }
    }
}

int main() {
    using namespace std;
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    int t;
    cin >> t;
    while (t--) {
        solve();
    }
    return 0;
}
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int t = sc.nextInt();
        while (t-- > 0) {
            solve(sc);
        }
    }

    private static void solve(Scanner sc) {
        int n = sc.nextInt();
        int[] a = new int[n];
        int nim_sum = 0;
        boolean all_ones = true;
        for (int i = 0; i < n; i++) {
            a[i] = sc.nextInt();
            if (a[i] > 1) {
                all_ones = false;
            }
            nim_sum ^= a[i];
        }

        if (all_ones) {
            // 如果所有堆都是 1, 堆数为偶数则先手胜
            if (n % 2 == 0) {
                System.out.println("YES");
            } else {
                System.out.println("NO");
            }
        } else {
            // 如果至少有一堆大于 1, Nim-sum 不为 0 则先手胜
            if (nim_sum != 0) {
                System.out.println("YES");
            } else {
                System.out.println("NO");
            }
        }
    }
}
import sys

def solve():
    n = int(sys.stdin.readline())
    a = list(map(int, sys.stdin.readline().split()))
    
    nim_sum = 0
    all_ones = True
    for x in a:
        if x > 1:
            all_ones = False
        nim_sum ^= x
        
    if all_ones:
        # 如果所有堆都是 1, 堆数为偶数则先手胜
        if n % 2 == 0:
            print("YES")
        else:
            print("NO")
    else:
        # 如果至少有一堆大于 1, Nim-sum 不为 0 则先手胜
        if nim_sum != 0:
            print("YES")
        else:
            print("NO")

def main():
    t = int(sys.stdin.readline())
    for _ in range(t):
        solve()

if __name__ == "__main__":
    main()

算法及复杂度

  • 算法: 博弈论, Anti-Nim 游戏
  • 时间复杂度: 每个测试用例需要遍历一次所有石子堆,计算 Nim-sum 和检查是否全为 1,因此时间复杂度为 。总时间复杂度为
  • 空间复杂度: 需要存储 堆石子的数量,空间复杂度为