1 条题解
-
0
D. 区间的区间 详细题解
问题重述
给定 个区间 ,定义区间之区间 ()的价值为第 到第 个区间的并集长度。需要选择恰好 个不同的区间之区间,使得它们的价值和最大,求这个最大值。
关键观察
观察 1:区间之区间价值的计算
对于固定的右端点 ,考虑所有左端点 ()对应的价值 。
当处理到第 个区间时,我们可以维护数轴上每个位置最后被覆盖的时间戳(即最后一次覆盖该位置的区间编号)。那么:
因为一个位置对 有贡献当且仅当它在 到 之间至少被覆盖一次。
观察 2:时间戳的更新
当我们加入新区间 时,需要将区间 内所有位置的时间戳更新为 。如果这些位置之前的时间戳是 ,那么对于所有 , 都会增加 (因为这些位置现在被覆盖了)。
观察 3:用线段维护相同时间戳的区间
由于时间戳相同的连续位置可以合并成一段,我们可以使用平衡树(如
std::set)来维护这些段。每段表示为 。当加入新区间 时:
- 找到与 相交的所有段
- 对于每个被完全覆盖的段,它会被删除,并触发对应的区间加操作
- 对于部分覆盖的段,需要分割
- 最后插入新的段 ,时间戳为
总操作次数为 ,因为每个段最多被创建和删除一次。
观察 4:选择最大的 个值
我们需要从所有 个区间之区间中选出最大的 个。可以使用二分答案:
设 为选中的最小值,我们需要:
- 统计价值 的区间之区间数量
- 计算这些区间的价值和
如果 ,说明可以选更大的 ;否则需要减小 。
最终答案 = (减去多余的部分)。
观察 5:快速统计 的区间
对于固定的 ,我们可以用滑动窗口计算:
对于每个右端点 ,定义 为最小的左端点,使得 。由于 随 增加而减小,所有 都满足条件。
维护:
t1:当前窗口内所有 的和t2:当前窗口内覆盖的位置总数
当 增加到 时,需要:
- 更新时间戳,可能增加 和
- 移动左边界 ,使
- 更新统计量
算法步骤
第一部分:预处理每个 的时间戳变化
对于每个 ,记录当加入区间 时,所有被覆盖段的时间戳变化:
- 每个变化表示为
(timestamp, length),表示有一段时间戳为timestamp的长度为length的段被覆盖
这些变化用于在二分答案时快速更新统计量。
第二部分:二分答案
二分价值阈值 :
-
初始化:
j = 1(窗口左边界)t1 = 0(当前窗口内所有 的和)t2 = 0(当前窗口内覆盖的位置总数)s1 = 0(所有 的区间价值和)s2 = 0(所有 的区间数量)
-
对于 到 :
- 应用第 个区间的所有变化(来自预处理)
- 移动左边界 ,使
- 更新 和
-
根据 与 的比较调整二分边界
第三步:计算答案
最终答案 = ,其中 是二分找到的阈值。
时间复杂度
- 预处理:(每个段最多被处理一次)
- 二分答案:
- 总复杂度:
完整代码实现
#include<bits/stdc++.h> using namespace std; const int MN = 300000; const int MAX_POS = 1e9; set<pair<int, int>> st; // (位置, 时间戳) vector<pair<int, int>> v[MN + 5]; // v[i]: 当加入区间 i 时的时间戳变化 int f[MN + 5]; // 辅助数组,记录每个时间戳对应的长度 int main() { int n, m; scanf("%d%d", &n, &m); // 初始化:整个数轴时间戳为 0 st.insert({1, 0}); st.insert({MAX_POS, 0}); for (int i = 1; i <= n; ++i) { int l, r; scanf("%d%d", &l, &r); // 找到第一个位置 >= l 的段 auto it = st.upper_bound({l, n}); auto tt = it; --tt; // tt 是前一个段 // 处理左边界部分覆盖 int x = tt->second; // 前一段的时间戳 v[i].push_back({x, tt->first - l}); // 左边被覆盖的部分 if (tt->first < l) { // 部分覆盖,需要保留左边部分 v[i].push_back({x, l - tt->first}); } else { // 完全覆盖,删除整个段 st.erase(tt); } // 插入新区间 v[i].push_back({i, r - l}); st.insert({l, i}); // 处理中间完全覆盖的段 while (tt->first < r) { it = tt; ++tt; x = it->second; v[i].push_back({x, it->first - tt->first}); st.erase(it); } // 处理右边界部分覆盖 if (tt->first > r) { x = tt->second; v[i].push_back({x, tt->first - r}); st.insert({r, x}); } } // 二分答案 int L = 1, R = 1e9, X = 0; long long S1 = 0, S2 = 0; while (L <= R) { int x = (L + R) / 2; memset(f, 0, sizeof(f)); long long s1 = 0, s2 = 0; // 总价值和,总数量 long long t1 = 0, t2 = 0; // 当前窗口的和,当前窗口的覆盖长度 int j = 1; // 窗口左边界 for (int i = 1; i <= n; ++i) { // 应用第 i 个区间的所有变化 for (auto [timestamp, len] : v[i]) { if (timestamp < j) { t1 += 1LL * timestamp * len; } else { t1 += 1LL * (j - 1) * len; t2 += len; f[timestamp] += len; } } // 移动左边界,使 t2 >= x while (j <= i && t2 >= x) { t1 += t2; t2 -= f[j]; ++j; } s1 += t1; s2 += j - 1; } if (s2 >= m) { X = x; S1 = s1; S2 = s2; L = x + 1; } else { R = x - 1; } } // 计算答案:减去多余的部分 printf("%lld\n", S1 - (S2 - m) * X); return 0; }代码说明
-
数据结构:
st:维护数轴上的段,每个段有起点和时间戳v[i]:记录加入区间 时的时间戳变化,每个变化为(时间戳, 长度)
-
预处理过程:
- 使用
set维护所有段 - 当新区间覆盖某个段时,记录该段被覆盖,并更新统计
- 每个段最多被创建和删除一次,总操作
- 使用
-
二分答案过程:
f[timestamp]:记录当前窗口内每个时间戳对应的总长度t1:当前窗口内所有 的和t2:当前窗口内覆盖的总长度- 滑动窗口维护左边界,使
-
边界处理:
- 时间戳为 表示从未被覆盖
- 窗口左边界 从 开始
- 当 时,不能移动左边界
示例演示
以第二个样例为例:
3 3 1 4 5 7 3 6处理过程:
- 加入区间1 :覆盖位置1-4,时间戳=1
- 加入区间2 :覆盖位置5-7,时间戳=2
- 加入区间3 :覆盖位置3-6,时间戳=3
- 位置3-4原来时间戳=1,变为3
- 位置5-6原来时间戳=2,变为3
最终统计所有区间之区间的价值:
- , ,
- ,
选择最大的3个:6+5+4=15
总结
本题的核心技巧:
- 用时间戳表示每个位置最后被覆盖的时间
- 用平衡树维护相同时间戳的连续段
- 通过预处理记录每次更新对答案的贡献
- 二分答案 + 滑动窗口统计价值 的区间数量和
- 最终答案 = 总价值和 - (超出数量) × 阈值
- 1
信息
- ID
- 6266
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 1
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者