题解:洛谷 P8901 Circular Barn

【题目来源】

洛谷:P8901 [USACO22DEC] Circular Barn S - 洛谷

【题目描述】

Farmer John 和他的死对头 Farmer Nhoj 在一个环形牛棚内玩游戏。牛棚内有 \(N(1 \le N \le 10^5)\) 个房间,第 \(i\) 个房间初始时内有 \(a_i\) 头奶牛 \((1 \le a_i \le 5 \times 10^6)\)。游戏玩法如下:

  • 两位农夫将总是在同一个房间里。进入一个房间后,每位农夫各执行一次行动,Farmer John 在先。两位农夫在游戏开始时进入房间 \(1\)
  • 如果当前房间中的奶牛数量为零,则此时执行行动的农夫即失败。否则,执行行动的农夫选择一个整数 \(P\),其中 \(P\)\(1\) 或一个不超过当前房间中奶牛的数量的质数,并从当前房间中移除 \(P\) 头奶牛。
  • 两位农夫均完成行动之后,两位农夫移动到环形牛棚的下一间房间。也就是说,如果农夫们在房间 \(i\),那么他们会移动到房间 \(i+1\),除非他们在房间 \(N\),在这种情况下他们会移动到房间 \(1\)

当两位农夫均采用最优策略时,求获胜的农夫。

【输入】

输入包含 \(T\) 个子测试用例。输入的第一行包含 \(T(1 \le T \le 1000)\)。下面是 \(T\) 个子测试用例。

每个子测试用例的第一行包含 \(N\),第二行包含 \(a_1, \cdots ,a_N\)

输入保证所有 \(N\) 之和不超过 \(2 \times 10^5\)

【输出】

对于每一个子测试用例,输出获胜的农夫,为 Farmer JohnFarmer Nhoj 之一。

【输入样例】

5
1
4
1
9
2
2 3
2
7 10
3
4 9 4

【输出样例】

Farmer Nhoj
Farmer John
Farmer John
Farmer John
Farmer Nhoj

