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\),定义
即上下两行中横坐标严格在 \((x_L, x_X)\) 之间的球员数量。
则内部顶点 \(X\) 的度数为
\(\deg(X)\) 与 \(k\) 同奇同偶,因此 \(\deg(X)\) 为奇数当且仅当 \(k\) 为奇数。
若存在内部顶点度数为奇数,则无法三染色,需要 \(4\) 色;否则可以三染色。
IV. 具体做法
-
建图
遍历每一行的每个球员 \(X=(x,y)\),在 \(y-1, y, y+1\) 三行中用upper_bound(x)找到第一个大于 \(x\) 的横坐标,连无向边。
每条边只加一次,最终得到邻接表。 -
判断答案 1 和 2
- 若边数 \(E=0\),答案为 \(1\)。
- 用 BFS 对图进行二分图染色。若能成功染色,说明无环,答案为 \(2\)。
-
判断答案 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\)。 - 找同行最近左邻 \(L\):在
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;
}

浙公网安备 33010602011771号