传送门
Codeforces 429E Points and Segments
题解
先贴一个最开始写的,后来觉得有误的方法:欧拉回路。
对于这类黑白染色,且存在差值绝对值不超过 1 的问题,可以考虑欧拉回路求解。因为按照欧拉回路将无向图的边指定方向后,任意点的入度与出度相等;若图中存在度数为奇的点,这样的点只有偶数个,那么将其两两一组进行连边,求解欧拉回路后删边,此时,任意点的入度与出度差值的绝对值不超过 1。
考虑 X 轴上任意点,合法的染色方案需要满足,覆盖它的区间中,蓝色与红色的数量差不超过 1。将区间看作边,其染色看作对边的定向(向左或右),根据欧拉回路的性质,X 轴上任一点上,向左与向右的边数量相等。为了处理区间的边界情况(即保证区间拼接起来,可以看作是一个大区间),将其表示为左闭右开 [ l , r + 1 ) [l, r+1) [l,r+1),对于每一个区间 l → r + 1 l\rightarrow r+1 l→r+1 连一条边。对于奇数度的点,按照 X 轴顺序两两一组连边;因为若需要删除的边出现覆盖,绝对差值可能大于 1。
建图后求解欧拉回路即可。总时间复杂度 O ( n log n ) O(n\log n) O(nlogn),瓶颈在离散化上。
上述算法的问题在于,原始的区间是左闭右闭,而为了满足区间左闭右开的性质,将 [ l , r ] [l,r] [l,r] 表示为 [ l , r + 1 ) [l,r+1) [l,r+1)。此时 ( r , r + 1 ) (r,r+1) (r,r+1) 应该与 [ l , r ] [l,r] [l,r] 逻辑一致,即表示某一种区间的染色方案,但实际上 ( r , r + 1 ) (r,r+1) (r,r+1) 并没有区间覆盖。这就导致了在 ( r , r + 1 ) (r,r+1) (r,r+1) 中可能出现不满足条件的点。下面的样例,存在一种欧拉回路的构造,使之在 ( 3 , 4 ) (3,4) (3,4) 区间内的任一点都不满足条件。
7
4 6
4 5
1 3
2 3
1 1
1 5
1 6
故上述欧拉回路的做法,仅在下述约束下正确:原始区间为左闭右开;或只要求在整数点上满足条件。
#include <bits/stdc++.h>
using namespace std;
struct edge
{
int to, rev, id;
bool used;
};
int main()
{
ios::sync_with_stdio(0), cin.tie(0);
int N;
cin >> N;
vector<pair<int, int>> seg(N);
vector<int> xs;
for (int i = 0; i < N; ++i)
{
cin >> seg[i].first >> seg[i].second;
++seg[i].second;
xs.push_back(seg[i].first);
xs.push_back(seg[i].second);
}
sort(xs.begin(), xs.end());
xs.erase(unique(xs.begin(), xs.end()), xs.end());
for (auto &p : seg)
{
p.first = lower_bound(xs.begin(), xs.end(), p.first) - xs.begin();
p.second = lower_bound(xs.begin(), xs.end(), p.second) - xs.begin();
}
int V = xs.size();
vector<vector<edge>> G(V);
vector<int> deg(V);
for (int i = 0; i < N; ++i)
{
int u = seg[i].first, v = seg[i].second;
G[u].push_back({v, (int)G[v].size(), i, 0});
G[v].push_back({u, (int)G[u].size() - 1, i, 0});
++deg[u], ++deg[v];
}
vector<int> odd;
for (int v = 0; v < V; ++v)
{
if (deg[v] & 1)
odd.push_back(v);
}
assert(((int)odd.size() & 1) == 0);
for (int i = 0; i < (int)odd.size(); i += 2)
{
int u = odd[i], v = odd[i + 1];
G[u].push_back({v, (int)G[v].size(), -1, 0});
G[v].push_back({u, (int)G[u].size() - 1, -1, 0});
}
vector<int> res(N);
vector<int> iter(V);
function<void(int)> euler_path = [&](int v)
{
int n = G[v].size();
for (int &i = iter[v]; i < n; ++i)
{
while (i < n && G[v][i].used)
++i;
if (i == n)
break;
auto &e = G[v][i];
if (e.id != -1)
res[e.id] = e.to > v ? 1 : -1;
e.used = G[e.to][e.rev].used = 1;
euler_path(e.to);
}
};
for (int v = 0; v < V; ++v)
if (iter[v] == 0)
euler_path(v);
for (int i = 0; i < N; ++i)
cout << max(0, res[i]) << " \n"[i + 1 == N];
return 0;
}
如下是个人认为的正解:2-SAT
为了避免歧义,题中的 [ l , r ] [l,r] [l,r] 区间,下面称之为线段。
类似于扫描线的思想,将线段拆为左右端点,在 X 轴上依次向右考虑。每遇到一个端点
x
x
x,区间所覆盖线段数量的奇偶性改变;对于任一偶数区间,两种颜色染色数量相等。观察发现,在奇数区间,左右端点同时为线段左(或右)端点时,两个端点代表的线段所染颜色不同;反之,染色相同。定义
x
i
为真
⇔
线段
i
染为红色
x_i 为真 \Leftrightarrow 线段i染为红色
xi为真⇔线段i染为红色 转化为 2-SAT 问题。
上述方法基于一个假设,各个端点坐标互不相同。若出现端点相同的坐标,考虑这样一个问题:对区间某个点 x x x 操作 n n n 次,操作只有 ± 1 \pm 1 ±1 两种,求最终 x x x 上的值绝对值不超过 1 1 1 的方案。显然之前对于奇数区间端点两两配对的方法也是可行的。
最后的问题还是关于原始线段左闭右闭的性质。对于左端点,是右连续的;对于右端点,是左连续的。即,坐标为 x x x 的左端点,改变的区间是 [ x , ⋯ ) [x,\cdots) [x,⋯);而右端点是 ( x , ⋯ ) (x, \cdots) (x,⋯)。故相同坐标的左端点应优先于右端点被处理。若原始的区间就是左闭右开,则不用考虑这个问题,直接按照坐标排序即可。
分解强连通分量进行求解,总时间复杂度 O ( n log n ) O(n\log n) O(nlogn)。
#include <bits/stdc++.h>
using namespace std;
struct Node
{
int x, d, id;
};
int main()
{
ios::sync_with_stdio(0), cin.tie(0);
int N;
cin >> N;
vector<Node> ns;
for (int i = 0; i < N; ++i)
{
int l, r;
cin >> l >> r;
ns.push_back({l, 1, i});
ns.push_back({r, -1, i});
}
sort(ns.begin(), ns.end(), [&](auto &a, auto &b)
{
if (a.x != b.x)
return a.x < b.x;
return a.d > b.d;
});
int V = N * 2;
vector<vector<int>> G(V), rG(V);
auto add_edge = [&](int u, int v)
{ G[u].push_back(v), rG[v].push_back(u); };
for (int i = 0; i < V; i += 2)
{
auto &a = ns[i], &b = ns[i + 1];
if (a.d == b.d)
{
add_edge(a.id + N, b.id);
add_edge(b.id + N, a.id);
add_edge(a.id, b.id + N);
add_edge(b.id, a.id + N);
}
else
{
add_edge(a.id, b.id);
add_edge(b.id + N, a.id + N);
add_edge(a.id + N, b.id + N);
add_edge(b.id, a.id);
}
}
vector<int> scc(V);
auto find_scc = [&]()
{
vector<bool> used(V);
vector<int> vs;
function<void(int)> dfs = [&](int v)
{
used[v] = 1;
for (int u : G[v])
if (!used[u])
dfs(u);
vs.push_back(v);
};
function<void(int, int)> rdfs = [&](int v, int k)
{
used[v] = 1;
scc[v] = k;
for (int u : rG[v])
if (!used[u])
rdfs(u, k);
};
for (int v = 0; v < V; ++v)
if (!used[v])
dfs(v);
int k = 0;
fill(used.begin(), used.end(), 0);
for (int i = (int)vs.size() - 1; i >= 0; --i)
if (!used[vs[i]])
rdfs(vs[i], k++);
};
find_scc();
for (int i = 0; i < N; ++i)
cout << (scc[i] > scc[i + N] ? 1 : 0) << " \n"[i + 1 == N];
return 0;
}