LeetCode 632. 最小区间
给定 k 个非递减排列的整数列表,需要找到一个最小闭区间 [a, b],使得这 k 个列表中每个列表至少有一个数包含在该区间内。
如果两个区间长度不同,较短者更小;若长度相同,则左端点较小者更小。
核心思路
要让区间覆盖所有列表,最直观的做法是先把每个列表的当前最小值拉进来。初始状态下,这些首元素构成的区间 [l1, r1] 肯定满足条件,但通常不是最优解。接下来的关键就是如何压缩这个区间。
在压缩之前必须明确,操作过程中集合内必须保证含有每个序列中的数。怎么压缩就体现了贪心的思想:每次从集合中弹出最小的数,为了满足'每个序列至少有一个数在区间内'的条件,需要在其所属序列中选择下一个数加入集合,获取新集合的区间对比更新所求区间。
例如:当前从集合中弹出的元素是 nums[i][j],那么就需要将 nums[i][j+1] 加入集合。假设弹出前集合的区间为 [l1, r1],即当前找到的最小区间 ans;加入元素后的新集合对应的新区间为 [l2, r2],此时就要将 ans 与新区间进行对比,如果新区间小于 ans,就更新 ans 为新区间,否则 ans 不动。
为什么每次弹出集合中最小的数?
这里有个直觉上的疑问:为什么非要弹出最小的那个?其实道理很简单。既然题目保证所有序列都为非递减序列,则每次弹出集合中最小的数(也就是 l1),并加入其原序列的下一个数 x,那么 x 一定大于 l1。
- 如果此时 x 为新集合中最小的数,那么新集合的新区间为 [x, r1],其长度一定小于原区间长度。又因为 x 是从 l1 的序列中加入集合的,说明弹出、加入的过程不会影响到集合中其他序列的数,也就是一定不会破坏'每个序列至少有一个数在区间内'的条件。所以 [x, r1] 一定是当前满足题意的更优区间,随之更新 ans,从而达到压缩区间的目的。
- 如果此时 x 不为新集合中最小的数,最小的数为 l2,x 可能大于 r1,也可能在 [l2, r1] 之间。此时就要对比新集合的新区间与 ans 的大小,判断是否更新 ans 同样可以压缩区间。
即使某个序列中有多个数在区间也无所谓,只要满足'每个序列至少有一个数在区间内'的条件即可。至此,不断弹出,加入元素,保证集合内元素个数不变,不断在贪心的基础上压缩,直到集合不可再压缩,返回 ans 区间。
参考实现
C++ 版本
// LeetCode 632. 最小区间
class Solution {
public:
typedef struct node {
int data; // 当前元素数据
int i; // 原序列位置
int idx; // 所在原序列下标
bool operator < (const node& a) const {
if (data == a.data) {
return i < a.i;
}
return data < a.data;
}
} Node;
vector<int> smallestRange(vector<vector<int>>& nums) {
vector<> ans;
(nums.()) {
ans;
}
set<Node> s;
( i = ; i < nums.(); i++) {
(nums[i].()) {
;
}
Node t = { nums[i][], i, };
s.(t);
}
(s.()) {
ans;
}
l = (*s.()).data;
r = (*s.()).data;
() {
it = s.();
i = (*it).i;
idx = (*it).idx;
(idx == nums[i].() - ) {
;
} {
s.(s.());
Node t = { nums[i][idx + ], i, idx + };
s.(t);
(((*s.()).data - (*s.()).data) < (r - l)) {
r = (*s.()).data;
l = (*s.()).data;
} (((*s.()).data - (*s.()).data) == (r - l)) {
((*s.()).data < l) {
r = (*s.()).data;
l = (*s.()).data;
}
}
}
}
ans.(l);
ans.(r);
ans;
}
};



