[CSP-S 2020] 贪吃蛇

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

用双队列模拟强弱顺序,先处理必吃局面,再用递归反推第一次冒险吃是否成立。

OJ: luogu

题目 ID: P7078

难度:提高+/省选-

标签:贪心博弈双端队列模拟

日期: 2026-07-06 08:46

题意

n 条蛇,每条蛇有体力值和编号。比较强弱时,体力值大的更强;体力值相同则编号大的更强。

每一轮由当前最强蛇决定是否吃掉当前最弱蛇。如果吃,它的体力值减去最弱蛇的体力值,最弱蛇退出;如果不吃,游戏立即结束。每条蛇最优先保证自己不被吃,在能保证存活时才希望多吃蛇。

要求输出最终剩余蛇的数量。第一组数据给出完整体力值,后续数据在上一组基础上做若干单点修改。

思路

小数据可以直接做博弈搜索:当前最强蛇先假设自己吃掉最弱蛇,然后递归看它在后续游戏中是否还能存活。如果能存活,它就会吃;否则它会停手。

cpp
// 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 是否敢吃,要看下一条最强蛇会不会吃它。下一条蛇如果吃完后也变成最弱蛇,又要继续看再下一条蛇。于是这一段是一个递归反推:

text
下一条蛇会吃  -> 当前蛇不敢吃
下一条蛇不吃  -> 当前蛇敢吃

实现分成两步:

  1. 先不断执行第一类“必吃”操作,直到某次吃完会变成最弱蛇,或者只剩一条蛇;
  2. 对剩余局面做一次递归判断,看这一次“冒险吃”能不能发生。

因为输入保证体力值不降,原始蛇天然有序。吃出来的新蛇也可以用另一个双端队列维护。每次取全局最弱、最强时,只需要比较两个队列的队首或队尾。

代码

cpp
// 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;
}

复杂度

每条蛇在一次测试数据中最多被取出、放入常数次。

单组时间复杂度为 O(n)O(n),空间复杂度为 O(n)O(n)。多组数据在修改体力值后重新计算,整体为 O(Tn+修改数)O(Tn + 修改数)

总结

本题的难点不是维护最大最小,而是判断“最强蛇是否敢吃”。只要抓住“吃完不是最弱就必吃;吃完变最弱就递归看下一条蛇”的博弈结构,剩下就是双端队列模拟。

对拍时,brute.cpp 使用完整博弈递归搜索,可以验证小数据下双队列结论的实现是否正确。