4.24算法题(不定长滑动窗口求方案数)

今天以两道不同类型求子数组个数(方案数)的题为例,写一下不定长双指针滑动窗口的解题思路。

力扣LCR 009.乘积小于k的子数组

LCR 009. 乘积小于 K 的子数组

给定一个正整数数组 nums和整数 k ,请找出该数组内乘积小于 k 的连续的子数组的个数。

示例 1:

输入: nums = [10,5,2,6], k = 100
输出: 8
解释: 8 个乘积小于 100 的子数组分别为: [10], [5], [2], [6], [10,5], [5,2], [2,6], [5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于100的子数组。

示例 2:

输入: nums = [1,2,3], k = 0
输出: 0

提示:

  • 1 <= nums.length <= 3 * 104
  • 1 <= nums[i] <= 1000
  • 0 <= k <= 106

注意:本题与主站 713 题相同:https://leetcode-cn.com/problems/subarray-product-less-than-k/

题目分析

找到连续的一段子数组,要求其中每个元素的乘积小于k。大于和等于都不要。

解题思路

拿到这道题,我们考虑用不定长滑动窗口解决。这个解法有点像双指针,也有点像不定长滑动窗口,所以就不管具体算什么方法了。我们来模拟一下得到答案的过程。我们首先进行初始化,定义左端点left为0,计数ans为0,乘积prod为1。这道题里面,我们使用固定右端点right移动左端点left的方法。设置for循环,枚举right,让prod每次乘上nums[right],然后进行判断。当prod大于等于k时就要开始移动left,让left++,并让prod除去此时窗口的第一个值,直到窗口内子数组乘积小于k。最后给ans加上**right-left+1**(后面解释原因)。

这个地方出现了一个疑问,判断prod大于等于k,**我们使用if还是while呢?**有些人觉得删掉前一个就行了,不需要多次判断啊,示例[10,5,2,6]就不用回退好几步。这个疑问其实很常见,尤其是在这类需要回退的题目里面,我们常常会把本该使用while的判断误写成if,比如用kmp算法求next数组的时候也会遇到这种问题。这就需要具体问题具体分析了。举一个例子:[10,9,10,4,3,8,3,3,6,2,10,10,9,3],k=19,假设此时right=4,left=3,此时[4,3]是符合要求的。接下来枚举移动right。于是right=5了,left没有变化依旧等于3。此时[4,3,8]不符合要求。我们如果使用if,就代表我们只会进行一步回退,只会把[4]踢出窗口,可是剩下的[3,8]也不符合要求,继续这么计算就会导致ans多加了1,导致后面的计算也出现问题,最后导致答案错误。我替大家运行过了,是真的(。

在这里插入图片描述

最后返回ans即可。

我们为什么给ans加的都是right-left+1呢?+1是哪来的?其实很好理解,我们随便举例都能证明这个规律。假设数组[10,5,2,6],k=100,right=0,left=0,如果只计算right-left,那就是0了,但实际上它们代表的子数组[10]明明是符合要求的,应该+1。对于这类**“越短越合法”**的题目,都写的是ans+=right-left+1。下面也放一下灵神对这个部分的解释:

越短越合法
一般要写 ans += right - left + 1

内层循环结束后,[left,right] 这个子数组是满足题目要求的。由于子数组越短,越能满足题目要求,所以除了 [left,right],还有 [left+1,right],[left+2,right],…,[right,right] 都是满足要求的。也就是说,当右端点固定在 right 时,左端点在 left,left+1,left+2,…,right 的所有子数组都是满足要求的,这一共有 right−left+1 个。

作者:灵茶山艾府
链接:https://leetcode.cn/discuss/post/0viNMK/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

代码实现

int numSubarrayProductLessThanK(int* nums, int numsSize, int k){
    if(k<=1) return 0;
    int left=0,ans=0,prod=1;
    for(int right=0;right<numsSize;right++){
        prod*=nums[right];
        while(prod>=k){
            prod/=nums[left];
            left++;
        }
        ans+=right-left+1;
    }
    return ans;
}

力扣1358.包含所有三种字符的子字符串数目

1358. 包含所有三种字符的子字符串数目

给你一个字符串 s ,它只包含三种字符 a, b 和 c 。

请你返回 a,b 和 c 都 至少 出现过一次的子字符串数目。

示例 1:

输入:s = “abcabc”
输出:10
解释:包含 a,b 和 c 各至少一次的子字符串为 “abc”, “abca”, “abcab”, “abcabc”, “bca”, “bcab”, “bcabc”, “cab”, “cabc” 和 “abc” (相同字符串算多次)。

示例 2:

输入:s = “aaacb”
输出:3
解释:包含 a,b 和 c 各至少一次的子字符串为 “aaacb”, “aacb” 和 “acb” 。

示例 3:

输入:s = “abc”
输出:1

提示:

  • 3 <= s.length <= 5 x 10^4
  • s 只包含字符 a,b 和 c 。

题目分析

找到一段连续子字符串,至少包含abc各一次,少于一次的不要。

解题思路

这道题就和上面的类型相反了,第一题求的是“至多”,这个题求的是“至少”。虽然第一题是求子数组这道题是求子字符串,但是套路还是一样的,使用的是固定右端点right移动左端点left的方法。在具体的操作上有些许不同:我们的初始化需要定义一个计数数组cnt,容量为3,初始值都为0。我们的计数原理是,用字符串s中的字符的ASCII码减去‘a’的ASCII码(比如cnt[s[right] - 'a']),对应cnt中的成员。因为题目中说了字符串s中只包含abc三种元素,所以用字符串s中的字符减‘a’的值也只会有三种结果,0,1,2,分别对应a,b,c在字符串s中出现的次数。只有cnt[0],cnt[1],cnt[2]同时不为零,才算此时的窗口里面包含abc至少各一次。

清楚了原理和判断方式,我们开始正式敲代码。第一步初始化,定义ans,left,数组cnt。接着枚举right,进入for循环,然后开始计数。以s = "abcabc"为例,此时的s[right]-‘a’等于0,cnt[0]对应的是对a的计数,让cnt[0]++。此时窗口里还只有a一个元素,不满足包含abc至少一次的条件,不进入while循环。等到循环进行到right=2时,此时窗口里包含a,b,c,满足了cnt[0] && cnt[1] && cnt[2]的条件,进入while循环。此时将left所代表的第一个字符踢出窗口,也就是cnt[s[left] - 'a']--,然后移动左端口left,让left++。最后更改ans为**ans+=left**。for循环结束后,返回ans。

在这类型题里面,为什么我们给ans加的是left呢?因为这个时候,我们的窗口内的字符串一定会保证包含abc至少一次,那么窗口左端点left以前的字符到右端点right所组成的子字符串,就都是符合要求的。从s[0]到s[left],不包括s[left]本身,总共有left个字符,对应着left个符合要求的子字符串,所以我们应该给ans每次加上left。下面也放上灵神对此的解释:

越长越合法
一般要写 ans += left

内层循环结束后,[left,right] 这个子数组是不满足题目要求的,但在退出循环之前的最后一轮循环,[left−1,right] 是满足题目要求的。由于子数组越长,越能满足题目要求,所以除了 [left−1,right],还有 [left−2,right],[left−3,right],…,[0,right] 都是满足要求的。也就是说,当右端点固定在 right 时,左端点在 0,1,2,…,left−1 的所有子数组都是满足要求的,这一共有 left 个。

作者:灵茶山艾府
链接:https://leetcode.cn/discuss/post/0viNMK/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

代码实现

int numberOfSubstrings(char* s) {
    int ans = 0, left = 0;
    int cnt[3] = {};
    for (int right = 0; s[right]; right++) {
        cnt[s[right] - 'a']++;
        while (cnt[0] && cnt[1] && cnt[2]) {
            cnt[s[left] - 'a']--;
            left++;
        }
        ans += left;
    }
    return ans;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值