文章目录
0. 前言
玄学的贪心问题,一般全凭直觉。
贪心问题没有固定讨论,没有模板,见多了就好了,证明想法的正确性是很困难的,大多采用反证法。
区间问题无非左端点、右端点、左右端点排序…
1. 区间问题+贪心
贪心思路:
- 区间按左端点从小到大排序
- 从前往后依次枚举每个区间
- 在所有能覆盖
start
的区间中,选择右端点最大的区间,然后将start
更新成右端点的最大值
- 在所有能覆盖
证明:
- 假设最优解为
ans
个区间,以上述贪心思路选出来的区间为cnt
个。即证明ans = cnt
,等价于ans >= cnt && ans <= cnt
- 首先,先不考虑无解情况,以上述贪心思路选择出的
cnt
个区间,是一组可行方案。且ans
表示的是所有可行方案的最小值,那么ans <= cnt
成立 - 反证法证明
ans >= cnt
,假设存在ans < cnt
。在此对比贪心思路和最优解ans
所找到的区间。由于数量不等,那么所选择的区间肯定也是不等的。我们可以通过对比两种方式的所选区间,找到第一个两者不同的区间。由于贪心思路保证了所选区间的右端点最大,那么ans
所选区间不同,意味着ans
的区间右端点是严格小于cnt
的右端点的。且由于ans
的下一个区间的左端点一定在ans
当前区间右端点的左侧,那么也一定在cnt
的左侧,故可将ans
这个短区间替换成cnt
这个长区间,且ans
中的区间数量不发生变化。同理,继续上述操作,替换ans
的区间,我们一定可以将最优解ans
的区间替换成cnt
的区间,那么就有ans = cnt
,与反证法假设矛盾
貌似不用反证法更香一点…最优解一定可以等价替换成贪心思路的解。直接证明相等即可。
注意下代码中的双指针部分…指针更新给理解成了 i = j
,debug
了很久很久…
lc 上该题是固定了左端点为 1,然后给定区间判断是否能进行覆盖,主题思路还是一致的。这是当时 1024 的每日一题,印象还是相当深刻的,可参考:[M贪心+区间问题] lc1024. 视频拼接(贪心+区间覆盖)
代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5+5;
struct Range {
int l, r;
bool operator<(const Range &W) const {
return l < W.l;
}
}range[N];
int main() {
int st, ed;
cin >> st >> ed;
int n;
cin >> n;
for (int i = 0; i < n; ++i) {
int a, b;
cin >> a >> b;
range[i] = {a, b};
}
sort(range, range + n);
int res = 0;
bool success = false; // 判断是否成成功覆盖
for (int i = 0; i < n; ++i) { // 枚举所有区间,区间已经按照左端点进行排序
int j = i, r = -2e9;
while (j < n && range[j].l <= st) { // 双指针遍历所有左端点在st左边的区间右端点的最大值
r = max(r, range[j].r);
++j;
}
if (r < st) { // 如果当前最大的区间右端点都无法到达st,那么则无法覆盖,覆盖失败返回-1
res = -1;
break;
}
++res; // 可以覆盖,区间数++
if (r >= ed) { // 如果所选区间加上已经大于了ed,则覆盖完毕
success = true;
break;
}
st = r; // 更新起点
i = j - 1; // 更新双指针位置,注意在此是i=j-1而不是i=j,循环中i还会++,j已经指向了下一个区间位置
// 需要的是将i更新到j的位置上来,不要让i在此加两遍...是一个双指针模板,搁着debug了好久....
}
if (!success) res = -1;
cout << res << endl;
return 0;
}