先做一个容量 200 的 0/1 背包,求每个总 p 下能得到的最大总 k,再按回合数与 d 值差做极小极大动态规划。
OJ: luogu
题目 ID: P7097
难度:提高+/省选-
标签:动态规划背包状态设计极小化极大
日期: 2026-06-21 09:59
题意
两个人轮流行动,但不是固定轮流,而是谁的 d 值更小就由谁行动;如果相同则扶苏先动。
一回合中,当前行动者:
- 可以从
m种道具里任意选一些,每种本回合最多用一次 - 一定会发动一次攻击
- 回合结束后自己的
d值一定增加w,并且还会额外增加所选道具的p之和
第 i 种道具的效果是:
- 本回合伤害额外增加
原始伤害 * k_i / 10^5 - 本回合结束后
d值额外增加p_i
并且任意一回合结束后,双方 d 值差的绝对值都不能超过 100。
游戏共进行 n 回合。
扶苏要最大化 扶苏总伤害 - 扶咕咕总伤害,扶咕咕会尽力让这个值尽量小。求双方都最优时的最终结果。
思路
先看一个可以直接验证想法的朴素解:
#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 越大越好:
- 扶苏行动时,他想让自己的这一回合伤害尽量大
- 扶咕咕行动时,她也想让自己的这一回合伤害尽量大,从而让最终差值更小
所以可以先做一个容量只有 200 的 0/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]
代码
#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;
}复杂度
背包复杂度是
DP 状态数是 p,所以复杂度是:
可以通过。
总结
这题最关键的是把“一回合选哪些道具”压成:
- 总
p - 该总
p下最大的总k
一旦压成这个形式,后面的博弈只剩下 d 值差上的小状态极小极大 DP。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
