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;
}
posted @ 2026-10-03 09:54  thenrify  阅读(11)  评论(0)    收藏  举报