题目描述
你有 k 个 非递减排列 的整数列表。找到一个 最小 区间,使得 k 个列表中的每个列表至少有一个数包含在其中。
我们定义如果 b-a < d-c 或者在 b-a == d-c 时 a < c,则区间 [a,b] 比 [c,d] 小。
示例 1:
输入:nums = [[4,10,15,24,26], [0,9,12,20], [5,18,22,30]]
输出:[20,24]
解释:
列表 1:[4, 10, 15, 24, 26],24 在区间 [20,24] 中。
列表 2:[0, 9, 12, 20],20 在区间 [20,24] 中。
列表 3:[5, 18, 22, 30],22 在区间 [20,24] 中。示例 2:
输入:nums = [[1,2,3],[1,2,3],[1,2,3]]
输出:[1,1]示例 3:
输入:nums = [[10,10],[11,11]]
输出:[10,11]示例 4:
输入:nums = [[10],[11]]
输出:[10,11]示例 5:
输入:nums = [[1],[2],[3],[4],[5],[6],[7]]
输出:[1,7]
提示:
nums.length == k
1 <= k <= 3500
1 <= nums[i].length <= 50
-105 <= nums[i][j] <= 105
nums[i] 按非递减顺序排列
算法分析
在讲这个方法之前我们先思考这样一个问题:有一个序列 A={a1,a2,⋯ ,an}和一个序列 B={b1,b2,⋯ ,bm},请找出一个 B 中的一个最小的区间,使得在这个区间中 A序列的每个数字至少出现一次,请注意 AAA 中的元素可能重复,也就是说如果 A中有 p 个 u,那么你选择的这个区间中 u的个数一定不少于 p。没错,这就是我们五月份的一道打卡题:「76. 最小覆盖子串」。官方题解使用了一种滑动窗口的方法,遍历整个 B 序列并用一个哈希表表示当前窗口中的元素:
右边界在每次遍历到新元素的时候右移,同时将拓展到的新元素加入哈希表;
左边界右移当且仅当当前区间为一个合法的答案区间,即当前窗口内的元素包含 A 中所有的元素,同时将原来左边界指向的元素从哈希表中移除;
答案更新当且仅当当前窗口内的元素包含 A中所有的元素。如果这个地方不理解,可以参考「76. 最小覆盖子串的官方题解」。
回到这道题,我们发现这两道题的相似之处在于都要求我们找到某个符合条件的最小区间,我们可以借鉴「76. 最小覆盖子串」的做法:这里序列 {0,1,⋯ ,k−1}就是上面描述的 A序列,即 k个列表,我们需要在一个 B序列当中找到一个区间,可以覆盖 A序列。这里的 B序列是什么?我们可以用一个哈希映射来表示 B序列—— B[i]表示 i在哪些列表当中出现过,这里哈希映射的键是一个整数,表示列表中的某个数值,哈希映射的值是一个数组,这个数组里的元素代表当前的键出现在哪些列表里。也许文字表述比较抽象,大家可以结合下面这个例子来理解。
如果列表集合为:
0: [-1, 2, 3]
1: [1]
2: [1, 2]
3: [1, 1, 3]那么可以得到这样一个哈希映射
-1: [0]
1: [1, 2, 3, 3]
2: [0, 2]
3: [0, 3]我们得到的这个哈希映射就是这里的 B序列。我们要做的就是在 B序列上使用两个指针维护一个滑动窗口,并用一个哈希表维护当前窗口中已经包含了哪些列表中的元素,记录它们的索引。遍历 B 序列的每一个元素:
指向窗口右边界的指针右移当且仅当每次遍历到新的元素,并将这个新的元素对应的值数组中的每一个数加入到哈希表中;
指向窗口左边界的指针右移当且仅当当前区间内的元素包含 A中所有的元素,同时将原来左边界对应的值数组的元素们从哈希表中移除;
答案更新当且仅当当前窗口内的元素包含 A中所有的元素。
代码
class Solution {
public:
vector<int> smallestRange(vector<vector<int>>& nums) {
int n = nums.size();
unordered_map<int, std::vector<int>> indices;
int min_value = INT_MAX, max_value = INT_MIN;
for(int i = 0; i < n; ++i) {
for(const int &x : nums[i]) {
indices[x].push_back(i);
min_value = std::min(x, min_value);
max_value = std::max(x, max_value);
}
}
std::vector<int> freq(n);
int count = 0;
int left = min_value, right = min_value - 1;
int best_right = max_value, best_left = min_value;
while(right < max_value) {
++right;
if(indices.count(right) > 0) {
for(const int &index: indices[right]) {
if(freq[index] == 0) {
++count;
}
++freq[index];
}
while(count == n) {
if(right - left < best_right - best_left) {
best_left = left;
best_right = right;
}
if(indices.count(left) > 0) {
for(const int &index : indices[left]) {
--freq[index];
if(freq[index] == 0) {
--count;
}
}
}
++left;
}
}
}
return {best_left, best_right};
}
};
时间复杂度分析
时间复杂度:O(nk+∣V∣)O,其中 n是所有列表的平均长度,k是列表数量,∣V∣是列表中元素的值域,在本题中 ∣V∣≤ 2 ∗10^5。构造哈希映射的时间复杂度为 O(nk,双指针的移动范围为 ∣V∣,在此过程中会对哈希映射再进行一次遍历,时间复杂度为 O(nk),因此总时间复杂度为 O(nk+∣V∣)。
空间复杂度:O(nk),即为哈希映射使用的空间。哈希映射的「键」的数量由列表中的元素个数 nknknk 以及值域 ∣V∣ 中的较小值决定,「值」为长度不固定的数组,但是它们的长度之和为 nk,因此哈希映射使用的空间为 O(nk)。在使用双指针时,还需要一个长度为 n的数组,其对应的空间在渐进意义下小于 O(nk),因此可以忽略。