博弈论1

第一题:取沙子游戏

  • 题目简述:有 n 粒沙子,Alice 先手,轮流取。
  • 规则:第一次取:取 1 ~ min(n, k) 任意粒。之后每次取的数量必须是之前所有已取数量的最大公约数 的约数,且不能超过k,也不能超过剩余沙子数。取完最后一粒的人获胜。
  • 问:在双方都绝顶聪明的情况下,Alice 能否获胜?
    题目链接:https://qoj.ac/problem/14532

先定义当前状态为 (s, g),其中 s 表示剩余量,g 表示当前的限制,即前面所有数的 gcd。

  • s 为奇数时,直接取 1,就能转化成 (偶数,1) 的情况,是必败态,所以 s 为奇数时一定是必胜态。
  • 接下来只需要分析 s 为偶数。
    g 为奇数时,肯定只存在奇数因子,状态就会转变为 (s-a, a),因此 s 为偶数,g 为奇数时败。

s = p * 2^mg = q * 2^n,下一步取的数量为 t * 2^u

情况一:u < m
下一个状态转变为 (2^u * (p * 2^(m-u) - t), t * 2^u),此时 s、g 的 2 的幂次相同,可以得到这个状态为必胜态。证明如下:

  1. 只要取 x = k * 2^u 就相当于 (p * 2^(m-u) - t - k, k) 的变化,属于必败态。(s 中 2 的因子更多)
  2. 下一步如果想要打破状态,就只能取 c * 2^vv < u。状态转变为 (2^v * (even - odd), c * 2^v),回到了之前的必胜态。
    综上所述,当取 u < m 时,必败。

情况二:u = m
下一个状态为 (2^u * (p - t), t * 2^u),形同上述的 1.1。因此 u = m 必胜。

情况三:u > m
状态变为 (2^m(p - t * 2^(u-m)), t * 2^u),此时 s 的 2 因子少于 g,只需选择 v < m 的情况,就能把 s 中 2 的因子变得比 g 中多,变成必败态。

因此只有取 u = m 才能维持必胜态,第一步的 g 就要取到 lowbit(n),因此只要比较 lowbit(n)k 就可以解决。

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

int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int t; cin >> t;
    while(t --) {
        ll n, k;
        cin >> n >> k;
        ll lowbit = n & -n;
        if(lowbit <= k) cout << "Alice\n";
        else cout << "Bob\n";
    }

    return 0;
}

第二题:数字消失术

  • 题目大意: Alice 和 Bob 轮流从一个正整数中删除一个数字。每次删除后,产生的所有前导零会被一并删除(例如从 105 中删除百位的 1 得到 5,删除十位的 0 得到 15,删除个位的 5 得到 10)。
    删除最后一个非零数字的玩家获胜。
    定义 ( W(N) ) 为所有小于 ( N ) 的正整数中,在双方都采取最优策略下先手玩家必胜的数的个数。
    给定 ( N ),请你计算 ( W(N) )。

  • 输入描述
    一行一个整数 ( N )(( 1 \le N \le 10^{18} ))。

  • 输出描述
    一行一个整数,表示 ( W(N) )。

  • 数据范围
    ( 1 \le N \le 10^{18} )
    题目链接:https://ac.nowcoder.com/acm/contest/136171/F

    先考虑没有0的情况,显然长度为奇数的为必胜态。
    接下来考虑有0存在的情况,就相当于在d * 10^n + r 这里的状态就取决于后续r的状态。如果r是奇数,就是必胜,此时如果第一步去掉d,就变成了,胜态,所以先手不会这么做,这就取决于中间0的数量,如果0的数量是奇数,就是必胜态,偶数则是必败态。而r为偶数则相反。
    由此可见,只要n的长度为奇数,则总是必胜态。

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

ll dp[20][2], sum[20][2], pw[20];//pw 10的幂次,dp 位置i的输赢数量,sum 到i的输赢数量

//两个函数的写法 可以参考
// ll win(ll n);
// ll lose(ll n) {//先手输的数量
//     if(n <= 1) return 0;
//     string s = to_string(n);
//     int len = s.size();
//     ll ans = sum[len-1][0];

