P13446 [GCJ 2009 #3] Football Team 题解

P13446 [GCJ 2009 #3] Football Team 题解

题目链接

打模拟赛被此题单杀了,刚开始还以为是水蓝呢/fn。主要是考场上想到了性质一,感觉稳了,结果T了……感觉印象挺深刻的,来写篇题解纪念一下。

I. 题意

有 \(N\) 名球员,每人位置为 \((x,y)\),其中 \(y\) 表示排号,\(x\) 表示该排内的横坐标,所有 \(x\) 互不相同。
若存在两名球员 \((x_1,y_1)\) 和 \((x_2,y_2)\),满足 \(x_1 < x_2\),且 \(y_1-1 \le y_2 \le y_1+1\),并且不存在 \(x_3\) 使得在 \((x_3,y_2)\) 有球员且 \(x_1 < x_3 < x_2\),则这两名球员颜色必须不同。

II. 大致方向

尝试往图论方面想,就是把每个球员看成顶点,在两个颜色不同的球员之间连一条边,约束转化成无向图的染色问题,问题就转化为求这个无向图的最小染色数。

III. 思路

1. 建图:只连“最近右邻”

对于每个球员 \(X=(x,y)\),在 \(y-1, y, y+1\) 三行中分别找横坐标大于 \(x\) 的最小者,连一条无向边。
每条边只由左端点生成一次,因此每个点最多产生 \(3\) 条边,总边数 \(E \le 3N\)。

注意:行号必须保留真实数值,不能把空行压缩掉(离散化),因为只有行号差 \(1\) 才算相邻行。

2. 重要性质 1

按 \(x\) 从大到小处理球员,每个点最多有 \(3\) 个已处理的邻点,因此总可以从 \(4\) 种颜色中选一种,所以答案 \(\le 4\)。

  • 若 \(E=0\),色数为 \(1\)。
  • 若有边但无环(森林),图是二分图,色数为 \(2\)。
  • 若有环,则至少需要 \(3\) 色(本题特有的平面结构保证有环时必含三角形,三角形需要 \(3\) 色)。
  • 剩下只需判断是否需要第 \(4\) 色。

我赛时就是发现这个性质,考虑如何判断答案是否为 \(1, 2, 3, 4\)。
显然的,若没有边,则色数为 \(1\)。
若为二分图,则色数为 \(2\)。
之后 dfs 贪心染色求答案。

赛时代码

/*
author: Nimbunny
powered by c++14
*/

#include <bits/stdc++.h>
#define endl '\n'
#define pi pair<int, int>
// #define int long long
// #pragma GCC optimize(2)

using namespace std;
bool Mbe;
const double eps = 1e-6;
const int inf = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const int N = 1e3 + 10;
int n, id[N], a[N];
vector<int> v[40];
int col[N];

int pre[N], k;
struct node {
    int to, next;
} e[N << 2];

inline void add(int u, int v) {
    e[++k] = {v, pre[u]};
    pre[u] = k;
    return;
}

inline void init() {
    memset(pre, 0, sizeof pre);
    k = 0;
    for (int i = 1; i <= 30; i++) v[i].clear();
    memset(col, 0, sizeof col);
    return;
}

vector<int> order;

bool dfs(int idx) {
    if (idx == n) return true;
    int u = order[idx];
    bool used[5] = {false};
    for (int i = pre[u]; i; i = e[i].next) {
        int v = e[i].to;
        if (col[v]) used[col[v]] = true;
    }
    for (int c = 1; c <= 3; c++) {
        if (!used[c]) {
            col[u] = c;
            if (dfs(idx + 1)) return true;
            col[u] = 0;
        }
    }
    return false;
}

inline int solve() {
    init();
    cin >> n;
    for (int i = 1, x, y; i <= n; i++) {
        cin >> x >> y;
        v[y].push_back(x);
        id[x] = i;
        a[i] = x;
    }
    for (int i = 1; i <= 30; i++) sort(v[i].begin(), v[i].end());

    for (int i = 1; i <= 30; i++) {
        for (int x : v[i]) {
            int u = id[x];
            if (i != 1) {
                auto pos = upper_bound(v[i - 1].begin(), v[i - 1].end(), x);
                if (pos != v[i - 1].end()) add(u, id[*pos]);
            }
            {
                auto pos = upper_bound(v[i].begin(), v[i].end(), x);
                if (pos != v[i].end()) add(u, id[*pos]);
            }
            if (i != 30) {
                auto pos = upper_bound(v[i + 1].begin(), v[i + 1].end(), x);
                if (pos != v[i + 1].end()) add(u, id[*pos]);
            }
        }
    }

    if (k == 0) return 1;

    vector<int> adj[N];
    for (int u = 1; u <= n; u++) {
        for (int i = pre[u]; i; i = e[i].next) {
            int v = e[i].to;
            adj[u].push_back(v);
            adj[v].push_back(u);
        }
    }
    int f[N];
    memset(f, 0xff, sizeof f);
    bool flag = true;
    for (int i = 1; i <= n; i++) {
        if (f[i] != -1) continue;
        queue<int> q;
        q.push(i);
        f[i] = 0;
        while (!q.empty() && flag) {
            int u = q.front();
            q.pop();
            for (int v : adj[u]) {
                if (f[v] == -1) {
                    f[v] = f[u] ^ 1;
                    q.push(v);
                } else if (f[v] == f[u]) {
                    flag = false;
                    break;
                }
            }
        }
    }
    if (flag) return 2;

    order.clear();
    for (int i = 1; i <= n; i++) order.push_back(i);
    sort(order.begin(), order.end(), [&](int u, int v) { return a[u] > a[v]; });

    if (dfs(0)) return 3;
    return 4;
}

signed main() {
    // freopen("jersey.in", "r", stdin);
    // freopen("jersey.out", "w", stdout);
    cin.tie(nullptr)->sync_with_stdio(false);
    int _;
    cin >> _;
    for (int tc = 1; tc <= _; tc++) cout << "Case #" << tc << ": " << solve() << endl;
    return 0;
}

显然是 TLE 了。

3. 重要性质 2

我们可以尝试找找答案为 \(3\) 的图的特点。

对于样例:

6
12 3
4 2
2 2
5 1
3 1
7 2

样例帮助我们破案:如果存在一个内部点,周围的点是奇环,那么该图一定不能用 \(3\) 中颜色染色。证明显然。

更严谨地,将行视为水平直线,按坐标连接边。可以证明这种画法无交叉(平面图),且所有有界面都是三角形。证明用反证法。
对于所有有界面为三角形的平面图,有以下结论:

图可三染色,当且仅当每个内部顶点的度数为偶数。

内部顶点:不在无界面边界上的顶点。

球员 \(X\) 是内部顶点,当且仅当它的同行最近左邻 \(L\)、同行最近右邻 \(R\)、上一行最近右邻 \(U\)、下一行最近右邻 \(D\) 都存在。

除了上下左右 \(4\) 个相邻的顶点,还有一些隐藏顶点。如下图:

(红点:\(X\) 为某内部顶点;黄点:\(L\) 为它的同行最近左邻、\(R\) 同行最近右邻、\(U\) 上一行最近右邻、\(D\) 下一行最近右邻;绿边:已知的边;粉边:可能有可能无的边;橙点:\(X^{'}\) 是隐藏节点。)

若四者齐全,设 \(L\) 的横坐标为 \(x_L\),定义

\[k = \#\{(x', y-1) \mid x_L < x' < x_X\} + \#\{(x', y+1) \mid x_L < x' < x_X\} \]

即上下两行中横坐标严格在 \((x_L, x_X)\) 之间的球员数量。
则内部顶点 \(X\) 的度数为

\[\deg(X) = k + 4 \]

\(\deg(X)\) 与 \(k\) 同奇同偶,因此 \(\deg(X)\) 为奇数当且仅当 \(k\) 为奇数。

若存在内部顶点度数为奇数,则无法三染色,需要 \(4\) 色;否则可以三染色。

IV. 具体做法

  1. 建图
    遍历每一行的每个球员 \(X=(x,y)\),在 \(y-1, y, y+1\) 三行中用 upper_bound(x) 找到第一个大于 \(x\) 的横坐标,连无向边。
    每条边只加一次,最终得到邻接表。

  2. 判断答案 1 和 2

    • 若边数 \(E=0\),答案为 \(1\)。
    • 用 BFS 对图进行二分图染色。若能成功染色,说明无环,答案为 \(2\)。
  3. 判断答案 3 或 4
    遍历所有球员,对每个球员 \(X=(x,y)\):

    • 找同行最近左邻 \(L\):在 v[y] 中 lower_bound(x) 的前一个元素。
    • 找同行最近右邻 \(R\):在 v[y] 中 upper_bound(x) 的第一个元素。
    • 找上一行最近右邻 \(U\):在 v[y-1] 中 upper_bound(x) 的第一个元素。
    • 找下一行最近右邻 \(D\):在 v[y+1] 中 upper_bound(x) 的第一个元素。

    若 \(L,R,U,D\) 都存在,则计算

    \[k = \big(\text{lower\_bound}(v_{y-1}, x) - \text{upper\_bound}(v_{y-1}, x_L)\big) + \big(\text{lower\_bound}(v_{y+1}, x) - \text{upper\_bound}(v_{y+1}, x_L)\big)\]

    其中 \(x_L\) 是 \(L\) 的横坐标。
    若存在某个 \(k\) 为奇数,则答案为 \(4\);否则答案为 \(3\)。

V. 复杂度

  • 排序与建图:每行排序 \(\mathcal{O}(n \log n)\),每个点三次二分查找 \(\mathcal{O}(n \log n)\)。
  • 二分图染色:\(\mathcal{O}(n+e), e \le 3n\)。
  • 内部顶点判定:每个点常数次二分查找,\(\mathcal{O}(n \log n)\)。

单组总时间复杂度为 \(\mathcal{O}(n \log n)\),空间复杂度为 \(\mathcal{O}(n)\)。
多组总时间为 \(\mathcal{O}(\sum n_i \log n_i)\)。

VI. 坑点

  • 容易将信息离散化。
  • 容易将横坐标和编号混淆。

VII. 代码

/*
author: Nimbunny
powered by c++14
*/

#include <bits/stdc++.h>
#define endl '\n'
#define pi pair<int, int>
// #define int long long
// #pragma GCC optimize(2)

using namespace std;
bool Mbe;
const double eps = 1e-6;
const int inf = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const int N = 1e3 + 10;
int n, id[N], a[N], ay[N];
vector<int> v[40];
int col[N];
bool vis[N];
bool F;

int pre[N], k;
struct node {
    int to, next;
} e[N << 2];

inline void add(int u, int v) {
    e[++k] = {v, pre[u]};
    pre[u] = k;
    return;
}

inline void init() {
    memset(pre, 0, sizeof pre);
    k = 0;
    for (int i = 1; i <= 30; i++) v[i].clear();
    memset(col, 0, sizeof col);
    memset(vis, false, sizeof vis);
    return;
}

inline int solve() {
    init();
    cin >> n;
    for (int i = 1, x, y; i <= n; i++) {
        cin >> x >> y;
        v[y].push_back(x);
        id[x] = i;
        a[i] = x;
        ay[i] = y;
    }
    for (int i = 1; i <= 30; i++) sort(v[i].begin(), v[i].end());

    for (int i = 1; i <= 30; i++) {
        for (int x : v[i]) {
            int u = id[x];
            if (i != 1) {
                auto pos = upper_bound(v[i - 1].begin(), v[i - 1].end(), x);
                if (pos != v[i - 1].end()) add(u, id[*pos]);
            }
            {
                auto pos = upper_bound(v[i].begin(), v[i].end(), x);
                if (pos != v[i].end()) add(u, id[*pos]);
            }
            if (i != 30) {
                auto pos = upper_bound(v[i + 1].begin(), v[i + 1].end(), x);
                if (pos != v[i + 1].end()) add(u, id[*pos]);
            }
        }
    }

    if (k == 0) return 1;

    vector<int> adj[N];
    for (int u = 1; u <= n; u++) {
        for (int i = pre[u]; i; i = e[i].next) {
            int to = e[i].to;
            adj[u].push_back(to);
            adj[to].push_back(u);
        }
    }
    int f[N];
    memset(f, 0xff, sizeof f);
    bool flag = true;
    for (int i = 1; i <= n; i++) {
        if (f[i] != -1) continue;
        queue<int> q;
        q.push(i);
        f[i] = 0;
        while (!q.empty() && flag) {
            int u = q.front();
            q.pop();
            for (int to : adj[u]) {
                if (f[to] == -1) {
                    f[to] = f[u] ^ 1;
                    q.push(to);
                } else if (f[to] == f[u]) {
                    flag = false;
                    break;
                }
            }
        }
    }
    if (flag) return 2;

    // 三色/四色判定
    for (int u = 1; u <= n; u++) {
        int x = a[u], y = ay[u];
        // 找 L: 同行小于 x 的最大值
        auto itL = lower_bound(v[y].begin(), v[y].end(), x);
        if (itL == v[y].begin()) continue;
        int Lx = *(--itL);
        // 找 R: 同行大于 x 的最小值
        auto itR = upper_bound(v[y].begin(), v[y].end(), x);
        if (itR == v[y].end()) continue;
        // 找 U: y-1 行大于 x 的最小值
        if (y - 1 < 1) continue;
        auto itU = upper_bound(v[y - 1].begin(), v[y - 1].end(), x);
        if (itU == v[y - 1].end()) continue;
        // 找 D: y+1 行大于 x 的最小值
        if (y + 1 > 30) continue;
        auto itD = upper_bound(v[y + 1].begin(), v[y + 1].end(), x);
        if (itD == v[y + 1].end()) continue;
        // 计算 k
        int kcnt = 0;
        kcnt += lower_bound(v[y - 1].begin(), v[y - 1].end(), x) -
                upper_bound(v[y - 1].begin(), v[y - 1].end(), Lx);
        kcnt += lower_bound(v[y + 1].begin(), v[y + 1].end(), x) -
                upper_bound(v[y + 1].begin(), v[y + 1].end(), Lx);
        if (kcnt & 1) return 4;
    }
    return 3;
}

signed main() {
    // freopen("jersey.in", "r", stdin);
    // freopen("jersey.out", "w", stdout);
    cin.tie(nullptr)->sync_with_stdio(false);
    int _;
    cin >> _;
    for (int tc = 1; tc <= _; tc++) cout << "Case #" << tc << ": " << solve() << endl;
    return 0;
}
posted @ 2026-10-02 22:51  酱云兔  阅读(12)  评论(0)    收藏  举报