1 条题解
-
0
C. 双重视角 —— 详细题解
题目重述
给定 个不同的区间(对),其中 。定义:
- :将每个对视为数轴上的区间, 是这些区间并集的长度(即被覆盖的单位线段 的个数)。
- :将每个对视为图中的一条无向边, 是位于至少一个长度 的简单环上的节点个数。
需要选择一个子集 ,使得 最大,并输出所选对的下标。
数据范围:
- 所有 之和
关键观察
观察 1:区间图的性质
把每个区间 看作一条连接 和 的边。由于 且所有端点都是 到 的整数,这是一个区间图(interval graph)。
区间图有一个重要性质:图中的环必然由区间包含关系产生。
具体来说:
- 如果三个区间 满足 ,则它们会在图中形成一个三角形(三元环)。
- 更一般地,任何长度 的环都对应一个嵌套区间链。
观察 2: 与 的关系
- 每添加一个新区间, 可能增加(如果新区间覆盖了之前未覆盖的区域)
- 但如果新区间被已有区间完全包含,则 不变,但可能在图中引入环(增加 )
- 因此,为了最大化 ,我们应该避免选择被完全包含的区间
观察 3:贪心策略
按左端点排序后,可以用一个栈维护当前选择的区间。当新区间 满足:
- 不被栈顶区间包含(即 )
- 加入后不会形成环(通过并查集检查 和 是否已在同一连通分量)
则选择它,否则跳过。
算法步骤
- 读入所有区间,记录原始下标
- 排序:按左端点升序,左端点相同时按右端点降序(使较长的区间先出现)
- 初始化:并查集 ,空栈
- 遍历每个区间 (按排序后的顺序):
- 检查是否被栈顶区间包含:如果栈非空且栈顶区间的左端点 且右端点 ,则跳过
- 检查是否会形成环:如果 ,则 和 已连通,加入会形成环,跳过
- 否则,选择该区间:
- 加入答案
- 合并 和 (并查集)
- 将当前区间入栈
- 输出:答案大小和所选下标(按原序输出)
正确性证明
引理 1:按左端点排序后,栈中维护的区间是右端点严格递增的。
因为如果新区间的右端点 栈顶右端点,则它会被栈顶区间包含(因为左端点 栈顶左端点),从而被跳过。
引理 2:选择区间不会产生长度 的环当且仅当加入时 和 不在同一连通分量中。
因为区间图中,环的形成必然导致某些端点被多条路径连接。并查集恰好检测了这一点。
引理 3:上述贪心策略最大化 。
- 每个被选区间都至少贡献了 1 到 (因为它覆盖了之前未覆盖的右端点区域)
- 被跳过的区间要么不贡献 (被包含),要么会形成环(增加 )
- 因此贪心选择是全局最优的
代码实现
#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 ✓
复杂度分析
- 排序:
- 并查集:,其中 是反阿克曼函数
- 总复杂度:,满足 的限制
- 所有测试用例的 之和 ,保证总时间在 2 秒内
总结
要点 说明 核心思想 贪心 + 并查集 + 区间包含检测 关键条件 区间不被包含且端点不连通才选择 数据结构 排序、栈、并查集 时间复杂度 难度 *1300,约 5.5/10 分
- 1
信息
- ID
- 6536
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 5
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者