[yLOI2020] 牵丝戏

GitHub跳转原题关系图返回列表

先做一个容量 200 的 0/1 背包,求每个总 p 下能得到的最大总 k,再按回合数与 d 值差做极小极大动态规划。

OJ: luogu

题目 ID: P7097

难度:提高+/省选-

标签:动态规划背包状态设计极小化极大

日期: 2026-06-21 09:59

题意

两个人轮流行动,但不是固定轮流,而是谁的 d 值更小就由谁行动;如果相同则扶苏先动。

一回合中,当前行动者:

  1. 可以从 m 种道具里任意选一些,每种本回合最多用一次
  2. 一定会发动一次攻击
  3. 回合结束后自己的 d 值一定增加 w,并且还会额外增加所选道具的 p 之和

i 种道具的效果是:

  • 本回合伤害额外增加 原始伤害 * k_i / 10^5
  • 本回合结束后 d 值额外增加 p_i

并且任意一回合结束后,双方 d 值差的绝对值都不能超过 100

游戏共进行 n 回合。
扶苏要最大化 扶苏总伤害 - 扶咕咕总伤害,扶咕咕会尽力让这个值尽量小。求双方都最优时的最终结果。

思路

先看一个可以直接验证想法的朴素解:

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

const long long NEG_INF = -(1LL << 60);

int subtask_id;
int n, m, w;
long long k[25];
int p[25];
long long choose_best_k[205];
long long memo[1005][205];
bool vis[1005][205];
long long xa, xb;
int start_da, start_db;

long long dfs(int turn_left, int delta) {
    if (turn_left == 0) {
        return 0;
    }
    if (vis[turn_left][delta + 100]) {
        return memo[turn_left][delta + 100];
    }
    vis[turn_left][delta + 100] = true;

    long long ans;

    if (delta <= 0) {
        ans = NEG_INF;
        for (int sum_p = 0; sum_p <= 200; sum_p++) {
            if (choose_best_k[sum_p] == NEG_INF) {
                continue;
            }
            int next_delta = delta + w + sum_p;
            if (next_delta < -100 || next_delta > 100) {
                continue;
            }
            long long damage = xa + (xa / 100000) * choose_best_k[sum_p];
            ans = max(ans, damage + dfs(turn_left - 1, next_delta));
        }
    } else {
        ans = NEG_INF;
        for (int sum_p = 0; sum_p <= 200; sum_p++) {
            if (choose_best_k[sum_p] == NEG_INF) {
                continue;
            }
            int next_delta = delta - w - sum_p;
            if (next_delta < -100 || next_delta > 100) {
                continue;
            }
            long long damage = xb + (xb / 100000) * choose_best_k[sum_p];
            ans = max(ans, damage - dfs(turn_left - 1, next_delta));
        }
        ans = -ans;
    }

    memo[turn_left][delta + 100] = ans;
    return ans;
}

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

    // brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
    // 先枚举每个子集的 (sum_p, sum_k),对同一 sum_p 保留最大的 sum_k,
    // 再按回合数和 d 值差做记忆化搜索。
    cin >> subtask_id;
    cin >> n >> m >> w;
    for (int i = 1; i <= m; i++) {
        cin >> k[i];
    }
    for (int i = 1; i <= m; i++) {
        cin >> p[i];
    }
    cin >> xa >> xb >> start_da >> start_db;

    for (int i = 0; i <= 200; i++) {
        choose_best_k[i] = NEG_INF;
    }
    choose_best_k[0] = 0;

    for (int mask = 0; mask < (1 << m); mask++) {
        int sum_p = 0;
        long long sum_k = 0;
        for (int i = 0; i < m; i++) {
            if ((mask >> i) & 1) {
                sum_p += p[i + 1];
                sum_k += k[i + 1];
            }
        }
        if (sum_p <= 200) {
            choose_best_k[sum_p] = max(choose_best_k[sum_p], sum_k);
        }
    }

    cout << dfs(n, start_da - start_db) << '\n';
    return 0;
}

设当前状态只看:

delta = d_a - d_b

因为谁行动只取决于这个差值的正负:

  • delta <= 0:轮到扶苏
  • delta > 0:轮到扶咕咕

关键观察是:一回合真正影响后续的,只是本回合所选道具的:

  • p
  • k

而对同一个总 p 来说,显然总 k 越大越好:

  • 扶苏行动时,他想让自己的这一回合伤害尽量大
  • 扶咕咕行动时,她也想让自己的这一回合伤害尽量大,从而让最终差值更小

所以可以先做一个容量只有 2000/1 背包:

best_k[s] = 总 p 恰好为 s 时,能得到的最大总 k

