[NOIP 2010 普及组] 三国游戏

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

把每个武将所在行的次大默契值看成他起手后能保住的最强搭档,再取所有次大值的最大者;同时用高边构成匹配证明小涵一定能赢。

OJ: luogu

题目 ID: P1199

难度:普及+/提高

标签:思维博弈图论构造最大次大值

日期: 2026-06-20 10:19

题意

n 个武将,任意两人之间都有一个互不相同的默契值。

小涵和计算机轮流选将,小涵先手。
计算机每次都会按题目指定的贪心策略,优先抢走最能破坏小涵下一步最强组合的那个自由武将。

所有武将被平分后,双方各自在自己军队里选出默契值最高的一对武将出战,默契值更大的一方获胜。

要求判断:

  1. 小涵是否一定能赢
  2. 如果能赢,在所有可能胜利结局中,小涵那对出战武将的最大默契值是多少

思路

先看一个可以完整模拟规则的小数据暴力:

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

const int MAXN = 20;

int n;
int w[MAXN][MAXN];
int all_mask;
map<long long, int> memo_win;
map<long long, int> memo_best;

// 计算某一方当前军队中的最强组合。
int best_pair_value(int mask) {
    int ans = -1000000000;
    for (int i = 0; i < n; i++) {
        if (((mask >> i) & 1) == 0) {
            continue;
        }
        for (int j = i + 1; j < n; j++) {
            if ((mask >> j) & 1) {
                ans = max(ans, w[i][j]);
            }
        }
    }
    return ans;
}

long long encode_state(int xmask, int cmask, int turn) {
    return ((long long) xmask << 21) | ((long long) cmask << 1) | turn;
}

pair<int, int> dfs(int xmask, int cmask, int turn) {
    long long key = encode_state(xmask, cmask, turn);
    if (memo_win.count(key)) {
        return make_pair(memo_win[key], memo_best[key]);
    }

    int free_mask = all_mask ^ xmask ^ cmask;
    if (free_mask == 0) {
        int x_best = best_pair_value(xmask);
        int c_best = best_pair_value(cmask);
        if (x_best > c_best) {
            memo_win[key] = 1;
            memo_best[key] = x_best;
        }
        else {
            memo_win[key] = 0;
            memo_best[key] = -1000000000;
        }
        return make_pair(memo_win[key], memo_best[key]);
    }

    if (turn == 0) {
        int can_win = 0;
        int best_value = -1000000000;
        for (int i = 0; i < n; i++) {
            if ((free_mask >> i) & 1) {
                pair<int, int> ret = dfs(xmask | (1 << i), cmask, 1);
                if (ret.first) {
                    can_win = 1;
                    best_value = max(best_value, ret.second);
                }
            }
        }
        memo_win[key] = can_win;
        memo_best[key] = best_value;
        return make_pair(can_win, best_value);
    }
    else {
        // 计算机的策略是:
        // 从“小涵已选武将”与“当前自由武将”的所有配对中,
        // 找到默契值最大的那一对,并抢走其中的自由武将。
        int choose = -1;
        int best_match = -1;
        for (int i = 0; i < n; i++) {
            if (((xmask >> i) & 1) == 0) {
                continue;
            }
            for (int j = 0; j < n; j++) {
                if ((free_mask >> j) & 1) {
                    if (w[i][j] > best_match) {
                        best_match = w[i][j];
                        choose = j;
                    }
                }
            }
        }
        pair<int, int> ret = dfs(xmask, cmask | (1 << choose), 0);
        memo_win[key] = ret.first;
        memo_best[key] = ret.second;
        return ret;
    }
}

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

    cin >> n;
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) {
            cin >> w[i][j];
            w[j][i] = w[i][j];
        }
    }

    all_mask = (1 << n) - 1;

    pair<int, int> ret = dfs(0, 0, 0);
    cout << ret.first << '\n';
    if (ret.first) {
        cout << ret.second << '\n';
    }

    return 0;
}

暴力会枚举小涵每一步选谁,计算机则按题意固定回应,最后比较双方最强二人组。 它只能用于很小的数据,但很适合帮我们验证结论。

第一步:固定第一手看会发生什么

假设小涵第一手选了武将 i

由于此时小涵军队里只有 i 一个人,所以计算机下一手一定会:

  • 在所有自由武将里找一个和 i 默契值最大的
  • 然后把它抢走

这就意味着:

  • i 最好的搭档一定被破坏
  • 小涵围绕 i 还能保住的最好搭档,只能是第二好的那个

所以对每个武将 i,只要看它这一行里的:

  • 最大值
  • 次大值

其中次大值,就是“小涵如果第一手拿 i,最终至少能保住的最好组合值”。

于是先得到一个候选答案:

ans = max(第 i 行次大值)

第二步:为什么这个值一定能赢

关键性质是:

  • 所有 严格大于 ans 的边,一定构成一个匹配

原因很简单:
如果某个点连出去有两条边都大于 ans,那么这个点所在行的次大值也会大于 ans,这和 ans 的定义矛盾。

既然这些高边构成匹配,那么在选将过程中:

  • 计算机一次最多只能拿走某条高边的一个端点
  • 小涵总能在自己的下一步把对应的另一个端点拿走

所以计算机最终不可能同时拥有某条高边的两个端点, 也就不可能形成默契值大于 ans 的组合。

而小涵只要从“次大值等于 ans”的那一行对应武将起手, 就能保证自己最后拿到一条默契值至少为 ans 的边。

因此:

  • 小涵一定能赢
  • 最大可保证值就是 ans

代码

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

const int MAXN = 505;

int n;
int w[MAXN][MAXN];

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

    cin >> n;
    for (int i = 1; i <= n; i++) {
        for (int j = i + 1; j <= n; j++) {
            cin >> w[i][j];
            w[j][i] = w[i][j];
        }
    }

    int answer = 0;

    // 对于每个武将 i:
    // 如果小涵第一步先拿 i,
    // 计算机一定会抢走和 i 默契值最大的那个武将。
    // 那么小涵以后能和 i 组成的最好搭档,只能是“这一行的次大值”对应的人。
    // 在所有起手武将里取最大的次大值,就是最终答案。
    for (int i = 1; i <= n; i++) {
        int first_max = 0;
        int second_max = 0;
        for (int j = 1; j <= n; j++) {
            if (i == j) {
                continue;
            }
            if (w[i][j] > first_max) {
                second_max = first_max;
                first_max = w[i][j];
            }
            else if (w[i][j] > second_max) {
                second_max = w[i][j];
            }
        }
        answer = max(answer, second_max);
    }

    cout << 1 << '\n';
    cout << answer << '\n';

    return 0;
}

复杂度

  • 时间复杂度:O(n2)O(n^2)
  • 空间复杂度:O(n2)O(n^2)

总结

这题表面是博弈,真正的关键却是一个很短的图论结论:

  1. 固定第一手后,计算机会破坏这一行最大值
  2. 所以这一行能保住的是次大值
  3. 所有超过全局答案的边会形成匹配,计算机拿不成完整的一条

所以最后答案就是:

  • 所有行的次大值中的最大值

一图流解析

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

一图流解析