用双队列模拟强弱顺序,先处理必吃局面,再用递归反推第一次冒险吃是否成立。
OJ: luogu
题目 ID: P7078
难度:提高+/省选-
标签:贪心博弈双端队列模拟
日期: 2026-07-06 08:46
题意
有 n 条蛇,每条蛇有体力值和编号。比较强弱时,体力值大的更强;体力值相同则编号大的更强。
每一轮由当前最强蛇决定是否吃掉当前最弱蛇。如果吃,它的体力值减去最弱蛇的体力值,最弱蛇退出;如果不吃,游戏立即结束。每条蛇最优先保证自己不被吃,在能保证存活时才希望多吃蛇。
要求输出最终剩余蛇的数量。第一组数据给出完整体力值,后续数据在上一组基础上做若干单点修改。
思路
小数据可以直接做博弈搜索:当前最强蛇先假设自己吃掉最弱蛇,然后递归看它在后续游戏中是否还能存活。如果能存活,它就会吃;否则它会停手。
// brute.cpp:小数据完整博弈搜索,判断最强蛇吃后自己是否还能存活。
#include <bits/stdc++.h>
using namespace std;
struct Snake {
long long value;
int id;
};
int T, n;
long long a[20];
map<string, vector<Snake> > memo_alive;
bool weaker_than(const Snake &a, const Snake &b) {
if (a.value != b.value) {
return a.value < b.value;
}
return a.id < b.id;
}
string encode_state(const vector<Snake> &state) {
string key;
for (int i = 0; i < (int)state.size(); i++) {
key += to_string(state[i].id);
key += ':';
key += to_string(state[i].value);
key += ',';
}
return key;
}
bool contains_id(const vector<Snake> &alive, int id) {
for (int i = 0; i < (int)alive.size(); i++) {
if (alive[i].id == id) {
return true;
}
}
return false;
}
vector<Snake> play_game(vector<Snake> state) {
sort(state.begin(), state.end(), weaker_than);
string key = encode_state(state);
auto it = memo_alive.find(key);
if (it != memo_alive.end()) {
return it->second;
}
if ((int)state.size() == 1) {
memo_alive[key] = state;
return state;
}
Snake weakest = state.front();
Snake strongest = state.back();
vector<Snake> next_state;
for (int i = 1; i + 1 < (int)state.size(); i++) {
next_state.push_back(state[i]);
}
Snake changed;
changed.value = strongest.value - weakest.value;
changed.id = strongest.id;
next_state.push_back(changed);
vector<Snake> alive_after_eat = play_game(next_state);
if (contains_id(alive_after_eat, strongest.id)) {
memo_alive[key] = alive_after_eat;
} else {
memo_alive[key] = state;
}
return memo_alive[key];
}
int solve_current_case() {
vector<Snake> state;
for (int i = 1; i <= n; i++) {
Snake x;
x.value = a[i];
x.id = i;
state.push_back(x);
}
memo_alive.clear();
vector<Snake> alive = play_game(state);
return (int)alive.size();
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> T;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
cout << solve_current_case() << '\n';
for (int tc = 2; tc <= T; tc++) {
int k;
cin >> k;
for (int i = 1; i <= k; i++) {
int x;
long long y;
cin >> x >> y;
a[x] = y;
}
cout << solve_current_case() << '\n';
}
return 0;
}满分做法要利用两个结论。
第一个结论:如果最强蛇 x 吃掉最弱蛇 y 后,新的 x-y 不是当前最弱蛇,那么 x 一定会吃。直观理解是:它吃完以后前面至少还有更弱的蛇替它挡刀;后面无论谁停手或继续吃,它都不会因为这一次操作立即变成最危险的目标。
第二个结论:如果 x-y 变成了最弱蛇,那么 x 是否敢吃,要看下一条最强蛇会不会吃它。下一条蛇如果吃完后也变成最弱蛇,又要继续看再下一条蛇。于是这一段是一个递归反推:
下一条蛇会吃 -> 当前蛇不敢吃
下一条蛇不吃 -> 当前蛇敢吃实现分成两步:
- 先不断执行第一类“必吃”操作,直到某次吃完会变成最弱蛇,或者只剩一条蛇;
- 对剩余局面做一次递归判断,看这一次“冒险吃”能不能发生。
因为输入保证体力值不降,原始蛇天然有序。吃出来的新蛇也可以用另一个双端队列维护。每次取全局最弱、最强时,只需要比较两个队列的队首或队尾。
代码
// main.cpp:双队列维护强弱顺序,先处理必吃局面,再递归判断冒险吃。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
struct Snake {
long long value;
int id;
int from_queue;
};
int T, n;
long long a[MAXN];
Snake q1[MAXN * 2], q2[MAXN * 2];
int l1, r1, l2, r2;
int eaten_count;
bool weaker_than(const Snake &a, const Snake &b) {
if (a.value != b.value) {
return a.value < b.value;
}
return a.id < b.id;
}
bool stronger_than(const Snake &a, const Snake &b) {
if (a.value != b.value) {
return a.value > b.value;
}
return a.id > b.id;
}
Snake get_min_snake() {
Snake result;
if (l1 <= r1 && l2 <= r2) {
if (weaker_than(q1[l1], q2[l2])) {
result = q1[l1++];
} else {
result = q2[l2++];
}
} else if (l1 <= r1) {
result = q1[l1++];
} else {
result = q2[l2++];
}
return result;
}
Snake get_max_snake() {
Snake result;
if (l1 <= r1 && l2 <= r2) {
if (stronger_than(q1[r1], q2[r2])) {
result = q1[r1--];
} else {
result = q2[r2--];
}
} else if (l1 <= r1) {
result = q1[r1--];
} else {
result = q2[r2--];
}
return result;
}
void push_back_original(const Snake &x) {
q1[++r1] = x;
}
void push_front_original(const Snake &x) {
q1[--l1] = x;
}
void push_back_new(const Snake &x) {
q2[++r2] = x;
}
void push_front_new(const Snake &x) {
q2[--l2] = x;
}
void restore_front(const Snake &x) {
if (x.from_queue == 1) {
push_front_original(x);
} else {
push_front_new(x);
}
}
void restore_back(const Snake &x) {
if (x.from_queue == 1) {
push_back_original(x);
} else {
push_back_new(x);
}
}
Snake make_after_eat(const Snake &strongest, const Snake &weakest) {
Snake result;
result.value = strongest.value - weakest.value;
result.id = strongest.id;
result.from_queue = 2;
return result;
}
// 判断 strongest 吃 weakest 后是否一定不是当前最弱蛇。
bool after_eat_not_weakest(const Snake &strongest, const Snake &weakest, const Snake &second_min) {
Snake changed = make_after_eat(strongest, weakest);
return stronger_than(changed, second_min);
}
void solve_forced_part() {
while (n - eaten_count > 1) {
Snake strongest = get_max_snake();
Snake weakest = get_min_snake();
Snake second_min = get_min_snake();
if (after_eat_not_weakest(strongest, weakest, second_min)) {
eaten_count++;
Snake changed = make_after_eat(strongest, weakest);
// 新蛇在这一阶段不会成为最弱;为了维持双队列顺序,先放新蛇,再还回第二弱蛇。
push_front_new(changed);
restore_front(second_min);
} else {
restore_back(strongest);
restore_front(second_min);
restore_front(weakest);
return;
}
}
}
bool can_eat_in_risky_part(int alive_count) {
if (alive_count <= 1) {
return false;
}
if (alive_count == 2) {
return true;
}
Snake strongest = get_max_snake();
Snake weakest = get_min_snake();
Snake second_min = get_min_snake();
if (after_eat_not_weakest(strongest, weakest, second_min)) {
return true;
}
Snake changed = make_after_eat(strongest, weakest);
restore_front(second_min);
push_front_new(changed);
// 如果下一条蛇会吃掉它,那么当前蛇就不能冒险;否则当前蛇可以吃。
return !can_eat_in_risky_part(alive_count - 1);
}
int solve_current_case() {
l1 = MAXN;
r1 = MAXN - 1;
l2 = MAXN;
r2 = MAXN - 1;
eaten_count = 0;
for (int i = 1; i <= n; i++) {
Snake x;
x.value = a[i];
x.id = i;
x.from_queue = 1;
push_back_original(x);
}
solve_forced_part();
if (can_eat_in_risky_part(n - eaten_count)) {
eaten_count++;
}
return n - eaten_count;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> T;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
cout << solve_current_case() << '\n';
for (int tc = 2; tc <= T; tc++) {
int k;
cin >> k;
for (int i = 1; i <= k; i++) {
int x;
long long y;
cin >> x >> y;
a[x] = y;
}
cout << solve_current_case() << '\n';
}
return 0;
}复杂度
每条蛇在一次测试数据中最多被取出、放入常数次。
单组时间复杂度为
总结
本题的难点不是维护最大最小,而是判断“最强蛇是否敢吃”。只要抓住“吃完不是最弱就必吃;吃完变最弱就递归看下一条蛇”的博弈结构,剩下就是双端队列模拟。
对拍时,brute.cpp 使用完整博弈递归搜索,可以验证小数据下双队列结论的实现是否正确。