为什么只需要做到 200
因为一回合结束后 |delta| <= 100,而 w <= 100,所以可行的总 p 不会超过 200

接着做极小极大 DP。

设:

f[i][delta] = 还剩 i 回合、当前 d_a-d_b=delta 时,最终最优伤害差

如果 delta <= 0,轮到扶苏,他会取最大值。
若本回合选出总 p = s,那么:

  • 本回合伤害增加:x_a + x_a / 10^5 * best_k[s]
  • 新的差值:delta + w + s

如果 delta > 0,轮到扶咕咕,她会取最小值。
若本回合选出总 p = s,那么:

  • 本回合伤害差要减去:x_b + x_b / 10^5 * best_k[s]
  • 新的差值:delta - w - s

因为 delta 只有 [-100,100]201 种情况,所以总状态很小。

DP 转移方程

核心状态:

f[i][delta] 为剩余 i 回合的最优伤害差

核心转移:

扶苏取 max,扶咕咕取 min,枚举 s 更新 delta±(w+s)

答案收束:

f[n][0]

代码

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

const int MAXP = 205;
const long long NEG_INF = -(1LL << 60);

int subtask_id;
int n, m, w;
long long k[MAXP * 500];
int p[MAXP * 500];
long long best_k[MAXP];
long long dp[2][205];

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

    cin >> subtask_id;
    cin >> n >> m >> w;
    for (int i = 1; i <= m; i++) {
        cin >> k[i];
    }
    for (int i = 1; i <= m; i++) {
        cin >> p[i];
    }

    long long xa, xb;
    int da, db;
    cin >> xa >> xb >> da >> db;

    for (int i = 0; i < MAXP; i++) {
        best_k[i] = NEG_INF;
    }
    best_k[0] = 0;

    // 只需要关心总 p 不超过 200 的方案。
    for (int i = 1; i <= m; i++) {
        if (p[i] > 200) {
            continue;
        }
        for (int s = 200; s >= p[i]; s--) {
            if (best_k[s - p[i]] == NEG_INF) {
                continue;
            }
            best_k[s] = max(best_k[s], best_k[s - p[i]] + k[i]);
        }
    }

    long long unit_a = xa / 100000;
    long long unit_b = xb / 100000;
    int offset = 100;

    for (int d = -100; d <= 100; d++) {
        dp[0][d + offset] = 0;
    }

    for (int turn = 1; turn <= n; turn++) {
        int cur = turn & 1;
        int pre = cur ^ 1;

        for (int d = -100; d <= 100; d++) {
            if (d <= 0) {
                // 轮到扶苏行动,他希望最大化最终伤害差。
                long long best = NEG_INF;
                int max_sum_p = 100 - d - w;
                if (max_sum_p < 0) {
                    dp[cur][d + offset] = NEG_INF;
                    continue;
                }
                if (max_sum_p > 200) {
                    max_sum_p = 200;
                }

                for (int sum_p = 0; sum_p <= max_sum_p; sum_p++) {
                    if (best_k[sum_p] == NEG_INF) {
                        continue;
                    }
                    int next_d = d + w + sum_p;
                    long long damage = xa + unit_a * best_k[sum_p];
                    best = max(best, damage + dp[pre][next_d + offset]);
                }
                dp[cur][d + offset] = best;
            } else {
                // 轮到扶咕咕行动,她会最小化扶苏 - 扶咕咕的伤害差。
                long long best = NEG_INF;
                int max_sum_p = d + 100 - w;
                if (max_sum_p < 0) {
                    dp[cur][d + offset] = NEG_INF;
                    continue;
                }
                if (max_sum_p > 200) {
                    max_sum_p = 200;
                }

                for (int sum_p = 0; sum_p <= max_sum_p; sum_p++) {
                    if (best_k[sum_p] == NEG_INF) {
                        continue;
                    }
                    int next_d = d - w - sum_p;
                    long long damage = xb + unit_b * best_k[sum_p];
                    best = max(best, damage - dp[pre][next_d + offset]);
                }
                dp[cur][d + offset] = -best;
            }
        }
    }

    cout << dp[n & 1][da - db + offset] << '\n';
    return 0;
}

复杂度

背包复杂度是 O(m200)O(m * 200)

DP 状态数是 O(n201)O(n * 201),每个状态再枚举一次总 p,所以复杂度是:

O(m200+n201201)O(m * 200 + n * 201 * 201)

可以通过。

总结

这题最关键的是把“一回合选哪些道具”压成:

  • p
  • 该总 p 下最大的总 k

一旦压成这个形式,后面的博弈只剩下 d 值差上的小状态极小极大 DP。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析