2024字节青训营笔记(九)子数组和的最大值问题 | 豆包MarsCode AI刷题

113 阅读5分钟

问题描述

小U手上有一个整数数组,他想知道如果从数组中删除任意一个元素后,能得到的长度为 k 的子数组和的最大值。你能帮小U计算出这个结果吗?
如果数组恰好为 k 个元素,那么不进行删除操作。


测试样例

样例1:

输入:n = 5,k = 3,nums = [2, 1, 3, -1, 4]
输出:8

样例2:

输入:n = 6,k = 2,nums = [-1, -1, 5, -2, 3, 4]
输出:8

样例3:

输入:n = 4,k = 2,nums = [-5, -3, 2, 1]
输出:3

解题思路

本题可以转换为维护一个大小为k+1的窗口,维护窗口内的最小值。子数组和可以通过前缀和快速求出,答案就是这段的子数组和减去窗口内的最小值。遍历数组,求出最大结果即可。

单调队列详解

队列内的元素值是单调的,递增或递减。

求滑动窗口的最小值: 用单调队列储存当前窗口内递增的元素,并且队头是窗口内的最小值,队尾是窗口内的尾元素, 队列从队头到队尾对应窗口内从最小值到尾元素的一个子序列。

求滑动窗口的最大值: 用单调队列储存当前窗口内递减的元素,并且队头是窗口内的最大值,队尾是窗口内的尾元素, 队列从队头到队尾对应窗口内从最大值到尾元素的一个子序列。


算法流程

下面以求滑动窗口的最大值为例:

1.队头出队

当队头元素从窗口滑出时,队头元素出队(hh++)

2.队尾入队

当新元素滑入窗口时,要把新元素从队尾插入,分两种情况:

①直接插入

如果新元素小于队尾元素,直接从队尾插入(++tt),因为他可能在前面的最大值滑出窗口后成为最大值

②先删后插

如果新元素大于等于队尾元素,那就删除队尾元素(因为他不可能成为窗口中的最大值),删除方法是tt--,即从队尾出队,循环删除,直到队空或者遇到一个大于新元素的值,插入其后(++tt)

这样做每次都能从队头取得窗口中的最大值。


图解分析过程

单调栈.png 如上图所示 给定数组2 7 9 8 5 5 1,窗口大小k为3

  1. 当i=1时,队空,2入队
  2. 当i=2时,当前元素7大于队尾元素2,2出队,7入队
  3. 当i=3时,当前元素9大于队尾元素7,7出队,9入队,此时i==k,窗口内最大值为9
  4. 当i=4时,当前元素8小于队尾元素9,8入队,此时i>k,窗口内最大值为9
  5. 当i=5时,当前元素5小于队尾元素8,5入队,此时i>k,窗口内最大值为9
  6. 当i=6时,队头元素9从窗口滑出,9出队,当前元素5等于队尾元素5,队尾元素5出队;当前元素5小于队尾元素8,5入队,此时i>k,,窗口内最大值为8
  7. 当i=7时,队头元素8从窗口滑出,8出队,当前元素1小于队尾元素5,1入队,此时i>k,窗口内最大值为5

所以滑动窗口位于每个位置时,窗口中的最大值为9 9 9 8 5


int q[N]; // 用于存储单调队列的索引

int solution(int n, int k, const std::vector<int>& nums) {
    // 如果数组长度等于k,直接返回数组元素的和
    if(n==k)return accumulate(nums.begin(),nums.end(),0);

    vector<int>s(n+1); // 前缀和数组,s[i]表示前i个元素的和

    // 计算前缀和数组,s[i]表示前i个元素的和
    partial_sum(nums.begin(),nums.end(),s.begin()+1);

    int ans=-1e9; // 初始化最大和为负无穷
    int hh=0,tt=-1; // 初始化单调队列的头尾指针

    for(int i=0;i<n;i++){
        // 如果队列不为空且队列头部的索引小于当前窗口的最小索引,弹出队列头部
        if(hh<=tt&&q[hh]<i-(k+1)+1)hh++;
        // 维护单调队列,确保队列中的元素是递增的
        while(hh<=tt&&nums[q[tt]]>=nums[i])tt--;
        q[++tt]=i; // 将当前索引加入队列
        if(i>=k){
            // 计算当前窗口的最大和,并更新ans
            ans=max(ans,s[i+1]-s[i+1-(k+1)]-nums[q[hh]]);
            // cout<<nums[q[hh]]<<" ";
        }
    }

    // cout<<ans<<'\n';

    return ans; // 返回最大和
}

STL双向队列写法
   //求窗口最大值
	deque<int>q;
	for(int i=0;i<n;i++)
	{
		if(!q.empty()&&q.front()<i-k+1)q.pop_front();
		//判断队头是否需要出队
		while(!q.empty()&&a[q.back()]<=a[i])q.pop_back();
		//维护队列单调性
		q.push_back(i);
		//下标入队,便于队头出队
		if(i>=k-1)cout<<a[q.front()]<<' ';
		//取队头元素作为窗口最大元素
		
	}

本题总结

本题的目标是找到在删除数组中的任意一个元素后,能够得到的长度为 k 的子数组和的最大值。我们需要通过以下步骤来实现:

1. 特殊情况处理

  • 如果数组长度 n 恰好等于 k,那么不需要删除任何元素,直接返回数组的总和。

2. 数据结构选择

  • 前缀和数组:用于快速计算任意子数组的和。
  • 单调队列:用于维护当前窗口内的最小值索引,以便在删除一个元素后,能够快速找到新的最大子数组和。

3. 算法步骤

  1. 计算前缀和:首先计算数组的前缀和数组 s,其中 s[i] 表示 nums 数组前 i 个元素的和。
  2. 滑动窗口:使用一个滑动窗口来维护当前窗口内的最小值索引。
  3. 更新最大值:在滑动窗口的过程中,计算删除一个元素后的子数组和,并更新最大值。

4. 具体实现

  • 初始化前缀和数组 s
  • 使用一个队列 q 来维护当前窗口内的最小值索引。
  • 遍历数组,更新队列 q,并计算删除一个元素后的子数组和,更新最大值。