Canvas Painting
Canvas Painting
涂色问题,连通性
出处: 25年ICPC亚洲区预选赛I
1. 问题转化:从涂色到选边
1.1 初始状态
- 画布有 n 个位置,每个位置颜色互不相同。
- 把每个位置看作一个独立的“连通块”(颜色相同的点的集合)。
- 初始连通块数 = n。
1.2 一次操作的效果
- 每次在区间 [l, r] 内选择两点 u, v,将 a_u 改为 a_v。
- 若 u, v 原本颜色不同(属于不同连通块),则 u 所在的整个连通块合并到 v 所在的连通块,连通块总数减少 1。
- 若 u, v 已同色,则操作无效,连通块数不变。
- 因此,一次操作最多贡献 1 次有效合并。
1.3 每个区间最多贡献一条边
- 每个区间(咒语)最多使用一次,所以每个区间最多提供一个操作机会。
- 只有区间内存在至少两个不同连通块时,这次操作才可能有效。
- 有效操作等价于在两个连通块之间连一条无向边。
1.4 有效合并必须构成森林
- 如果选出的边集中存在环,则环上最后一条边的两个端点在执行时早已同色,该操作无效。
- 因此,能真正减少连通块数的合并序列,其边集必须是无环的,即构成森林。
1.5 目标转化
- 设最终选出 E 条有效边,这些边构成森林。
- 森林的连通块数 = 顶点数 n - 边数 E。
- 最终颜色数 = 连通块数 = n - E。
- 要最小化颜色数,即最大化 E。
- 问题等价于:给定 m 个区间,每个区间最多选一条边(边的两个端点必须在区间内),要求选出的边无环,求最大边数 E。
2. 贪心策略
2.1 排序规则
- 将所有区间按右端点 r 从小到大排序。
- 若右端点相同,左端点的顺序不影响正确性(可任意,比如降序)。
2.2 核心思想
- 按排序后的顺序依次处理每个区间。
- 对于当前区间 [l, r],我们希望尽可能用它连一条边。
- 在区间内选择最左的“活点” u 和最右的“活点” v。
- 若 u < v,则用该区间连边 (u, v),边数 +1,并“删除” u(表示 u 已与 v 合并,不再作为独立连通块的入口)。
- 若 u >= v,说明区间内不足两个不同的活点,无法连边,跳过。
2.3 为什么删除最左点?
- 因为区间按右端点排序,后续区间的右端点只会更大(即整体偏右)。
- 删除左端点 u,保留右端点 v,相当于将合并后的连通块的代表点向右移动,使得后续区间更容易覆盖到该连通块,从而增加未来的合并机会。
- 这是由“交换论证”可证明的贪心最优策略。
2.4 数据结构:活点的连续区间块
- 由于 n 可达 1e9,不能逐个存储点。
- 用 set 存储所有连续活点区间 [L, R](闭区间),区间之间不相交且按 L 递增。
- 初始时只有一个区间 [1, n]。
- 每次删除一个点 x,将其所在区间拆分为 [L, x-1] 和 [x+1, R](若非空)。
3. 具体实现细节
3.1 查找最左活点 u
- 用
blocks.lower_bound({l, -1})找到第一个左端点 >= l 的块。 - 如果前一个块存在且其右端点 >= l,则实际相交块是前一个块(因为前一个块覆盖了 l)。
- 令
u = max(l, it->first)。
3.2 查找最右活点 v
- 用
blocks.upper_bound({r, LLONG_MAX})找到第一个左端点 > r 的块。 - 回退一步得到最后一个左端点 <= r 的块。
- 若该块的右端点 >= l,则
v = min(r, it->second)。
3.3 删除点 u
- 找到包含 u 的块 [L, R]。
- 从 set 中删除该块。
- 若 L <= u-1,插入 [L, u-1]。
- 若 u+1 <= R,插入 [u+1, R]。
3.4 区间处理的两种分支(利用排序性质简化)
由于按右端点升序处理,当前区间的右侧点一定尚未被删除(因为删除右侧点需要用到更大的右端点,还未处理)。因此:
- 分支1:
it->first >= l(最左活点所在的块完全落在区间内)- u = it->first,v = min(r, it2->second)。
- 若 u == v,则区间内只有一个活点,跳过。
- 否则删除 u。
- 分支2:
it->first < l(最左活点所在的块覆盖了 l)- 此时 u = l,且因为 l < r(若 l == r 直接跳过),区间内必有至少两个活点(l 和 r 都在该块内或右侧块内),所以无需判断 u == v,直接删除 l。
4. 算法复杂度
- 排序:O(m log m)
- 每个区间处理:O(log m)(set 的 lower_bound/upper_bound 及插入删除)
- 总时间复杂度:O(m log m)
- 空间复杂度:O(m)(活点块数量最多 2m+1)
5. 特殊情况处理
- 区间 [l, r] 中 l == r:内部只有一个点,永远不可能连边,直接跳过。
- 重复区间:处理第一次可能成功,后续因活点减少而自动失效(u >= v 或活点不足)。
- 区间内无活点:由查找逻辑中的条件(it == end() 或 it->first > r)自动跳过。
6. 正确性证明要点(简述)
- 无环性:每次只删除左端点 u,并将 u 与 v 合并,v 保留。删除的 u 不会再出现,因此不会形成环(等价于每次将点归并到其右侧,类似于并查集按秩合并)。
- 最优性:按右端点排序的贪心是经典的区间调度思想。通过交换论证,若存在最优解在某区间未选最左点,可调整为选最左点而不减少总边数,因此贪心得到的是最优解。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pll = pair<ll, ll>;
using vvi = vector<vector<int>>;
void solve() {
int m; ll n; cin >> m >> n;
vector<pll> p(m);
for(int i = 0; i < m; i ++) cin >> p[i].first >> p[i].second;
sort(p.begin(), p.end(), [](const pll& a, const pll& b) {
if(a.second != b.second) return a.second < b.second;
return a.first > b.first;
});
ll ans = 0;
set<pll> st; st.insert({1, n});
for(auto&[l, r] : p) {
if(l == r) continue;
auto it = st.lower_bound({l, -1});
if(it != st.begin()) {
auto preit = prev(it);
if(preit->second >= l) it = preit;
}
//情况1 R(i-1) < l <= Li it = i
//情况2 L(i-1) < l <= R(i-1) it = i-1
if(it == st.end() || it->first > r) continue;
auto it2 = st.upper_bound({r, LLONG_MAX});//it2为严格大于l r 的区间
if(it2 == st.begin()) continue;
it2 --;
if(it->first >= l) {
ll v = min((ll)r, it2->second);
if(it->first == v) continue;
ll L = it->first, R = it->second;
st.erase(it);
if(L + 1 <= R) st.insert({L + 1, R});
}
else {
ll L = it->first, R = it->second;
st.erase(it);
if(L <= l - 1) st.insert({L, l - 1});
if(l + 1 <= R) st.insert({l + 1, R});
}
ans ++;
}
cout << n - ans << "\n";
}
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
int _=1; cin >> _;
while(_--) solve();
return 0;
}

浙公网安备 33010602011771号