问题描述
在某些情况下,一个数列被称为“迷人数列”,如果这个数列的最大值和最小值之差不超过某个给定的阈值 k。现在,给定一个由 n 个整数构成的数列和阈值 k,你的任务是统计出数列中有多少个连续的子序列是“迷人的”。
测试样例
样例1:
输入:
n = 4,k = 2,sequence = [3, 1, 2, 4]
输出:8
样例2:
输入:
n = 5,k = 3,sequence = [7, 3, 5, 1, 9]
输出:6
样例3:
输入:
n = 6,k = 1,sequence = [2, 2, 3, 1, 1, 2]
输出:12
解题思路
由于我们要求一个序列中的最大值和最小值,可能会出现序列缩小导致最大值或者最小值发生变化,所以我们需要利用单调队列来保存序列中的最大值和最小值的索引。使得最大值单调队列里面存储的索引都是对应的值单调递减的,最小值单调队列里面存储的索引都是对应的值单调递增的。
对于一个满足迷人序列的连续序列来说,由于我们每次更新right,一旦确定迷人子序列时,它的连续子序列一定也是迷人子序列,因为这个子序列只会出现最大值变小,最小值变大的情况。为了避免重复计数,我们每次只对sequence[right]结尾的序列进行计数。
以下是关键步骤:
-
滑动窗口:
- 使用两个指针
left和right来定义一个窗口,这个窗口用来检查当前子序列是否是“迷人数列”。 - 初始化窗口的左边界
left和右边界right都为0,然后逐步移动right来扩展窗口,寻找所有符合条件的子序列。
- 使用两个指针
-
双端队列:
- 使用
max_queue和min_queue分别维护窗口内的最大值和最小值的索引。 - 这两个队列帮助在O(1)时间复杂度内获取当前窗口的最大值和最小值。
- 使用
-
更新队列:
- 对于
max_queue,如果新元素sequence[right]大于等于队列尾部所指的元素(由索引max_queue.back()获取),则不断将队列尾部元素弹出。这样保证队列中的索引对应的元素是从大到小的顺序,队首始终是窗口内最大值的索引。 - 对于
min_queue,如果新元素sequence[right]小于等于队列尾部所指的元素,则不断将队列尾部元素弹出。这样保证队列中的索引对应的元素是从小到大的顺序,队首始终是窗口内最小值的索引。
- 对于
-
调整窗口:
- 当
sequence[max_queue.front()] - sequence[min_queue.front()] > k时,当前窗口不再是“迷人数列”,需要收缩窗口。通过移动left来缩小窗口,直到窗口再次成为“迷人数列”。 - 在收缩的过程中,如果
left等于max_queue或min_queue的队首索引,则需要将队首弹出以保持合法性。
- 当
-
计数“迷人数列” :
- 当窗口通过右移扩展并保持合法时,所有以
right为结束的子序列都是“迷人数列”,其数量为right - left + 1。
- 当窗口通过右移扩展并保持合法时,所有以
-
返回结果:
- 移动
right直到遍历整个数组,并累加每次得到的子序列数量,即为最终结果。
- 移动
关键点
-
双端队列的使用:
max_queue和min_queue分别维护窗口内的最大值和最小值效率地更新和获取。
-
滑动窗口的调整:
- 通过调整
left指针来保证窗口的“迷人数列”性质。
- 通过调整
-
复杂度:
- 由于每个元素最多被插入和删除一次,整个算法的时间复杂度是O(n),这确保了算法在处理大输入时依然高效。
代码
int solution(int n, int k, std::vector<int> sequence) {
deque<int> max_queue;
deque<int> min_queue;
int count=0;
int left=0,right=0;
while(right<n){
while(!max_queue.empty()&&sequence[right]>sequence[max_queue.back()]){
max_queue.pop_back();
}
max_queue.push_back(right);
while(!min_queue.empty()&&sequence[right]<sequence[min_queue.back()]){
min_queue.pop_back();
}
min_queue.push_back(right);
while (sequence[max_queue.front()] - sequence[min_queue.front()] > k) {
if (max_queue.front() == left) max_queue.pop_front();
if (min_queue.front() == left) min_queue.pop_front();
left++;
}
count += (right - left + 1);
++right;
}
return count;
}
算法复杂度
时间复杂度:O(n),因为每个元素只入队一次,出队一次。
空间复杂度:O(n),只额外建立了两个最多存放n个元素的队列。