梦想不会自己发光,真正闪耀的是那个为梦狂奔的你。献给知行的孩子们!(Eric.He著)
对多个一维区间进行覆盖、合并、选点,目标是最小化覆盖点数、最大化区间合并效率,每一步选择对区间操作最优的基准点 / 区间。
本教程以多个字符串处理贪心算法案例场景为例,让大家彻底理解以 “局部最优” 推导 “全局最优” 的核心逻辑,并掌握区间覆盖问题、区间合并问题、区间选点问题的原理及代码实现。
问题描述:
算法解析:
#include <iostream>
#include <vector>
using namespace std;
// 区间结构体:左端点、右端点
struct Interval {
int start;
int end;
bool operator<(const Interval& other) const {
return start < other.start; // 左端点升序
}
};
// 区间覆盖:target-目标区间,intervals-作业区间列表,返回最少需要的子区间数
int intervalCover(Interval target, vector<Interval>& intervals) {
sort(intervals.begin(), intervals.end());
int n = intervals.size();
int res = 0; // 选中的作业区间数
int curStart = target.start; // 当前需要覆盖的起点
int i = 0;
int maxEnd = -1; // 本次能覆盖的最远右端点
while (curStart < target.end) {
// 找到所有包含curStart的作业区间中,右端点最远的
while (i < n && intervals[i].start <= curStart) {
maxEnd = max(maxEnd, intervals[i].end);
i++;
}
if (maxEnd <= curStart) return -1; // 无法覆盖当前起点,失败
res++;
curStart = maxEnd; // 更新起点为最远右端点
}
return res;
}
int main() {
// 测试:目标区间[0, 20],作业区间列表
Interval target = {0, 20};
vector<Interval> intervals = {{1,5}, {0,3}, {4,8}, {7,12}, {9,15}, {14,20}, {10,18}};
int res = intervalCover(target, intervals);
if (res == -1) cout << "无法覆盖目标区间" << endl;
else cout << "最少洒水作业次数:" << res << endl;
return 0;
}
最少洒水作业次数:6
问题描述:
算法解析:
核心思路分为 3 步,排序是合并的前提(无序的区间无法直接判断重叠),具体如下:
#include <iostream>
#include <vector>
using namespace std;
// 合并重叠/相邻日程事件的核心函数
vector<vector<int>> merge(vector<vector<int>>& intervals) {
// 处理边界情况:输入为空,直接返回空数组
if (intervals.empty()) {
return {};
}
// 步骤1:按事件开始时间升序排序(核心前提,无序无法直接合并)
sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b) {
return a[0] < b[0]; // 按start升序排列
});
// 步骤2:初始化结果集,加入第一个排序后的事件作为合并基准
vector<vector<int>> merged;
merged.push_back(intervals[0]);
// 步骤3:遍历剩余事件,逐个判断并合并
for (size_t i = 1; i < intervals.size(); ++i) {
// 取结果集最后一个已合并的事件
vector<int>& last = merged.back();
// 当前待判断的事件
const vector<int>& curr = intervals[i];
if (curr[0] <= last[1]) {
// 重叠/相邻,合并事件:更新结束时间为两者的最大值
last[1] = max(last[1], curr[1]);
} else {
// 无重叠,直接将当前事件加入结果集
merged.push_back(curr);
}
}
return merged;
}
int main() {
// 测试用例(与之前示例一致)
vector<vector<int>> schedule = {{1,3},{2,6},{8,10},{15,18},{7,9}};
vector<vector<int>> result = merge(schedule);
// 打印合并后的结果
cout << "合并后的日程表:";
for (const auto& interval : result) {
cout << "[" << interval[0] << "," << interval[1] << "] ";
}
cout << endl;
// 测试边界情况:单个事件
vector<vector<int>> single = {{5,8}};
vector<vector<int>> singleRes = merge(single);
cout << "单个事件合并结果:";
for (const auto& interval : singleRes) {
cout << "[" << interval[0] << "," << interval[1] << "] ";
}
cout << endl;
return 0;
}
合并后的日程表:[1,6] [7,10] [15,18]
单个事件合并结果:[5,8]
问题描述:
算法解析:
区间选点问题的最优贪心策略是按区间右端点升序排序,核心逻辑为:每次选择当前未覆盖区间的右端点作为站点,该策略能让单个站点覆盖尽可能多的后续区间,从而保证站点数量最少。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 核心函数:计算最少公交站点数及站点位置
// 输入:intervals - 居民区出行区间集合
// 输出:pair<最少站点数, 站点位置列表>
pair<int, vector<int>> minBusStops(vector<vector<int>>& intervals) {
vector<int> stops; // 存储最终站点位置
// 边界情况1:无居民区,直接返回0个站点
if (intervals.empty()) {
return {0, stops};
}
// 步骤1:按区间右端点升序排序(贪心策略的核心前提)
sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b) {
return a[1] < b[1]; // 按end升序,优先选右端点小的区间
});
// 步骤2:贪心选点
int n = intervals.size();
vector<bool> covered(n, false); // 标记区间是否被覆盖,初始全未覆盖
for (int i = 0; i < n; ++i) {
if (!covered[i]) { // 找到第一个未覆盖的区间
int stop = intervals[i][1]; // 选其右端点作为站点(贪心核心)
stops.push_back(stop); // 加入站点列表
// 标记所有包含该站点的区间为已覆盖
for (int j = i; j < n; ++j) {
if (intervals[j][0] <= stop && stop <= intervals[j][1]) {
covered[j] = true;
}
}
}
}
// 返回:站点数 + 站点位置
return {stops.size(), stops};
}
// 辅助函数:打印区间(方便测试输出)
void printIntervals(const vector<vector<int>>& intervals) {
for (const auto& p : intervals) {
cout << "[" << p[0] << "," << p[1] << "] ";
}
cout << endl;
}
int main() {
// 测试用例1:示例输入
vector<vector<int>> test1 = {{1,3},{2,5},{4,7},{6,8}};
// 测试用例2:边界情况 - 单个居民区
vector<vector<int>> test2 = {{5,9}};
// 测试用例3:边界情况 - 所有区间重叠
vector<vector<int>> test3 = {{2,6},{1,4},{3,5},{2,7}};
// 测试用例4:边界情况 - 区间无重叠
vector<vector<int>> test4 = {{1,2},{3,4},{5,6},{7,8}};
// 遍历测试用例执行
vector<vector<vector<int>>> tests = {test1, test2, test3, test4};
for (int idx = 0; idx < tests.size(); ++idx) {
cout << "======== 测试用例" << idx+1 << " ========" << endl;
cout << "居民区出行区间:";
printIntervals(tests[idx]);
auto [cnt, stops] = minBusStops(tests[idx]); // C++17及以上支持结构化绑定
// 若编译器不支持C++17,替换为:
// pair<int, vector<int>> res = minBusStops(tests[idx]);
// int cnt = res.first;
// vector<int> stops = res.second;
cout << "最少站点数:" << cnt << ",站点位置:[";
for (int i = 0; i < stops.size(); ++i) {
cout << stops[i];
if (i < stops.size()-1) cout << ",";
}
cout << "]" << endl << endl;
}
return 0;
}
======== 测试用例1 ========
居民区出行区间:[1,3] [2,5] [4,7] [6,8]
最少站点数:2,站点位置:[3,7]
======== 测试用例2 ========
居民区出行区间:[5,9]
最少站点数:1,站点位置:[9]
======== 测试用例3 ========
居民区出行区间:[2,6] [1,4] [3,5] [2,7]
最少站点数:1,站点位置:[4]
======== 测试用例4 ========
居民区出行区间:[1,2] [3,4] [5,6] [7,8]
最少站点数:4,站点位置:[2,4,6,8]