Sliding Windows

本质为同向双指针。大致来说就是当要统计符合某一性质的区间数量时,可以考虑左右指针选取一个窗口,不停地扩展窗口,边扩展边统计,直至窗口大小达到区间长度上限或窗口不再满足该性质为止。

说的再玄没用,看题。

1. 双周赛120 T3 LC 2972 统计移除递增子数组地数目Ⅱ

其实场上我想到一个O(n²)的滑窗。大致就是枚举窗口长度,然后将窗口从一开始移动到右指针等于区间末为止。移动过程中检查递增性质:

  1. 假设[lp,rp]为指定长度的待删除的子数组,则下一次滑动该区间将变为[lp+1,rp+1]

    左侧 : [0,lp]

    右侧 : [rp+2,n-1]

  2. 检查 nums[lp] < nums[rp+2] ,保证左右两侧交界处的单调性

  3. 而左右两侧的单调性,通过维护最长递增前缀和最长递增后缀实现。比如最长递增前缀的最右端为i,最长递增后缀的最左端为j。那么滑窗至少要覆盖[i+1,j-1],不然中间肯定不严格递增。

但可以看到,这里面充斥着lp+1,rp+2,i+1,j-1这样会导致下标越界的东西,所以最后我交了一发,里面充斥着大量特判,还是吃RE了,码力还是不够。

最后看了灵神题解,发现其实可以有O(n)的滑窗做法。记录于此。

滑窗思路还是一样的,删掉中间的一段,保证前面单调,后面单调,前面和后面交接处单调,这个交接处就是我说的nums[lp] 和 nums[rp+2]。保证了前后两段的单调递增。

既然前面要单调,那势必要把不单调的全删了,也就是我们要维护最长递增前缀[0,i],假设后缀的首个数字的索引是j,那么[i+1,j-1]的部分是一定要删掉的,这个东西没有选择的余地,不然不单增了。

而前方[0,i]的部分,是可选项。你当然可以选择只删掉[i+1,j-1],但无疑也可以删[i-k,j-1],其中k≤i。这是因为删完了之后,你将得到[0,i-k-1]和[j,n-1],而[0,i]是最长递增前缀,单增,所以[0,i-k-1]也是单增的。因此总计有

[0,j-1]
[1,j-1]
...
[i+1,j-1]

这i+2种删除前缀的方案。

对于后缀,我们可以直接从右向左枚举删除后的起始点j。只要保证nums[j]<nums[j+1]即可,这样后缀也能保证单调了。

细节在代码中

class Solution {
    public long incremovableSubarrayCount(int[] nums) {
        int n = nums.length;
        // 最长递增前缀
        int i = 0;
        while(i<n-1){
            if(nums[i]<nums[i+1]){
                i++;
            }else{
                break;
            }
        }
        // 特判整个数组严格递增
        // 长度为1的子数组 n个
        // 长度为2的子数组 n-1个
        // 长度为3的子数组 n-2个
        // ...
        // 长度为n的子数组 1个
        if(i == n-1){
            return (long) (1 + n) *n/2;
        }
        // 枚举删除后的后缀起始点j
        // 也就是最多删到j-1
        int j = n-1;
        // 那么删掉j的情况就要额外计算
        // 因为[0,i]是严格增的(最长递增前缀算过)
        // 可删的就是[0,j-1] [1,j-1] ... [i+1,j-1]
        // 删[i+1,j-1],前缀就是[0,i],递增,否则不严格递增(这是一定的,否则i不会停在这个位置)
        // 因此在前缀的递增性质得到保障之后,我们只要保障后缀也有递增性质即可
        long ans = i+2;
        while(j==n-1||nums[j]<nums[j+1]){
            // j为n-1时后续没有数字,要特判的
            while(i>=0 && nums[i]>=nums[j]){ // 注意nums[0]也是可以删的
                i--; // 保障前缀和后缀的交接处的严格递增性质
            }
            ans += i+2;
            j--;
        }
        return ans;
    }
}

别看while里套while,内层while由于i一定≤n-1,那么无论外层怎么套,最多总共也就执行O(n)次。因此时间复杂度是O(n)的。

2. LC 2831 找出最长等值子数组

这题一开始我的思路其实差不多对了。就是把每个本来连续的块的位置记录下来,存到哈希表,然后每个列表进行滑窗求最大值。

简单举个例子,nums = [1,1,2,2,1,1],以1为基值的块有两个,[0:1]和[4:5],那么当滑窗滑到[4:5]时,就会消耗掉4-1-1 = 2个删除元素的机会。当这个删除元素的机会不够,就要把滑窗左指针右移。比如k=1的时候。

from typing import List
from collections import defaultdict

