1 条题解

  • 0
    @ 2026-4-15 18:54:30

    C. 双重视角 —— 详细题解


    题目重述

    给定 nn 个不同的区间(对)(ai,bi)(a_i, b_i),其中 1ai<bi2n1 \le a_i < b_i \le 2n。定义:

    • f(S)f(S):将每个对视为数轴上的区间,f(S)f(S) 是这些区间并集的长度(即被覆盖的单位线段 [x,x+1][x, x+1] 的个数)。
    • g(S)g(S):将每个对视为图中的一条无向边,g(S)g(S) 是位于至少一个长度 3\ge 3 的简单环上的节点个数。

    需要选择一个子集 SS',使得 f(S)g(S)f(S') - g(S') 最大,并输出所选对的下标。

    数据范围:

    • 1n3×1031 \le n \le 3 \times 10^3
    • 1ai<bi2n1 \le a_i < b_i \le 2n
    • 所有 n2n^2 之和 9×106\le 9 \times 10^6
    • t104t \le 10^4

    关键观察

    观察 1:区间图的性质

    把每个区间 [ai,bi][a_i, b_i] 看作一条连接 aia_ibib_i 的边。由于 ai<bia_i < b_i 且所有端点都是 112n2n 的整数,这是一个区间图(interval graph)。

    区间图有一个重要性质:图中的环必然由区间包含关系产生

    具体来说:

    • 如果三个区间 I1,I2,I3I_1, I_2, I_3 满足 I1I2I3I_1 \subseteq I_2 \subseteq I_3,则它们会在图中形成一个三角形(三元环)。
    • 更一般地,任何长度 3\ge 3 的环都对应一个嵌套区间链

    观察 2:f(S)f(S)g(S)g(S) 的关系

    • 每添加一个新区间,f(S)f(S) 可能增加(如果新区间覆盖了之前未覆盖的区域)
    • 但如果新区间被已有区间完全包含,则 f(S)f(S) 不变,但可能在图中引入环(增加 g(S)g(S)
    • 因此,为了最大化 f(S)g(S)f(S) - g(S),我们应该避免选择被完全包含的区间

    观察 3:贪心策略

    按左端点排序后,可以用一个栈维护当前选择的区间。当新区间 [l,r][l, r] 满足:

    • 不被栈顶区间包含(即 r>栈顶的右端点r > \text{栈顶的右端点}
    • 加入后不会形成环(通过并查集检查 llrr 是否已在同一连通分量)

    则选择它,否则跳过。


    算法步骤

    1. 读入所有区间,记录原始下标
    2. 排序:按左端点升序,左端点相同时按右端点降序(使较长的区间先出现)
    3. 初始化:并查集 parent[12n]parent[1 \dots 2n],空栈
    4. 遍历每个区间 [l,r][l, r](按排序后的顺序):
      • 检查是否被栈顶区间包含:如果栈非空且栈顶区间的左端点 <l< l 且右端点 >r> r,则跳过
      • 检查是否会形成环:如果 find(l)==find(r)find(l) == find(r),则 llrr 已连通,加入会形成环,跳过
      • 否则,选择该区间:
        • 加入答案
        • 合并 llrr(并查集)
        • 将当前区间入栈
    5. 输出:答案大小和所选下标(按原序输出)

    正确性证明

    引理 1:按左端点排序后,栈中维护的区间是右端点严格递增的。

    因为如果新区间的右端点 \le 栈顶右端点,则它会被栈顶区间包含(因为左端点 \ge 栈顶左端点),从而被跳过。

    引理 2:选择区间不会产生长度 3\ge 3 的环当且仅当加入时 llrr 不在同一连通分量中。

    因为区间图中,环的形成必然导致某些端点被多条路径连接。并查集恰好检测了这一点。

    引理 3:上述贪心策略最大化 f(S)g(S)f(S) - g(S)

    • 每个被选区间都至少贡献了 1 到 f(S)f(S)(因为它覆盖了之前未覆盖的右端点区域)
    • 被跳过的区间要么不贡献 ff(被包含),要么会形成环(增加 gg
    • 因此贪心选择是全局最优的

    代码实现

    #include <bits/stdc++.h>
    using namespace std;
    
    struct Interval {
        int l, r, id;
    };
    
    int main() {
        ios::sync_with_stdio(false);
        cin.tie(nullptr);
        
        int t;
        cin >> t;
        
        while (t--) {
            int n;
            cin >> n;
            
            vector<Interval> intervals(n);
            for (int i = 0; i < n; i++) {
                cin >> intervals[i].l >> intervals[i].r;
                intervals[i].id = i + 1;  // 1-indexed 输出
            }
            
            // 按左端点升序,左端点相同时右端点降序
            sort(intervals.begin(), intervals.end(), [](const Interval& x, const Interval& y) {
                if (x.l != y.l) return x.l < y.l;
                return x.r > y.r;
            });
            
            // 并查集初始化
            vector<int> parent(2 * n + 5);
            iota(parent.begin(), parent.end(), 0);
            
            function<int(int)> find = [&](int x) -> int {
                if (parent[x] != x) parent[x] = find(parent[x]);
                return parent[x];
            };
            
            vector<int> ans;
            vector<int> stk;  // 存储选中的区间下标(排序后的索引)
            
            for (int i = 0; i < n; i++) {
                int l = intervals[i].l;
                int r = intervals[i].r;
                
                // 检查是否被栈顶区间包含
                bool covered = false;
                if (!stk.empty()) {
                    int top = stk.back();
                    if (intervals[top].l < l && intervals[top].r > r) {
                        covered = true;
                    }
                }
                
                // 检查是否会形成环
                if (!covered && find(l) != find(r)) {
                    ans.push_back(intervals[i].id);
                    parent[find(l)] = find(r);
                    stk.push_back(i);
                }
            }
            
            // 输出结果
            cout << ans.size() << '\n';
            for (size_t i = 0; i < ans.size(); i++) {
                cout << ans[i] << " \n"[i == ans.size() - 1];
            }
        }
        
        return 0;
    }
    

    样例验证

    样例 1

    n=1, intervals=[(1,2)]
    排序后:[(1,2)]
    栈空 → 选择 (1,2)
    输出:1\n1 ✓
    

    样例 2

    n=4, intervals=[(1,2),(2,3),(1,3),(3,5)]
    排序后(按 l 升序,l 相同按 r 降序):
    (1,3), (1,2), (2,3), (3,5)
    
    处理 (1,3):栈空 → 选择,ans=[1], 并查集合并1-3
    处理 (1,2):检查栈顶(1,3)包含(1,2)? 是 → 跳过
    处理 (2,3):检查栈顶(1,3)包含(2,3)? 3>3? 否(不包含),且 find(2)!=find(3) → 选择,ans=[1,2]
    处理 (3,5):检查栈顶(2,3)包含(3,5)? 否,且 find(3)!=find(5) → 选择,ans=[1,2,4]
    
    输出:3\n1 2 4 ✓
    

    复杂度分析

    • 排序:O(nlogn)O(n \log n)
    • 并查集:O(nα(n))O(n \alpha(n)),其中 α(n)\alpha(n) 是反阿克曼函数
    • 总复杂度:O(nlogn)O(n \log n),满足 n3000n \le 3000 的限制
    • 所有测试用例的 n2n^2 之和 9×106\le 9 \times 10^6,保证总时间在 2 秒内

    总结

    要点 说明
    核心思想 贪心 + 并查集 + 区间包含检测
    关键条件 区间不被包含且端点不连通才选择
    数据结构 排序、栈、并查集
    时间复杂度 O(nlogn)O(n \log n)
    难度 *1300,约 5.5/10 分
    • 1

    信息

    ID
    6536
    时间
    1000ms
    内存
    256MiB
    难度
    5
    标签
    递交数
    1
    已通过
    1
    上传者