//     if(len % 2 == 0) {
//         int first = n / pw[len-1];
//         ans += (first-1) * sum[len-1][1];
//         ll r = n % pw[len-1];
//         ans += win(r);
//     }
//     return ans;
// }
// ll win(ll n) {//先手赢的数量
//     if(n <= 1) return 0;
//     return n - 1 - lose(n);
// }
一个函数的写法
ll cal(ll n) {
    if(n <= 1) return 0;
    string s = to_string(n);
    int len = s.size();

    ll ans = sum[len - 1][1];
    int first = s[0] - '0';
    ll r = n % pw[len - 1];

    if(len % 2 == 1) {
        ans += (first - 1) * pw[len - 1];
        ans += r;
    }
    else {
        ll loseCnt = sum[len - 1][0];
        loseCnt += (first - 1) * sum[len - 1][1];
        loseCnt += cal(r);
        ans = (n - 1) - loseCnt;
    }
    return ans;
}
void solve() {
    pw[0] = 1;
    for(int i = 1; i < 20; i ++) pw[i] = pw[i-1] * 10;

    for(int i = 1; i < 20; i ++) {
        ll tol = pw[i - 1] * 9;
        if(i & 1) dp[i][1] = tol, dp[i][0] = 0;
        else dp[i][0] = sum[i-1][1] * 9, dp[i][1] = tol - dp[i][0];
        
        sum[i][0] = sum[i-1][0] + dp[i][0];
        sum[i][1] = sum[i-1][1] + dp[i][1];
    }

    ll n; cin >> n;
    cout << cal(n) << endl;
}

int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    solve();
    return 0;
}

第三题:粑粑博弈

  • 题目大意: 给定一个长度为 n 的非负整数数组 a 和一个正整数 k。Alice 和 Bob 轮流进行游戏,Alice 先手。
    每轮操作,当前玩家需要选择:
    一个正整数 x1 ≤ x ≤ k
    一个下标 y1 ≤ y ≤ n
    并且必须满足 x ≤ a_y
    然后执行更新:
    k 变为 x
    a_y 变为 a_y - x

注意:如果玩家选择 x = 0 或无法做出有效操作,则立即判负。
问:在双方都采取最优策略的情况下,先手 Alice 是否有必胜策略?

  • 数据范围
    测试用例组数:1 ≤ t ≤ 10^5
    单组数组长度:1 ≤ n ≤ 10^6
    所有测试用例的 n 之和:∑ n ≤ 2 × 10^6
    初始上限:1 ≤ k ≤ 10^9
    数组元素:0 ≤ a_i ≤ 10^9
    题目链接:https://qoj.ac/contest/3758/problem/18279

虽然游戏中的上限 k 会不断变化,但通过定义一个二进制进位边界 p(严格大于当前 k 的最小 2 的幂),可以将整个游戏压缩成经典的 Nim 游戏。胜负完全由所有 a_ip 取模后的异或和决定。

对于任意正整数 k,定义函数:
$$
P(k) = 2^{\lfloor \log_2 k \rfloor + 1}
$$

严格大于 k 的最小的 2 的幂
例如:P(5)=8, P(6)=8, P(8)=16

该函数具有两个关键性质(在证明中会反复使用):

  1. 对于任意合法操作 x(满足 1 ≤ x ≤ k),必有 x < P(k)
  2. p = P(k),则 p/2 ≤ k < p
  3. 任何新的上限 x 所对应的新模数 p' = P(x) 一定是旧模数 p 的约数(因为 pp' 都是 2 的幂,且 p' ≤ p)。
  • 单堆情形的严格证明

状态(k, a),当前上限为 k,堆大小为 a

结论:单堆状态下,必败态 当且仅当:

$$
a \bmod P(k) = 0
$$

  1. 证明“余数为 0 是必败态”(无法走向余数为 0)

设当前模数 p = P(k),且 a % p = 0
当前玩家取 x1 ≤ x ≤ k),新状态为 (x, a-x),新模数为 p' = P(x)

  • 因为 x < pp 是 2 的幂,所以 p' 整除 p
  • 由于 ap 的倍数,而 p' | p,因此 a 也是 p' 的倍数,即 a % p' = 0
  • 计算新堆的余数:
    $$
    (a - x) \bmod p' = (0 - x) \bmod p' = (-x) \bmod p'
    $$
  • 由于 1 ≤ x < p'(因为 p' 是大于 x 的最小 2 的幂),所以 x 不可能是 p' 的倍数。
  • 因此 (-x) mod p' ≠ 0,即新状态的余数不为 0。

结论:从余数为 0 的状态出发,无论如何走,都会走到余数非 0 的状态(即必胜态)。符合“必败态”定义。

  1. 证明“余数非 0 是必胜态”(总能走向余数为 0)

设当前模数 p = P(k),余数 r = a % p,显然 1 ≤ r < p。由于 p/2 ≤ k < p,分两种情况构造取法 x

情况 A:若 r ≤ k
直接取 x = r

  • 取走后,a - xp 的倍数。
  • 新模数 p' = P(x) 整除 p,所以 a - x 也是 p' 的倍数。
  • 新状态余数为 0,成功。

