迷人子序列计数问题思路

97 阅读3分钟

问题描述

在某些情况下,一个数列被称为“迷人数列”,如果这个数列的最大值和最小值之差不超过某个给定的阈值 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

在算法中一个常见的题型是分析连续子序列,本题的目标为:

给定一个长度为 n n 的数列,以及一个阈值k,找到数列中所有满足以下条件的连续子序列:

子序列的最大值和最小值之差不超过阈值k。 例如,对于数列 [3, 1, 2, 4] 和k=2,有8个连续子序列符合条件。如何高效地统计这些符合条件的子序列,是本题的关键。

思路分析

这道题的核心在于快速确定每个连续子序列的最大值和最小值是否满足条件。如果直接穷举所有可能的子序列,对于一个长度为n的数列,子序列的数量为 O(n^2)。对每个子序列逐个判断其最大值和最小值,时间复杂度会达到 O(n^3),显然效率过低。因此,我们需要一种高效的办法:

  1. 滑动窗口:使用滑动窗口法,通过两个指针(左指针和右指针)动态维护一个合法窗口。 当窗口内的最大值和最小值之差大于k时,移动窗口的左边界,使其重新变得合法。
  2. 双端队列:为了快速获取窗口内的最大值和最小值,使用双端队列分别维护递减和递增序列,便于动态调整窗口时快速查询最大值和最小值。
  3. 计算结果:对于每个右边界j,只要窗口内满足条件,就可以直接计算以j为右边界的所有子序列数量,数量为j−i+1(其中i为左边界)。

题解方案

算法步骤

  1. 初始化:使用两个双端队列分别维护当前窗口内的最大值和最小值。 左边界初始化为0,计数变量初始化为0。
  2. 右边界扩展:右边界从0开始遍历到n−1,将当前元素添加到双端队列中。更新双端队列,确保它们分别存储窗口内的递减序列(最大值队列)和递增序列(最小值队列)。
  3. 调整左边界: 如果当前窗口的最大值与最小值之差超过阈值 k k,需要移动左边界 i i,同时更新双端队列,直到窗口重新满足条件。
  4. 计算子序列数量: 每当右边界移动时,统计以当前右边界为终点的合法子序列数量,加入计数器中。
def solution(n, k, sequence):
    from collections import deque

    # 双端队列用于维护窗口内的最大值和最小值
    max_deque = deque()
    min_deque = deque()
    left = 0  # 滑动窗口左边界
    count = 0  # 计数结果

    for right in range(n):
        # 更新最大值队列
        while max_deque and sequence[max_deque[-1]] <= sequence[right]:
            max_deque.pop()
        max_deque.append(right)

        # 更新最小值队列
        while min_deque and sequence[min_deque[-1]] >= sequence[right]:
            min_deque.pop()
        min_deque.append(right)

        # 如果当前窗口不满足条件,移动左边界
        while sequence[max_deque[0]] - sequence[min_deque[0]] > k:
            if max_deque[0] == left:
                max_deque.popleft()
            if min_deque[0] == left:
                min_deque.popleft()
            left += 1

        # 统计以当前右边界为结尾的合法子序列数量
        count += right - left + 1

    return count