贪心算法解决固定长度区间覆盖问题:最少区间数计算
题目描述
给定实直线上的 n
个点和一个固定长度 k
的闭区间,要求选择若干个这样的闭区间(区间起点、终点可任意选择),使得所有点都被区间覆盖,且使用的区间数量最少。最终输出最少需要的区间数。
输入格式
- 第一行:两个正整数
n
(点的数量,n≤10000
)和k
(区间固定长度,k≤100
); - 第二行:
n
个整数,代表n
个点在实直线上的坐标(可能存在重复点)。
输出格式
- 一个整数,代表覆盖所有点所需的最少固定长度闭区间数。
解题思路:贪心算法的核心逻辑
要解决 “最少区间覆盖” 问题,贪心算法是最优选择,其核心思想是每次尽可能用一个区间覆盖更多的点,具体步骤如下:
- 排序点坐标:首先将所有点按坐标从小到大排序。排序是贪心的基础 —— 只有有序的点,才能保证 “从左到右覆盖” 的逻辑正确性。
- 初始化区间:以排序后的第一个点为起点,构建第一个闭区间
[current, current + k]
(current
初始为第一个点的坐标),此时最少区间数cnt
初始化为 1(至少需要一个区间)。 - 遍历覆盖点:从第二个点开始依次检查每个点:
- 若当前点在当前区间
[current, current + k]
内(即nums[i] ≤ current + k
),则该点已被覆盖,无需新增区间; - 若当前点超出当前区间(即
nums[i] > current + k
),则需要新增一个区间,新区间的起点设为当前点(保证新区间能覆盖该点,且尽可能覆盖后续更多点),同时cnt
加 1。
- 若当前点在当前区间
- 输出结果:遍历完成后,
cnt
即为最少需要的区间数。
完整代码实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;/*** 计算覆盖所有点所需的最少固定长度闭区间数* @param k:固定区间长度* @param n:点的数量* @param nums:存储点坐标的向量(传入引用避免拷贝开销)*/
void calculateMinIntervals(int k, int n, vector<int>& nums) {// 步骤1:将点按坐标从小到大排序(贪心算法的前提)sort(nums.begin(), nums.end());int min_intervals = 1; // 最少区间数,至少需要1个int current_start = nums[0]; // 第一个区间的起点(从第一个点开始)// 步骤2:遍历所有点,判断是否需要新增区间for (int i = 1; i < n; ++i) {// 若当前点超出当前区间的范围(current_start + k 是当前区间的终点)if (current_start + k < nums[i]) {min_intervals++; // 新增一个区间current_start = nums[i]; // 新区间的起点设为当前点}// 若当前点在区间内,无需操作}// 输出结果cout << min_intervals << endl;
}int main() {int n, k;// 输入点的数量和区间长度cin >> n >> k;vector<int> points(n);// 输入n个点的坐标for (int i = 0; i < n; ++i) {cin >> points[i];}// 调用函数计算最少区间数calculateMinIntervals(k, n, points);return 0;
}
代码解析
1. 关键变量说明
min_intervals
:记录最少区间数,初始为 1(因为即使只有一个点,也需要一个区间覆盖);current_start
:当前区间的起点,初始为排序后第一个点的坐标,后续每次新增区间时更新为当前点的坐标;sort(nums.begin(), nums.end())
:排序是核心预处理步骤,确保点按从左到右的顺序处理,避免遗漏或重复覆盖。
2. 核心判断逻辑
if (current_start + k < nums[i]) {min_intervals++;current_start = nums[i];
}
- 区间的终点是
current_start + k
(因为区间长度为k
,闭区间[current_start, current_start + k]
刚好覆盖长度k
); - 当
nums[i]
大于区间终点时,说明当前点不在当前区间内,必须新增一个区间,且新区间的起点设为nums[i]
(这样能最大限度覆盖后续的点,减少区间总数)。
3. 处理重复点
若输入中存在重复点(如 [2, 2, 3]
),排序后重复点会相邻,此时重复点必然在同一个区间内,无需额外处理,代码会自动覆盖。
测试案例与运行结果
案例 1:基础覆盖
5 3
1 2 3 4 5
排序后点:[1, 2, 3, 4, 5]
过程:
- 第一个区间
[1, 4]
,覆盖 1、2、3、4; - 点 5 超出
[1,4]
,新增区间[5, 8]
,覆盖 5;
输出:2
案例 2:含负坐标
输入:
6 2
-1 0 1 3 4 5
排序后点:[-1, 0, 1, 3, 4, 5]
过程:
- 第一个区间
[-1, 1]
,覆盖 -1、0、1; - 点 3 超出
[-1,1]
,新增区间[3, 5]
,覆盖 3、4、5;
输出:2
案例 3:单个点
输入:
1 10
5
过程:仅需一个区间 [5, 15]
覆盖点 5;
输出:1
案例 4:重复点
输入:
4 2
2 2 3 5
排序后点:[2, 2, 3, 5]
过程:
- 第一个区间
[2, 4]
,覆盖 2、2、3; - 点 5 超出
[2,4]
,新增区间[5,7]
;
输出:2
算法正确性证明
要证明贪心算法的正确性,需验证其满足贪心选择性质和最优子结构性质:
1. 贪心选择性质
“每次选择以当前未覆盖点为起点的区间,能覆盖最多后续点” 是最优选择。
假设存在更优的方案:在某个步骤中,没有选择当前未覆盖点 nums[i]
作为新区间起点,而是选择了 nums[i]
左侧的某个点 x
(x < nums[i]
)作为起点。此时区间 [x, x + k]
虽然能覆盖 nums[i]
,但后续点可能仍需新增相同数量的区间(甚至更多),因此该选择不会比 “以 nums[i]
为起点” 更优。
2. 最优子结构性质
若覆盖前 i
个点的最少区间数为 m
,则覆盖前 i+1
个点的最少区间数要么是 m
(第 i+1
个点在第 m
个区间内),要么是 m+1
(第 i+1
个点超出第 m
个区间,需新增区间)。这种 “局部最优解扩展为全局最优解” 的特性,证明了问题具有最优子结构。
综上,该贪心算法能得到全局最优解(最少区间数)。
算法复杂度分析
- 时间复杂度:
O(n log n)
主要耗时操作是对n
个点的排序(O(n log n)
),后续遍历点的操作是O(n)
,整体复杂度由排序决定。 - 空间复杂度:
O(1)
(不含输入存储)
仅使用了min_intervals
、current_start
等常数个变量,空间开销与n
无关。
总结
本文通过贪心算法解决了固定长度区间覆盖问题,核心是 “排序后从左到右覆盖,每次尽可能覆盖更多点”。代码简洁高效,能处理 n≤10000
的输入规模,且正确性有严格的数学证明支撑。该思路不仅适用于本题,还可推广到 “区间覆盖” 类问题的变种(如区间选点、最长不重叠区间等),是算法学习中的基础经典模型。