【核心思想】

  1. 问题分析\(N\) 个环形房间,每房间 \(a_i\) 头奶牛。两位农夫轮流从当前房间移除 \(1\) 或一个质数头奶牛,然后移动到下一房间。当房间为空时行动的农夫失败。求最优策略下的获胜者。本质上是博弈论 + SG 函数分析问题:每个房间独立构成一个 Nim-like 博弈,通过分析每房间的"轮数"(即双方各行动一次的周期)确定全局胜负。

  2. 算法选择

    • 欧拉筛预处理:预处理 \(1\)\(5 \times 10^6\) 的所有质数
    • 轮数计算:对每个房间 \(a_i\),计算在双方最优策略下能进行的轮数 \(Round[i]\)
    • 最小轮数决定胜负:轮数最少的房间决定胜负,若该房间轮数为奇数则 John 胜,偶数则 Nhoj 胜
  3. 关键步骤

    • 预处理质数:欧拉筛法求 \(1\)\(maxA\) 的所有质数
    • 处理每个测试用例
      • 读入 \(N\)\(a[1..N]\)
      • 对每个房间计算 \(Round[i]\)
        • \(a_i = 1\)\(Round[i] = 1\)
        • \(a_i \bmod 4 = 0\)\(Round[i] = a_i / 4 + 1\)
        • \(a_i \bmod 4 = 2\)\(Round[i] = a_i / 4 + 1\)
        • \(a_i \bmod 4 = 1\)\(3\):找最大质数 \(p \leq a_i\) 使得 \(a_i - p\)\(4\) 的倍数,\(Round[i] = (a_i - p) / 4 + 1\)
      • \(Round\) 最小的房间 \(minRoom\)
      • \(a[minRoom] \bmod 4 \neq 0\),输出 Farmer John;否则输出 Farmer Nhoj
  4. 时间/空间复杂度

    • 时间复杂度:\(O(maxA + T \cdot N \cdot \sqrt{a_i})\),预处理 \(O(maxA)\),每个房间找质数 \(O(\sqrt{a_i})\)
    • 空间复杂度:\(O(maxA)\),质数表和标记数组
  5. 博弈论 + 质数分析的核心思想

    • 轮数模型:每轮双方各行动一次,房间奶牛数减少 \(4\) 的倍数(因为双方各取 \(1\) 或质数,最优策略下总和趋向 \(4\)
    • \(4\) 分类\(a_i \bmod 4\) 的结果决定博弈结构,\(0\)\(2\) 可直接计算轮数,\(1\)\(3\) 需找合适的质数调整
    • 最小轮数原理:环形游戏中,双方会尽量将对手引向轮数最少的房间,该房间成为决胜点
    • 奇偶性判定:轮数为奇数意味着 John 在该房间执行最后一次非零行动,Nhoj 面对空房间失败
    • 适用于组合博弈、质数性质、环形策略类问题

【解题思路】

【算法标签】

普及+ #博弈论

【代码详解】

#include <bits/stdc++.h>
using namespace std;

const int maxA = 5e6 + 5;  // 预计算质数的最大值
int prime[maxA], cnt;      // 存储质数的数组和计数器
bool notPrime[maxA];       // 标记是否为非质数

/**
 * 欧拉筛法,获取1到maxA之间的所有质数
 */
void getPrime()
{
    notPrime[1] = true;    // 1不是质数
    for (int x = 2; x < maxA; x++)
    {
        if (!notPrime[x])  // 如果x是质数
        {
            prime[cnt++] = x;  // 将x存入质数数组
        }
        // 筛去x的倍数
        for (int j = 0; j < cnt && x * prime[j] < maxA; j++)
        {
            notPrime[x * prime[j]] = true;  // 标记为合数
            if (x % prime[j] == 0) break;   // 保证每个数只被最小质因子筛去
        }
    }
}

const int maxN = 1e5 + 5;  // 最大房间数
int a[maxN];               // 存储每个房间的初始干草捆数
int Round[maxN];           // 存储每个房间的轮数

int main()
{
    getPrime();  // 预处理质数表

    int t;       // 测试用例数
    cin >> t;
    while (t--)
    {
        int n;   // 当前测试用例的房间数
        cin >> n;

        // 处理每个房间
        for (int i = 1; i <= n; i++)
        {
            cin >> a[i];  // 输入干草捆数

            // 特殊情况处理
            if (a[i] == 1)  // 只有1捆干草
            {
                Round[i] = 1;  // 只需1轮
            }
            else if (a[i] % 4 == 0)  // 干草数是4的倍数
            {
                Round[i] = a[i] / 4 + 1;  // 计算轮数
            }
            else if (a[i] % 4 == 2)  // 干草数除4余2
            {
                Round[i] = a[i] / 4 + 1;  // 计算轮数
            }
            else  // 其他情况(干草数除4余1或3)
            {
                int target = 0;
                // 寻找最大的质数,使得a[i]-target是质数
                while (notPrime[a[i] - target])
                {
                    target += 4;  // 每次增加4
                }
                Round[i] = target / 4 + 1;  // 计算轮数
            }
        }

        // 找出轮数最少的房间
        int minRound = maxA, minRoom = -1;
        for (int i = 1; i <= n; i++)
        {
            if (Round[i] < minRound)
            {
                minRound = Round[i];
                minRoom = i;
            }
        }

        // 根据轮数判断赢家
        cout << (a[minRoom] % 4 ? "Farmer John" : "Farmer Nhoj") << endl;
    }
    return 0;
}

【运行结果】

5
1
4
Farmer Nhoj
1
9
Farmer John
2
2 3
Farmer John
2
7 10
Farmer John
3
4 9 4
Farmer Nhoj
posted @ 2026-08-17 22:49  团爸讲算法  阅读(7)  评论(0)    收藏  举报