📜  门|门 IT 2006 |问题 15(1)

📅  最后修改于: 2023-12-03 15:42:21.802000             🧑  作者: Mango

门|门 IT 2006 |问题 15

本题是门|门 IT 2006年的问题15,是一道经典的算法题,涉及到了二分查找、动态规划等算法思想。

题目描述

有一个长度为 $n$ 的整数数组 $nums$,你需要找到一个最长的连续子数组,其中最小值等于 $target$。

返回最长连续子数组的长度。

例子
输入:nums = [1,2,3,2,1], target = 2
输出:3
解释:最长子数组为 [2,3,2],长度为 3。
解题思路

我们可以使用二分查找来找到每个数作为最小值时的最长连续子数组长度。

首先,我们可以观察到一个性质,即对于任意的 $i$ 和 $j$,如果 $i < j$ 且 $nums_i \leq nums_j$,那么 $i$ 一定不是最小值为 $nums_i$ 的子数组的左端点。因为如果 $i$ 是最小值为 $nums_i$ 的子数组的左端点,那么由于 $nums_i \leq nums_j$,所以 $j$ 也是这个子数组的左端点,而这与 $i < j$ 矛盾。

因此,我们可以从左到右遍历数组 $nums$,使用一个单调栈来维护所有符合上述性质的下标。具体来说,栈中存放的下标的值是单调递增的,每当遍历到一个新的下标 $j$ 时,我们将栈顶的下标 $i$ 对应的 $nums_i$ 与 $nums_j$ 进行比较:

  • 如果 $nums_i = target$,那么以它为最小值的最长子数组的左端点 $i$ 就是栈顶的下标,我们将其弹出栈并计算答案。
  • 如果 $nums_i < nums_j$,那么 $i$ 不可能是以 $nums_i$ 为最小值的最长子数组的左端点,直接将其弹出栈。
  • 如果 $nums_i > nums_j$,那么 $j$ 不可能是以 $nums_i$ 为最小值的最长子数组的右端点,不做处理,让 $j$ 入栈。

但是,该做法的时间复杂度是 $O(n^2)$ 的,我们需要使用动态规划来进行优化。

具体来说,我们使用 $f(i,j)$ 表示 $nums_i\sim nums_j$ 中最小值为 $target$ 的最长连续子数组长度。其中 $i\leq j$。

根据上述的性质,我们知道如果 $k$ 是 $i\sim j$ 中最小值为 $target$ 的子数组的左端点,那么 $i\sim k-1$ 中不存在小于 $nums_k$ 的数,$k+1\sim j$ 中不存在小于 $nums_k$ 的数。因此,我们可以使用两个数组 $left$ 和 $right$ 来记录这些信息。

具体来说,$left_k$ 表示 $k$ 是 $i\sim j$ 中最小值为 $target$ 的子数组的左端点时,$k$ 左边第一个小于 $nums_k$ 的位置。因为 $left_k\leq k$,所以我们可以用 $nums_{left_k}$ 来表示 $left_k$。

同理,$right_k$ 表示 $k$ 是 $i\sim j$ 中最小值为 $target$ 的子数组的左端点时,$k$ 右边第一个小于 $nums_k$ 的位置。

然后,我们可以枚举 $i$,用 $left$ 和 $right$ 数组来计算出以 $nums_i$ 为最小值的最长连续子数组长度。具体来说,对于每个 $i$,我们枚举其所有可能的区间右端点 $j$,如果存在某个 $k$ 满足 $left_k\geq i$ 且 $right_k\leq j$,那么以 $nums_i$ 为最小值的最长连续子数组的长度就是 $j-i+1$。我们可以用一个变量 $ans$ 来记录所有连续子数组长度的最大值。

不难发现,$left$ 和 $right$ 数组满足单调栈的性质,因此可以使用单调栈来进行优化。具体来说,我们可以先使用单调栈来计算出 $left$ 数组,然后将 $nums$ 数组反转之后再使用单调栈来计算出 $right$ 数组,以便统计连续子数组长度。

时间复杂度

该算法中对于每个 $i$,需要枚举 $i$ 所在的连续子数组的右端点 $j$,因此总时间复杂度为 $O(n^2)$。使用单调栈进行优化后,时间复杂度为 $O(n)$。

代码实现
def solve(nums, target):
    n = len(nums)
    stack = []
    left = [-1] * n
    for i in range(n):
        while stack and nums[stack[-1]] >= nums[i]:
            stack.pop()
        if stack and nums[stack[-1]] == target:
            left[i] = stack[-1]
        stack.append(i)
    stack = []
    right = [-1] * n
    for i in range(n - 1, -1, -1):
        while stack and nums[stack[-1]] >= nums[i]:
            stack.pop()
        if stack and nums[stack[-1]] == target:
            right[i] = stack[-1]
        stack.append(i)
    ans = 0
    for i in range(n):
        j = right[i] + 1 if right[i] != -1 else n
        if left[i] < i and j < n:
            ans = max(ans, j - left[i] - 1)
    return ans