情况 B:若 r > k
q = p / 2,因为 r > k ≥ q,且 r < p = 2q,我们构造取 x = r - q

  • 合法性验证:因为 r > q,所以 x ≥ 1;因为 r < 2q,所以 x < q ≤ k,故 1 ≤ x ≤ k,合法。
  • 计算新堆大小:
    $$
    a - x = a - (r - q) = (p \cdot t + r) - r + q = p \cdot t + q
    $$
    因为 p = 2q,所以:
    $$
    a - x = 2q \cdot t + q = q \cdot (2t + 1)
    $$
    a - xq 的倍数。
  • 新模数 p' = P(x),因为 0 < x < q,所以 p' 整除 q
  • 因此 a - x 也是 p' 的倍数,新状态余数为 0,成功。

结论:任意余数非 0 的状态,都能一步走到余数为 0 的状态。符合“必胜态”定义。

三、多堆情形的严格证明

状态(k, a_1, ..., a_n),当前上限为 k。设 p = P(k)

定义当前局面的 特征异或值 X

$$
X = (a_1 \bmod p) \oplus (a_2 \bmod p) \oplus \dots \oplus (a_n \bmod p)
$$

其中 是按位异或运算。

结论:多堆状态下,必败态 当且仅当:

$$
X = 0
$$

  1. 证明“X=0 是必败态”(无法走向 X=0)

设当前模数 p = P(k),且 X = 0
当前玩家选择 x1 ≤ x ≤ k)修改第 y 堆:a_y → a_y - x。新状态模数为 p' = P(x),显然 p' | p

对于除了第 y 堆以外的所有堆,其模 p' 下的余数不变。
因为原 X=0,在模 p 下异或为 0。由于 p'p 的因子(相当于只取二进制低位),我们可以推导出新异或值:

