用双队列维护强弱顺序,先处理必吃局面,再递归判断冒险吃。
OJ: luogu
题目 ID: P7078
难度:提高+/省选-
标签:贪心博弈双端队列模拟
创建: 2026-07-06 08:46
更新: 2026-10-07 10:45
目录
形式化题目
有
决斗进行若干轮。每轮由当前最强蛇决定是否吃掉当前最弱蛇:若吃,最强蛇的体力值减去最弱蛇的体力值,最弱蛇退出,决斗继续;若不吃,决斗立刻结束。每条蛇的目标是:先保证自己不被吃,在此前提下尽量多吃别的蛇。
求决斗结束时存活蛇的数量。有多组测试数据,每组在上一组的体力值上做若干单点修改后重新求解。
解法路线
题面按数据规模给分:
- 暴力解法(20/40 分档):完全按题面规则递归模拟决斗,不加任何结论。
- 子任务解法(55/70 分档):证明两条贪心结论,把"敢不敢吃"变成一条递归判断链,用
std::set维护强弱顺序。 - 正解(100 分档):复用同样的贪心结论,用双队列代替有序集合,把单组复杂度降到
。
后一层直接复用前一层的观察,依次消除的瓶颈是"暴力搜索"和"取最大最小的容器开销"。正式主解是 main.cpp。
暴力解法:直接模拟决斗(20/40 分档)
适用范围
思路
直接翻译题面规则:当前最强蛇先假设自己吃掉最弱蛇,然后递归看它在后续游戏中是否还能存活。如果能存活,它就会吃(多吃一条总是更优);否则它会停手,决斗结束。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-01 22:33
* update_at: 2026-10-01 22:33
*/
// subtask_40.cpp:20/40 分档(n <= 10)解法。完全按题面规则递归模拟决斗:
// 最强蛇若吃掉最弱蛇后自己仍能存活到最后就吃,否则选择不吃、决斗立刻结束。
// 不依赖任何贪心结论,直接翻译题面规则,适合小数据。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 15;
struct Snake {
ll value;
int id;
};
int T, n;
ll a[MAXN]; // a[i]:第 i 条蛇的体力值
// 比较两条蛇的强弱:体力值大者强,相同则编号大者强。
// 参数用 x/y,避开全局体力值数组 a[]。
bool weaker_than(const Snake &x, const Snake &y) {
if (x.value != y.value) {
return x.value < y.value;
}
return x.id < y.id;
}
// 判断编号为 id 的蛇是否在存活集合中。
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);
if ((int)state.size() == 1) {
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)) {
return alive_after_eat; // 吃了能活到最后,就吃
}
return state; // 吃了会死,选择不吃,决斗结束
}
// 求解单组测试数据,返回最终存活蛇的数量。
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);
}
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;
ll y;
cin >> x >> y;
a[x] = y;
}
cout << solve_current_case() << '\n';
}
return 0;
}对拍时使用等价的带记忆化实现 brute.cpp,两者决策逻辑相同。
复杂度与瓶颈
每层递归排序一次当前存活集合,递归深度
子任务解法:贪心结论 + 有序集合(55/70 分档)
适用范围
std::set 的对数常数偏大。
思路
这一层的关键是两条贪心结论。
第一个结论:如果最强蛇
第二个结论:如果
下一条蛇会吃 -> 当前蛇不敢吃
下一条蛇不吃 -> 当前蛇敢吃实现分成两步:先不断执行第一类"必吃"操作,直到某次吃完会变成最弱蛇,或者只剩一条蛇;再对剩余局面做一次递归判断,看这一次"冒险吃"能不能发生。
容器用 std::set 存所有存活蛇,(体力值, 编号) 升序即从弱到强,取最大最小都是
注意一个边界:只剩两条蛇时,最强吃最弱后独自存活,必吃,不需要取"第二弱"。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-01 22:33
* update_at: 2026-10-01 22:33
*/
// subtask_55.cpp:55/70 分档解法。先用两条贪心结论把"敢不敢吃"理清
// (吃完不是最弱必吃;吃完变最弱则递归看下一条蛇敢不敢吃),
// 再用 std::set 维护强弱顺序,单组 O(n log n)。n <= 1e6 时建议换双队列 O(n)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// (体力值, 编号) 升序即从弱到强:体力值小者弱,相同则编号小者弱。
typedef pair<ll, int> Snake;
int T, n;
ll a[1000005]; // a[i]:第 i 条蛇的体力值
set<Snake> alive; // 当前存活的蛇,begin() 最弱,rbegin() 最强
int eaten_count; // 已经被吃掉的蛇的数量
// 取出当前最弱蛇。
Snake get_min_snake() {
Snake result = *alive.begin();
alive.erase(alive.begin());
return result;
}
// 取出当前最强蛇。
Snake get_max_snake() {
set<Snake>::iterator it = alive.end();
it--;
Snake result = *it;
alive.erase(it);
return result;
}
// 最强蛇吃掉最弱蛇后生成的新蛇:体力值相减,编号保留。
Snake make_after_eat(const Snake &strongest, const Snake &weakest) {
return make_pair(strongest.first - weakest.first, strongest.second);
}
// 判断 strongest 吃 weakest 后,新蛇是否一定不是当前最弱蛇(即可安全吃)。
bool after_eat_not_weakest(const Snake &strongest, const Snake &weakest, const Snake &second_min) {
return make_after_eat(strongest, weakest) > second_min;
}
// 第一阶段:处理所有"必吃"局面。
// 只剩两条时不进循环(见 solve_current_case 的特判),保证取第二弱时集合非空。
void solve_forced_part() {
while ((int)alive.size() > 2) {
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++;
alive.insert(make_after_eat(strongest, weakest));
alive.insert(second_min);
} else {
// 吃完会变最弱:把取出来的三条还回去,交给冒险阶段递归判断。
alive.insert(strongest);
alive.insert(second_min);
alive.insert(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;
}
alive.insert(second_min);
alive.insert(make_after_eat(strongest, weakest));
return !can_eat_in_risky_part(alive_count - 1);
}
// 求解单组测试数据,返回最终存活蛇的数量。
int solve_current_case() {
alive.clear();
eaten_count = 0;
for (int i = 1; i <= n; i++) {
alive.insert(make_pair(a[i], i));
}
solve_forced_part();
int alive_count = (int)alive.size();
if (alive_count == 2) {
// 只剩两条:最强吃最弱后独自存活,必吃。
eaten_count++;
} else if (can_eat_in_risky_part(alive_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;
ll y;
cin >> x >> y;
a[x] = y;
}
cout << solve_current_case() << '\n';
}
return 0;
}复杂度与瓶颈
必吃阶段每条蛇进出集合常数次,
正解:双队列线性扫描(100 分档)
关键观察
输入保证
思路
贪心结论与上一层完全相同,只换容器:原始蛇放队列 1,吃出来的新蛇放队列 2,两个队列都按从弱到强排列。
- 取最弱:比较两队列队首,取较弱者;
- 取最强:比较两队列队尾,取较强者;
- 吃完生成的新蛇放队列 2 的弱端,被还回的第二弱蛇放回原队列弱端。
因为每次都在当前约束下做最优决策,前面数字的最优选择不会被后面破坏。每条蛇在一次测试数据中最多被取出、放入常数次,单组
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-01 21:11
* update_at: 2026-10-01 22:33
*/
// main.cpp:双队列维护强弱顺序,先处理必吃局面,再递归判断冒险吃。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 1000005;
struct Snake {
ll value;
int id;
int from_queue;
};
int T, n;
ll a[MAXN];
Snake q1[MAXN * 2], q2[MAXN * 2];
int l1, r1, l2, r2;
int eaten_count;
// 比较两条蛇的强弱:体力值大者强,相同则编号大者强。
// 参数用 x/y,避开全局体力值数组 a[]。
bool weaker_than(const Snake &x, const Snake &y) {
if (x.value != y.value) {
return x.value < y.value;
}
return x.id < y.id;
}
bool stronger_than(const Snake &x, const Snake &y) {
if (x.value != y.value) {
return x.value > y.value;
}
return x.id > y.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);
}
// 第一阶段:处理所有"必吃"局面。
// 若最强蛇吃完后不是最弱,则它一定会吃,继续循环。
// 只剩两条时不进循环(见 solve_current_case 的特判),保证取第二弱时队列非空。
void solve_forced_part() {
while (n - eaten_count > 2) {
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();
int alive_count = n - eaten_count;
if (alive_count == 2) {
// 只剩两条:最强吃最弱后独自存活,必吃。
eaten_count++;
} else if (can_eat_in_risky_part(alive_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;
ll y;
cin >> x >> y;
a[x] = y;
}
cout << solve_current_case() << '\n';
}
return 0;
}复杂度
- 时间复杂度:单组
。多组数据在修改体力值后重新计算,整体为 。 - 空间复杂度:
,两个双端队列各开 的容量。
总结
本题的难点不是维护最大最小,而是判断"最强蛇是否敢吃"。只要抓住"吃完不是最弱就必吃;吃完变最弱就递归看下一条蛇"的博弈结构,剩下就是容器选型:std::set 足够 55/70 分档,双端队列利用输入有序性把 100 分档做到
对拍时,brute.cpp 使用完整博弈递归搜索,可以验证小数据下贪心结论的实现是否正确。