class Solution:
    def longestEqualSubarray(self, nums: List[int], k: int) -> int:
        m = defaultdict(list)

        i = 0
        while i<len(nums):
            j = i
            while j<len(nums) and nums[i]==nums[j]:
                j += 1
            
            m[nums[i]].append((i,j-1))
            i = j
        
        ans = 0
        for v in m.values():
            tmp = v[0][1]-v[0][0]+1
            ans = max(ans,tmp)
            l = 0
            r = 1
            rest = k
            while r<len(v):
                useage = v[r][0]-v[r-1][1]-1
                if useage <= rest:
                    tmp += v[r][1]-v[r][0]+1
                    r += 1
                    ans = max(ans,tmp)
                    rest -= useage
                else:
                    l += 1
                    back = v[l][0]-v[l-1][1]-1
                    tmp -= (v[l-1][1]-v[l-1][0]+1)
                    rest += back
            ans = max(ans,tmp)
        
        return ans

随后这份代码跑的非常慢,压线过了差点T。后来我看了其他人的提交,发现不用维护块的信息,单个维护就可以了。计算消耗的方式是两个等值元素之间的元素个数(含这两个等值元素)减去除了等值元素以外的元素个数,这下哈希表维护每个基值的所有元素的索引就行了。

from typing import List
from collections import defaultdict

class Solution:
    def longestEqualSubarray(self, nums: List[int], k: int) -> int:
        m = defaultdict(list)

        for i in range(len(nums)):
            m[nums[i]].append(i)

        ans = 0
        for v in m.values():
            if len(v)<=ans:
                continue

            j = 0 
            for i in range(len(v)):
                if v[i]-v[j]+1 - (i-j+1) > k: # 实际距离差 减去 基值个数 例如 区间[3,10],里面有5个基值,那么总共消耗10-3+1-5=3个删除机会
                    j+=1
                
                ans = max(ans,i-j+1)
            
            
        return ans

这个复杂度看似O(n^2),实则O(n)

3. LC 1004/2024 最大连续1的个数Ⅲ/考试的最大困扰度

后者就是前者nums的数全部反转再做一次。

滑窗维护最大窗口长度。如果次数用完那么需要右移左指针。

class Solution:
    def longestOnes(self, nums: List[int], k: int) -> int:
        left,right,ans = 0,0,0

        cnt = 0
        while right<len(nums):
            cnt += 1-nums[right]
            while cnt>k:
                cnt -= 1-nums[left]
                left += 1
            ans = max(ans,right-left+1)
            right += 1
        
        return ans
class Solution:
    def maxConsecutiveAnswers(self, answerKey: str, k: int) -> int:
        aid1 = [1 if answerKey[i]=='T' else 0 for i in range(len(answerKey))]
        aid2 = [1 if answerKey[i]=='F' else 0 for i in range(len(answerKey))]

        return max(self.longestOnes(aid1,k),self.longestOnes(aid2,k))

    def longestOnes(self, nums: List[int], k: int) -> int:
        left,right,ans = 0,0,0

        cnt = 0
        while right<len(nums):
            cnt += 1-nums[right]
            while cnt>k:
                cnt -= 1-nums[left]
                left += 1
            ans = max(ans,right-left+1)
            right += 1
        
        return ans

第二题还有另外一个做法。如果一个区间里T和F的数量存在一个不大于K,那么就可以把这个不大于K的字符全部改掉。否则必须右移左指针。

class Solution:
    def maxConsecutiveAnswers(self, answerKey: str, k: int) -> int:
        left,T,F = 0,0,0

        for ch in answerKey:
            if ch=='T':
                T+=1
            else:
                F+=1
            if T>k and F>k:
                if answerKey[left]=='T':
                    T-=1
                else:
                    F-=1
                left+=1
        return len(answerKey)-left

4. LC 2555 两个线段获得的最多奖品

这题如果是一条线段的话那么直接滑窗就可以了。但现在有两条线段。

不难发现,对于任意一种两条线段相交的情况,必存在至少一种其他不相交的方案,使得后者获得的 奖品数不少于前者。

那么我们可以枚举后一条线段的位置,然后计算前一条线段能获得的最大奖品数。

后一条线段获得的最大奖品数可以用滑窗。对于前一条,我们可以定义dp(i+1)为第一条线段右端点≤prizePositions[i]情况下的可以获得的最大奖品数。

则:

dp(i+1) = max(dp(i),r-l+1) \\ dp(0) = 0

其中,r,l为当前滑窗的左右指针。

则答案为:

dp(l) + r-l+1

class Solution:
    def maximizeWin(self, prizePositions: List[int], k: int) -> int:
        n = len(prizePositions)
        if 2*k+1 >= prizePositions[-1]-prizePositions[0]:
            return n
        
        mx = [0 for _ in range(n+1)]
        l,ans = 0,0
        for r,pos in enumerate(prizePositions):
            while l<r and prizePositions[r]-prizePositions[l]>k:
                l += 1
            mx[r+1] = max(mx[r],r-l+1)
            ans = max(ans,mx[l]+r-l+1)
        return ans

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值