$$
X' = \big( (a_y \bmod p) \bmod p' \big) \oplus \big( (a_y - x) \bmod p' \big)
$$

如果 X' = 0,则必须有:
$$
(a_y \bmod p') = (a_y - x) \bmod p'
$$
这等价于要求 x ≡ 0 (mod p')
但是,因为 1 ≤ x < p'p' 是严格大于 x 的 2 的幂),x 不可能整除 p',矛盾。

结论:从 X=0 出发,所有走法都会导致 X' ≠ 0。符合“必败态”定义。

  1. 证明“X≠0 是必胜态”(总能走向 X=0)

设当前模数 p = P(k),且 X ≠ 0。利用经典 Nim 博弈的思路构造操作。

在模 p 下,找到异或和最高位为 1 的那一堆(设为第 t 堆)。设该堆当前余数为 b = a_t mod p。令 b' = b ⊕ X,根据异或性质必有 b' < b。令标准 Nim 需要取走的数量为 r = b - b'

由于 p/2 ≤ k < p,分两种情况构造:

情况 A:若 r ≤ k
直接取 x = r

  • 在模 p 下,第 t 堆余数变为 b',所有堆的异或和变为 0。
  • 新模数 p' = P(x) 整除 p。既然在模 p 下异或为 0,那么在模 p'(去掉高位)下异或也一定为 0。
  • 新状态 X' = 0,成功。

情况 B:若 r > k
q = p / 2,构造取 x = r - q

  • 合法性:因为 r > k ≥ q,且 r < p = 2q,所以 1 ≤ r - q < q ≤ k,即 x 合法。
  • 取走后,第 t 堆在模 p 下的新余数为:
    $$
    b - x = b - (r - q) = b - r + q = b' + q
    $$
  • 新模数 p' = P(x),因为 0 < x < q,所以 p' 整除 q。因此 (b' + q) mod p' = b' mod p'
  • 因为原构造保证了在模 p 下所有堆异或为 0,取模 p' 后(去除高位)依然为 0,所以新状态 X' = 0,成功。

结论:任意 X≠0 的状态,都能一步走到 X=0 的状态。符合“必胜态”定义。

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

int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);

    int t; cin >> t;
    while(t --) {
        int n;
        long long k; cin >> n >> k;

        long long p = 1;
        while(p <= k) p <<= 1;

        int xr = 0;
        for(int i = 0; i < n; i ++) {
            long long a; cin >> a;
            xr ^= (int)(a % p);
        }
        cout << (xr == 0 ? "Bob" : "Alice") << '\n';
    }

    return 0;
}

第四题:巧克力游戏

  • 题目大意: 有一块 n 行 m 列的巧克力,每格有一个正整数美味值 a_{i,j}。Putata 和 Budada 轮流吃,Putata 先手。每次操作可以选择吃掉剩余巧克力的最后 x 行(1 ≤ x ≤ 当前行数)或最后 y 列(1 ≤ y ≤ 当前列数),即只能从右下角切除一个连续的行块或列块。若某人操作后,剩余巧克力的总美味值 ≤ s,则此人被批评(输)。二人均采取最优策略避免被批评。

  • 给定 q 个容忍度 s_i,对每个 s_i 判断最终谁会被批评。

  • 输入格式
    第一行一个整数 T,表示测试用例数。
    每个测试用例:
    第一行三个整数 n, m, q。
    接下来 n 行,每行 m 个整数,表示 a_{i,j}。
    接下来 q 行,每行一个整数 s_i。
    对于每个查询,输出一行 "Putata"(如果 Putata 会被批评)或 "Budada"(如果 Budada 会被批评)。

  • 数据范围
    1 ≤ T ≤ 10^5
    1 ≤ n·m ≤ 2×10^5
    1 ≤ q ≤ 2×10^5
    1 ≤ a_{i,j} ≤ 10^9
    0 ≤ s_i < 所有 a_{i,j} 的总和
    所有测试用例的 n·m 之和 ≤ 2×10^5 q 之和 ≤ 2×10^5
    题目链接:https://qoj.ac/contest/2021/problem/10732

    定义坏格为(i, j)为顶点和(1, 1)围成的矩形中甜度<=s的格子,用L表示。P表示必败态,N表示必胜态。列数大于行数,否则就转置。  
    当一个格子上 左都是L的时候,这个格子为必败态,或者往上方和左方延伸,只能传递到必胜态的也是必败态。而某一个格子为必胜态,只有从该格子方和左方延伸,找得到必败态的时候,才能定义为必胜态。而由此可以得出,L往下方和右方延伸的格子都必然是必胜态。  
而存在特殊情况,有些格子无法被覆盖:  
L L L L L L   L L L L L P  
L L P N N N   L L P N N N  
L P N N N N   L P N N N N  
L N N P N N   L N N P N N  
L N N N P N   L N N N P N  
L N N N N P   L N N N N N  

L L P N N  
L P N N N  
L N N P N  
L N N N P  
L N N N N  

右下方3*2的格子是由其他已知的部分推算的。(3, 4)因为无法转移到P,所以是P,然后该点下方和右方都转变为N,以此类推  
填充必然使用如下矩阵,从左上角裁剪:  
P N N N N  
N P N N N  
N N P N N  
N N N P N  
N N N N P  

而观察可以发现,这些填充格子必然会补全原本没有P的行列,因为复杂区域就是由没有P的行列产生,而由P的分布可知,第一次出现的特殊区域必定在现在推导的矩形的右下角。所以,只有当列数大于行数的时候,才能保证P不落在最后一格上面。当前面都满足,而最后一行满足为正方形时,P只能落在右下角。  

定义f = i - e - (m - pi) 非满行-可用列。i为当前行,e是全为L的行数,pi是当前行的最大坏格列  
必败条件:  
前n行, f < 0 否则说明这一行的末尾存在P  
第n行, f = 0  

如果不存在复杂情况,那么(n-1, m-1)的区域必然全覆盖,而如果(1, m-1)为L,(1, m)不为,那么(1, m)为P。此时f=0  
(n-1, 1)为L,而(n, 1)不为L,此时,非满行 = 可用列,说明下方是个正方形,而p不可能相邻,这就说明每个缝隙都被一个P填充  

代码:

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

void solve() {
    int n, m, q; cin >> n >> m >> q;
    vector<vector<ll>> a(n, vector<ll>(m));
    for(int i = 0; i < n; i ++)
        for(int j = 0; j < m; j ++) cin >> a[i][j];
    //转置
    if(n > m) {
        vector<vector<ll>> b(m, vector<ll>(n));
        for(int i = 0; i < n; i ++)
            for(int j = 0; j < m; j ++) b[j][i] = a[i][j];
        swap(n, m);
        a = move(b);
    }

    vector<vector<ll>> pre(n + 1, vector<ll>(m + 1, 0));
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= m; j ++) pre[i][j] = pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] + a[i-1][j-1];
    
    vector<pair<ll, int>> queries(q);// 离线查询
    for(int i = 0; i < q; i ++) {
        cin >> queries[i].first;
        queries[i].second = i;
    }
    sort(queries.begin(), queries.end());
    vector<int> p(n + 1, 0);
    vector<int> ans(q);

    for(auto &[s, idx] : queries) {
        for(int i = 1; i <= n; i ++) {
            while(p[i] < m && pre[i][p[i] + 1] <= s) p[i] ++;
        }
        int e = 0;
        while (e < n && p[e + 1] == m) e ++;

        bool ok = 1;
        for(int i = e + 1; i < n; i ++) {
            if(i - e - (m - p[i]) >= 0) {
                ok = 0;
                break;
            }
        }
        if(ok && n > e && n - e - (m - p[n]) != 0) ok = 0;
        ans[idx] = ok;
    }
    for(const int& ok : ans) cout << (ok ? "Putata" : "Budada") << '\n';
}

int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int t; cin >> t;
    while(t --) solve();
    return 0;
}
posted @ 2026-08-05 15:18  thenrify  阅读(2)  评论(0)    收藏  举报