今天以两道不同类型求子数组个数(方案数)的题为例,写一下不定长双指针滑动窗口的解题思路。
力扣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.包含所有三种字符的子字符串数目
给你一个字符串 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;
}