返回的markdown格式:

门|门 IT 2006 |问题 15

本题是门|门 IT 2006年的问题15,是一道经典的算法题,涉及到了二分查找、动态规划等算法思想。

题目描述

有一个长度为 $n$ 的整数数组 $nums$,你需要找到一个最长的连续子数组,其中最小值等于 $target$。

返回最长连续子数组的长度。

例子
输入:nums = [1,2,3,2,1], target = 2
输出:3
解释:最长子数组为 [2,3,2],长度为 3。
解题思路

我们可以使用二分查找来找到每个数作为最小值时的最长连续子数组长度。

首先,我们可以观察到一个性质,即对于任意的 $i$ 和 $j$,如果 $i < j$ 且 $nums_i \leq nums_j$,那么 $i$ 一定不是最小值为 $nums_i$ 的子数组的左端点。因为如果 $i$ 是最小值为 $nums_i$ 的子数组的左端点,那么由于 $nums_i \leq nums_j$,所以 $j$ 也是这个子数组的左端点,而这与 $i < j$ 矛盾。

因此,我们可以从左到右遍历数组 $nums$,使用一个单调栈来维护所有符合上述性质的下标。具体来说,栈中存放的下标的值是单调递增的,每当遍历到一个新的下标 $j$ 时,我们将栈顶的下标 $i$ 对应的 $nums_i$ 与 $nums_j$ 进行比较:

  • 如果 $nums_i = target$,那么以它为最小值的最长子数组的左端点 $i$ 就是栈顶的下标,我们将其弹出栈并计算答案。
  • 如果 $nums_i < nums_j$,那么 $i$ 不可能是以 $nums_i$ 为最小值的最长子数组的左端点,直接将其弹出栈。
  • 如果 $nums_i > nums_j$,那么 $j$ 不可能是以 $nums_i$ 为最小值的最长子数组的右端点,不做处理,让 $j$ 入栈。

但是,该做法的时间复杂度是 $O(n^2)$ 的,我们需要使用动态规划来进行优化。

具体来说,我们使用 $f(i,j)$ 表示 $nums_i\sim nums_j$ 中最小值为 $target$ 的最长连续子数组长度。其中 $i\leq j$。

根据上述的性质,我们知道如果 $k$ 是 $i\sim j$ 中最小值为 $target$ 的子数组的左端点,那么 $i\sim k-1$ 中不存在小于 $nums_k$ 的数,$k+1\sim j$ 中不存在小于 $nums_k$ 的数。因此,我们可以使用两个数组 $left$ 和 $right$ 来记录这些信息。

具体来说,$left_k$ 表示 $k$ 是 $i\sim j$ 中最小值为 $target$ 的子数组的左端点时,$k$ 左边第一个小于 $nums_k$ 的位置。因为 $left_k\leq k$,所以我们可以用 $nums_{left_k}$ 来表示 $left_k$。

同理,$right_k$ 表示 $k$ 是 $i\sim j$ 中最小值为 $target$ 的子数组的左端点时,$k$ 右边第一个小于 $nums_k$ 的位置。

然后,我们可以枚举 $i$,用 $left$ 和 $right$ 数组来计算出以 $nums_i$ 为最小值的最长连续子数组长度。具体来说,对于每个 $i$,我们枚举其所有可能的区间右端点 $j$,如果存在某个 $k$ 满足 $left_k\geq i$ 且 $right_k\leq j$,那么以 $nums_i$ 为最小值的最长连续子数组的长度就是 $j-i+1$。我们可以用一个变量 $ans$ 来记录所有连续子数组长度的最大值。

不难发现,$left$ 和 $right$ 数组满足单调栈的性质,因此可以使用单调栈来进行优化。具体来说,我们可以先使用单调栈来计算出 $left$ 数组,然后将 $nums$ 数组反转之后再使用单调栈来计算出 $right$ 数组,以便统计连续子数组长度。

时间复杂度

该算法中对于每个 $i$,需要枚举 $i$ 所在的连续子数组的右端点 $j$,因此总时间复杂度为 $O(n^2)$。使用单调栈进行优化后,时间复杂度为 $O(n)$。

代码实现
def solve(nums, target):
    n = len(nums)
    stack = []
    left = [-1] * n
    for i in range(n):
        while stack and nums[stack[-1]] >= nums[i]:
            stack.pop()
        if stack and nums[stack[-1]] == target:
            left[i] = stack[-1]
        stack.append(i)
    stack = []
    right = [-1] * n
    for i in range(n - 1, -1, -1):
        while stack and nums[stack[-1]] >= nums[i]:
            stack.pop()
        if stack and nums[stack[-1]] == target:
            right[i] = stack[-1]
        stack.append(i)
    ans = 0
    for i in range(n):
        j = right[i] + 1 if right[i] != -1 else n
        if left[i] < i and j < n:
            ans = max(ans, j - left[i] - 1)
    return ans