前言
hello大家好,本期文章紧接着上期,讲述滑窗的下一个大分类——不定长。
定长滑窗请看我上期文章,有详细介绍。温馨提醒,代码大部分为手搓,答案方法不唯一。如果想要优雅的版本可以去找其他题解,我的代码只是会把注释标清楚。另外题目不会有很多,只是其中比较有代表性的节选。
不定长滑动窗口主要分为三类:求最长子数组,求最短子数组,以及求子数组个数。
注:滑动窗口相当于在维护一个队列。右指针的移动可以视作入队,左指针的移动可以视作出队。
一、求最长/最大
一般题目都会有‘最多’的要求,并且题目的解题套路与写法都很像。
1、基础部分
例题1 — 3.无重复字符的最长子串
给定一个字符串 s
,请你找出其中不含有重复字符的 最长 子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc"
,所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b"
,所以其长度为 1。
示例 3:
输入: s = "pwwkew" 输出: 3 解释: 因为无重复字符的最长子串是"wke"
,所以其长度为 3。请注意,你的答案必须是 子串 的长度,"pwke"
是一个子序列,不是子串。
提示:
0 <= s.length <= 5 * 104
s
由英文字母、数字、符号和空格组成
class Solution:def lengthOfLongestSubstring(self, s: str) -> int:g = defaultdict(int) #初始化字典用来计算字符数量res = ans = 0for i, x in enumerate(s): #通过i来计算滑窗的左右边界if g[x] <= 0: #当右侧可以纳入时g[x] += 1res += 1ans = max(ans, res)elif g[x] > 0:while res >= 0 and g[x] > 0: #不可以纳入时减到0为止g[s[i - res]] -= 1 #可以直接减不要判断,#因为滑窗内始终保持字典数字不大于1res -= 1 #要记得变化res的值,否则会死循环g[x] += 1res += 1ans = max(ans, res)return ans
#思路
用字典中字符的值来作为维护滑窗的依据,不满足要求时动滑窗的左端点直到符合题意。动态滑窗的长度就是要求的最大最小值。
例题2 — 1493.删掉一个元素以后全为一的最长子数组
给你一个二进制数组 nums
,你需要从中删掉一个元素。
请你在删掉元素的结果数组中,返回最长的且只包含 1 的非空子数组的长度。
如果不存在这样的子数组,请返回 0 。
提示 1:
输入:nums = [1,1,0,1] 输出:3 解释:删掉位置 2 的数后,[1,1,1] 包含 3 个 1 。
示例 2:
输入:nums = [0,1,1,1,0,1,1,0,1] 输出:5 解释:删掉位置 4 的数字后,[0,1,1,1,1,1,0,1] 的最长全 1 子数组为 [1,1,1,1,1] 。
示例 3:
输入:nums = [1,1,1] 输出:2 解释:你必须要删除一个元素。
提示:
1 <= nums.length <= 105
nums[i]
要么是0
要么是1
。
class Solution:def longestSubarray(self, nums: List[int]) -> int:cnt = left = ans = 0 #只需计算0的次数,不用单开数组节省空间for i, x in enumerate(nums):if x == 0:cnt += 1 if cnt > 1: #先判断是否需要移动左指针while cnt > 1 and left <= i: #常规循环变化left指针位置if nums[left] == 0:cnt -= 1left += 1ans = max(ans, i - left) #注意不要加一,因为最后答案要求不包含0ans = max(ans, i - left)return ans
#思路
与上题一样,通过while循环变换滑窗长度,动态求解最大值,只是这题不用计数多个字符,不用字典可以节约空间。
例题3 — 1208.尽可能使字符串相等
给你两个长度相同的字符串,s
和 t
。
将 s
中的第 i
个字符变到 t
中的第 i
个字符需要 |s[i] - t[i]|
的开销(开销可能为 0),也就是两个字符的 ASCII 码值的差的绝对值。
用于变更字符串的最大预算是 maxCost
。在转化字符串时,总开销应当小于等于该预算,这也意味着字符串的转化可能是不完全的。
如果你可以将 s
的子字符串转化为它在 t
中对应的子字符串,则返回可以转化的最大长度。
如果 s
中没有子字符串可以转化成 t
中对应的子字符串,则返回 0
。
示例 1:
输入:s = "abcd", t = "bcdf", maxCost = 3 输出:3 解释:s 中的 "abc" 可以变为 "bcd"。开销为 3,所以最大长度为 3。
示例 2:
输入:s = "abcd", t = "cdef", maxCost = 3
输出:1
解释:s 中的任一字符要想变成 t 中对应的字符,其开销都是 2。因此,最大长度为 1。
示例 3:
输入:s = "abcd", t = "acde", maxCost = 0 输出:1 解释:a -> a, cost = 0,字符串未发生变化,所以最大长度为 1。
提示:
1 <= s.length, t.length <= 10^5
0 <= maxCost <= 10^6
s
和t
都只含小写英文字母。
class Solution:def equalSubstring(self, s: str, t: str, maxCost: int) -> int:left = ans = num = 0 #用变量来存动态判断值,节省空间for i, x in enumerate(s):num += abs(ord(s[i]) - ord(t[i])) #ASCLL码的差的绝对值while num > maxCost: #常规循环,通过num的值来做滑窗变化的判断num -= abs(ord(s[left]) - ord(t[left]))left += 1 ans = max(ans, i - left + 1)return ans
#思路
与写法与前几题大致一样,只是参杂了一些函数的变化。另外不要老是使用字典来存值,太浪费空间。这题还有前缀和的解法,具体看官方题解。前缀和的专题会在后面的文章提及。
例题4 — 904.水果成篮
你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits
表示,其中 fruits[i]
是第 i
棵树上的水果 种类 。
你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:
- 你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
- 你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
- 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。
给你一个整数数组 fruits
,返回你可以收集的水果的 最大 数目。
示例 1:
输入:fruits = [1,2,1] 输出:3 解释:可以采摘全部 3 棵树。
示例 2:
输入:fruits = [0,1,2,2] 输出:3 解释:可以采摘 [1,2,2] 这三棵树。 如果从第一棵树开始采摘,则只能采摘 [0,1] 这两棵树。
示例 3:
输入:fruits = [1,2,3,2,2] 输出:4 解释:可以采摘 [2,3,2,2] 这四棵树。 如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。
示例 4:
输入:fruits = [3,3,3,1,2,1,1,2,3,3,4] 输出:5 解释:可以采摘 [1,2,1,1,2] 这五棵树。
提示:
1 <= fruits.length <= 105
0 <= fruits[i] < fruits.length
class Solution:def totalFruit(self, fruits: List[int]) -> int:cnt = defaultdict(int)ans = left = 0for i, x in enumerate(fruits):cnt[x] += 1while len(cnt) > 2:cnt[fruits[left]] -= 1if cnt[fruits[left]] == 0:del cnt[fruits[left]] #要去掉为0的键,不然前面计算长度会出错left += 1ans = max(ans, i - left + 1)return ans
#思路
这题需要一点点的理解能力,做多能摘多少果子,其实就是求最长滑窗的长度(因为一棵树只能摘一个果子),其限制条件就是两个篮子(最多两个相同的数)。搞明白这两点后,就可以按照常规的不定长滑窗的题目来写。但由于不知道有几种水果,这题只能是用字典。通过字典长度来更新滑窗长度,所以要记得把值为0的键去掉。
例题5 — 最大连续1的个数 III
给定一个二进制数组 nums
和一个整数 k
,假设最多可以翻转 k
个 0
,则返回执行操作后 数组中连续 1
的最大个数 。
示例 1:
输入:nums = [1,1,1,0,0,0,1,1,1,1,0], K = 2 输出:6 解释:[1,1,1,0,0,1,1,1,1,1,1] 粗体数字从 0 翻转到 1,最长的子数组长度为 6。
示例 2:
输入:nums = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], K = 3 输出:10 解释:[0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1] 粗体数字从 0 翻转到 1,最长的子数组长度为 10。
提示:
1 <= nums.length <= 105
nums[i]
不是0
就是1
0 <= k <= nums.length
class Solution:def longestOnes(self, nums: List[int], k: int) -> int:ans = left = cnt0 = 0for right, x in enumerate(nums):cnt0 += 1 - x # 0 变成 1,用来统计 cnt0while cnt0 > k:cnt0 -= 1 - nums[left]left += 1ans = max(ans, right - left + 1) #循环结束前统一更新ans的值return ans
#思路
这题看着人畜无害短短的一点点,但实际上他和前面题目过程中有很大的不一样,我第一次写的时候也栽了,虽然最后改对了,但整体代码思路会变得比较复杂。上面给大家放的是别人的答案。
这题的陷阱就在于,当0计数到大于k时,ans要更新的值其实是i - left,但是如果0计数不超过k时,ans要更新的值就是i - left + 1,这个大家画个图就明白了。其实每次滑窗右移完都变化一次ans的值就可以解决这个问题(如上面的代码),但如果是顺着目前这个浅显的思维走,就要用两个if来解决,把这两种情况区分开,代码就会复杂些,如下(如果更惨的没看出来,那最后几个测试点你会死活都过不了)。
class Solution:def longestOnes(self, nums: List[int], k: int) -> int:cnt = ans = left = 0for i, x in enumerate(nums):if x == 0:cnt += 1if cnt > k:ans = max(ans, i - left)while cnt > k and left <= i:if nums[left] == 0:cnt -= 1left += 1continue #避免重复更新,下面的值会比上面的大,那if就没意义了ans = max(ans, i - left + 1)return ans
2、进阶部分
需要在传统的排序上多思考一些临界条件以及会参杂其他算法的一些思想(还有可能是数学的计算,那就回难一点),或者题目会比较难读懂,整体题目难度会比基础灵活不少(但也没有特别难)。
例题6 — 2730.找到最长的半重复子串
给你一个下标从 0 开始的字符串 s
,这个字符串只包含 0
到 9
的数字字符。
如果一个字符串 t
中至多有一对相邻字符是相等的,那么称这个字符串 t
是 半重复的 。例如,"0010"
、"002020"
、"0123"
、"2002"
和 "54944"
是半重复字符串,而 "00101022"
(相邻的相同数字对是 00 和 22)和 "1101234883"
(相邻的相同数字对是 11 和 88)不是半重复字符串。
请你返回 s
中最长 半重复 子字符串 的长度。
示例 1:
输入:s = "52233"
输出:4
解释:
最长的半重复子字符串是 "5223"。整个字符串 "52233" 有两个相邻的相同数字对 22 和 33,但最多只能选取一个。
示例 2:
输入:s = "5494"
输出:4
解释:
s
是一个半重复字符串。
示例 3:
输入:s = "1111111"
输出:2
解释:
最长的半重复子字符串是 "11"。子字符串 "111" 有两个相邻的相同数字对,但最多允许选取一个。
提示:
1 <= s.length <= 50
'0' <= s[i] <= '9'
class Solution:def longestSemiRepetitiveSubstring(self, s: str) -> int:left = ans = res = cnt = 0 #初始化cnt用来标记上次连续字符的位置for i in range(len(s)):if i == len(s) - 1: #防止数组越界ans = max(ans, i - left + 1)continueif s[i] == s[i + 1] and res == 0:res = 1 #第一次相等的情况cnt = i + 1ans = max(ans, i - left + 1)continueif s[i] == s[i + 1] and res == 1: #后续相等情况ans = max(ans, i - left + 1)left = cnt #left接到上次相等的位置,res值不变,此时滑窗中必然有连续cnt = i + 1continueans = max(ans, i - left + 1) #除去上述条件,ans的值要更新,不然会漏答案return ans
#思路
我第一次写的时候,选择按照情况来分类写,比较直观,但是这些情况其实都是题目的坑。特别是cnt这个变量,大家容易想当然直接让left=i,这就错的很离谱了。
下面的代码是另一种写法,也是标准的while语句滑窗,大家也可以参考一下。
以下是它的思路
class Solution:def longestSemiRepetitiveSubstring(self, s: str) -> int:ans, left, same = 1, 0, 0for right in range(1, len(s)):same += s[right] == s[right - 1]if same > 1: # same == 2left += 1while s[left] != s[left - 1]:left += 1same = 1ans = max(ans, right - left + 1)return ans
例题7 — 2779.数组的最大美丽值
给你一个下标从 0 开始的整数数组 nums
和一个 非负 整数 k
。
在一步操作中,你可以执行下述指令:
- 在范围
[0, nums.length - 1]
中选择一个 此前没有选过 的下标i
。 - 将
nums[i]
替换为范围[nums[i] - k, nums[i] + k]
内的任一整数。
数组的 美丽值 定义为数组中由相等元素组成的最长子序列的长度。
对数组 nums
执行上述操作任意次后,返回数组可能取得的 最大 美丽值。
注意:你 只 能对每个下标执行 一次 此操作。
数组的 子序列 定义是:经由原数组删除一些元素(也可能不删除)得到的一个新数组,且在此过程中剩余元素的顺序不发生改变。
示例 1:
输入:nums = [4,6,1,2], k = 2 输出:3 解释:在这个示例中,我们执行下述操作: - 选择下标 1 ,将其替换为 4(从范围 [4,8] 中选出),此时 nums = [4,4,1,2] 。 - 选择下标 3 ,将其替换为 4(从范围 [0,4] 中选出),此时 nums = [4,4,1,4] 。 执行上述操作后,数组的美丽值是 3(子序列由下标 0 、1 、3 对应的元素组成)。 可以证明 3 是我们可以得到的由相等元素组成的最长子序列长度。
示例 2:
输入:nums = [1,1,1,1], k = 10 输出:4 解释:在这个示例中,我们无需执行任何操作。 数组 nums 的美丽值是 4(整个数组)。
提示:
1 <= nums.length <= 105
0 <= nums[i], k <= 105
class Solution:def maximumBeauty(self, nums: List[int], k: int) -> int:nums.sort() #原地排序,因为题目没说要按顺序left = ans = 0for i, x in enumerate(nums):while 2 * k < x - nums[left]: #数学转换left += 1ans = max(ans, i - left + 1)return ans
#思路
这题乍一看就很多,实际上它确实不简单。需要一点数学想法,再加上把原本的数组转化为可以滑窗的数组,我一开始写的时候也是没什么想法,直到看了一眼别人的图才恍然大悟。
这里放一下大佬的详细题解,我的思路和他差不多,个别参数不一样大家应该也能看懂,毕竟他写的挺细致的
“由于选的是子序列,且操作后子序列的元素都相等,所以元素顺序对答案没有影响,可以先对数组排序。
示例 1 排序后 nums=[1,2,4,6]。由于每个数 x 可以改成闭区间 [x−k,x+k] 中的数,我们把示例 1 的每个数看成闭区间,也就是
题目要求的「由相等元素组成的最长子序列」,相当于选出若干闭区间,这些区间的交集不为空。
排序后,选出的区间是连续的,我们只需考虑最左边的区间 [x−k,x+k] 和最右边的区间 [y−k,y+k],如果这两个区间的交集不为空,那么选出的这些区间的交集就不为空。也就是说,要满足
x+k≥y−k
即
y−x≤2k
于是原问题等价于:
排序后,找最长的连续子数组,其最大值减最小值 ≤2k。由于数组是有序的,相当于子数组的最后一个数减去子数组的第一个数 ≤2k。
只要子数组满足这个要求,对应的区间的交集就不为空,也就是子数组的元素都可以变成同一个数。
这可以用 滑动窗口 解决。枚举 nums[right] 作为子数组的最后一个数,一旦 nums[right]−nums[left]>2k,就移动左端点 left。
左端点停止移动时,下标在 [left,right] 的子数组就是满足要求的子数组,用子数组长度 right−left+1 更新答案的最大值。”
例题8 — 2516.每种字符至少取k个
给你一个由字符 'a'
、'b'
、'c'
组成的字符串 s
和一个非负整数 k
。每分钟,你可以选择取走 s
最左侧 还是 最右侧 的那个字符。
你必须取走每种字符 至少 k
个,返回需要的 最少 分钟数;如果无法取到,则返回 -1
。
示例 1:
输入:s = "aabaaaacaabc", k = 2 输出:8 解释: 从 s 的左侧取三个字符,现在共取到两个字符 'a' 、一个字符 'b' 。 从 s 的右侧取五个字符,现在共取到四个字符 'a' 、两个字符 'b' 和两个字符 'c' 。 共需要 3 + 5 = 8 分钟。 可以证明需要的最少分钟数是 8 。
示例 2:
输入:s = "a", k = 1 输出:-1 解释:无法取到一个字符 'b' 或者 'c',所以返回 -1 。
提示:
1 <= s.length <= 105
s
仅由字母'a'
、'b'
、'c'
组成0 <= k <= s.length
class Solution:def takeCharacters(self, s: str, k: int) -> int:cnt = Counter(s) #计算总的abc字符的值,这里返回一个字典if any(cnt[c] < k for c in 'abc' ): #python的便捷写法return -1ans = left = 0for i, x in enumerate(s): #用滑窗的方法求解最大剩余子串cnt[x] -= 1while cnt[x] < k: #字典内是要取走的字符,不够就要移动滑窗还回字符cnt[s[left]] += 1left += 1ans = max(ans, i - left + 1)return len(s) - ans #与总长相减就是答案
#思路
经典的逆向思维的题目,正着取走不好算,就反过来计算留下的长度,写多了一眼就能看出来。只要思想上绕过来这个弯这题就简单了。
比如 s 中有 3 个 a,4 个 b,5 个 c,k=2,每种字母至少取走 2 个,等价于剩下的字母至多有 1 个 a,2 个 b 和 3 个 c。
由于只能从 s 最左侧和最右侧取走字母,所以剩下的字母是 s 的子串。
设 s 中的 a,b,c 的个数分别为 x,y,z,现在问题变成:
计算 s 的最长子串长度,该子串满足 a,b,c 的个数分别至多为 x−k,y−k,z−k。
由于子串越短越能满足要求,越长越不能满足要求,有单调性,可以用滑动窗口解决。
实现思路
与其维护窗口内的字母个数,不如直接维护窗口外的字母个数,这也是我们取走的字母个数。
一开始,假设我们取走了所有的字母。或者说,初始窗口是空的,窗口外的字母个数就是 s 的每个字母的出现次数。
右端点字母进入窗口后,该字母取走的个数减一。
如果减一后,窗口外该字母的个数小于 k,说明子串太长了,或者取走的字母个数太少了,那么就不断右移左端点,把左端点字母移出窗口,相当于我们取走移出窗口的字母,直到该字母个数等于 k,退出内层循环。
内层循环结束后,用窗口长度 right−left+1 更新子串长度的最大值。
最后,原问题的答案为 n 减去子串长度的最大值。
特别地,如果 s 中某个字母的个数不足 k,那么无法满足题目要求,返回 −1。
例题9 — 2271.毯子覆盖的最多白色砖块数
给你一个二维整数数组 tiles
,其中 tiles[i] = [li, ri]
,表示所有在 li <= j <= ri
之间的每个瓷砖位置 j
都被涂成了白色。
同时给你一个整数 carpetLen
,表示可以放在 任何位置 的一块毯子的长度。
请你返回使用这块毯子,最多 可以盖住多少块瓷砖。
示例 1:
输入:tiles = [[1,5],[10,11],[12,18],[20,25],[30,32]], carpetLen = 10 输出:9 解释:将毯子从瓷砖 10 开始放置。 总共覆盖 9 块瓷砖,所以返回 9 。 注意可能有其他方案也可以覆盖 9 块瓷砖。 可以看出,瓷砖无法覆盖超过 9 块瓷砖。
示例 2:
输入:tiles = [[10,11],[1,1]], carpetLen = 2 输出:2 解释:将毯子从瓷砖 10 开始放置。 总共覆盖 2 块瓷砖,所以我们返回 2 。
提示:
1 <= tiles.length <= 5 * 104
tiles[i].length == 2
1 <= li <= ri <= 109
1 <= carpetLen <= 109
tiles
互相 不会重叠 。
class Solution:def maximumWhiteTiles(self, tiles: List[List[int]], carpetLen: int) -> int:tiles.sort(key = lambda x : x[0])#对原本二维数组排序,方便后续处理ans = res = left = temp = 0#初始化,这题可以说是滑窗也可以说是双指针,但其实两者本质上就没有什么太大的差别for p, q in tiles:res += q - p + 1#把数组里面每段白色的值先加上while q - carpetLen + 1 > tiles[left][1]:#当毯子左端点大于左指针所在的片段的右端点时,说明滑窗太长了res -= tiles[left][1] - tiles[left][0] + 1#扣掉left移动后移出滑窗的白色区域left += 1temp = max(q - carpetLen + 1 - tiles[left][0], 0)#计算未覆盖的区域,由于一开始毯子还没有完全移进来,所以是负数,不能减#这里是拿毯子的左端点减掉left指针在的白色区域的左端点,去掉未覆盖的区域#毯子的右端点一直和白色区域的右端点重合ans = max(ans, res - temp)return ans
#思路
这题确实是有点上强度了,有点难度并且不好理解。如果不考虑空间的情况下,是可以再搞一个数组出来,然后把题目变成标准的滑窗,但是那样复杂度就高了。如果直接在这个二维数组上运行,又不能直接整个毯子放上去,那样还要提前考虑后面可能的n个区域是否被覆盖,也很麻烦。这里提供一个大佬的思路,他是始终将毯子的右端点和白色区域的右端点重合,一点点把毯子放上去,把毯子作为一个定长滑窗,然后外侧套了一个双指针来更新白色区域被覆盖数的写法。
首先,将 tiles 按左端点 li
排序后,我们可以枚举毯子的摆放位置,然后计算毯子能覆盖多少块瓷砖。
实际上,毯子右端点放在一段瓷砖中间,是不如直接放在这段瓷砖右端点的(因为从中间向右移动,能覆盖的瓷砖数不会减少),所以可以枚举每段瓷砖的右端点来摆放毯子的右端点。
这样就可以双指针了,左指针 left 需要满足其指向的那段瓷砖的右端点被毯子覆盖。
关键点
设毯子右端点在瓷砖段 i 上,则毯子左端点位于 tiles[i][1]−carpetLen+1,对于 left 需要满足
tiles[left][1]≥tiles[i][1]−carpetLen+1
如果毯子左端点在瓷砖段 tiles[left] 内部,则覆盖的瓷砖数还需要额外减去这段瓷砖没被覆盖的部分,即减去
(tiles[i][1]−carpetLen+1)−tiles[left][0]
如果上式是负数则不减。
例题10 — 2106.摘水果(困难)
在一个无限的 x 坐标轴上,有许多水果分布在其中某些位置。给你一个二维整数数组 fruits
,其中 fruits[i] = [positioni, amounti]
表示共有 amounti
个水果放置在 positioni
上。fruits
已经按 positioni
升序排列 ,每个 positioni
互不相同 。
另给你两个整数 startPos
和 k
。最初,你位于 startPos
。从任何位置,你可以选择 向左或者向右 走。在 x 轴上每移动 一个单位 ,就记作 一步 。你总共可以走 最多 k
步。你每达到一个位置,都会摘掉全部的水果,水果也将从该位置消失(不会再生)。
返回你可以摘到水果的 最大总数 。
示例 1:
输入:fruits = [[2,8],[6,3],[8,6]], startPos = 5, k = 4 输出:9 解释: 最佳路线为: - 向右移动到位置 6 ,摘到 3 个水果 - 向右移动到位置 8 ,摘到 6 个水果 移动 3 步,共摘到 3 + 6 = 9 个水果
示例 2:
输入:fruits = [[0,9],[4,1],[5,7],[6,2],[7,4],[10,9]], startPos = 5, k = 4 输出:14 解释: 可以移动最多 k = 4 步,所以无法到达位置 0 和位置 10 。 最佳路线为: - 在初始位置 5 ,摘到 7 个水果 - 向左移动到位置 4 ,摘到 1 个水果 - 向右移动到位置 6 ,摘到 2 个水果 - 向右移动到位置 7 ,摘到 4 个水果 移动 1 + 3 = 4 步,共摘到 7 + 1 + 2 + 4 = 14 个水果
示例 3:
输入:fruits = [[0,3],[6,4],[8,5]], startPos = 3, k = 2 输出:0 解释: 最多可以移动 k = 2 步,无法到达任一有水果的地方
提示:
1 <= fruits.length <= 105
fruits[i].length == 2
0 <= startPos, positioni <= 2 * 105
- 对于任意
i > 0
,positioni-1 < positioni
均成立(下标从 0 开始计数) 1 <= amounti <= 104
0 <= k <= 2 * 105
class Solution:def maxTotalFruits(self, fruits: List[List[int]], startPos: int, k: int) -> int:#初始化左右两侧滑窗位置,先从向左走到底的方法#bisect-用二分查找来在有序列表中快速定位“插入点”,#bisect_left(a, x)返回第一个 i,使得 a[i] ≥ x;#也就是把 x 插到所有 == x 的元素 最左边。left = bisect_left(fruits, [startPos - k])right = bisect_left(fruits, [startPos + 1])ans = s = sum(c for _, c in fruits[left : right])#开始遍历,不断将新增数字加入while right < len(fruits) and fruits[right][0] <= startPos + k:s += fruits[right][1]#判断条件:无论先向左还是先向右,步数都大于k,则左侧滑窗端点移动while fruits[right][0] * 2 - fruits[left][0] - startPos > k and
fruits[right][0] - fruits[left][0] * 2 + startPos > k:s -= fruits[left][1]left += 1ans = max(ans, s)right += 1return ans
#思路
这题是在上一题写法上的进一步扩展,也是以离散点的形式给出滑窗的基本条件。如果仅仅用基础的不定长滑窗,将离散点转化成连续列表,那样时间复杂度会非常非常大,连第二个测试点都跑不完。
因为走的时候虽然可以来回走,但是走的过程一定是连续的,所以此题也可以用滑窗来解决。这里通过一个正确思路供大家参考(配合代码注释看的更明白)
‘由于只能一步步地走,人移动的范围必然是一段连续的区间。
如果反复左右移动,会白白浪费移动次数,所以最优方案要么先向右再向左,要么先向左再向右(或者向一个方向走到底)。
设向左走最远可以到达 fruits[left][0],这可以用枚举或者二分查找得出,其中 left 是最小的满足
fruits[left][0]≥startPos−k的下标。
假设位置不超过 startPos 的最近水果在 fruits[right][0],那么当 right 增加时,left 不可能减少,有单调性,因此可以用同向双指针(滑动窗口)解决。
如何判断 left 是否需要增加呢?
如果先向右再向左,那么移动距离为
(fruits[right][0]−startPos)+(fruits[right][0]−fruits[left][0])
如果先向左再向右,那么移动距离为
(startPos−fruits[left][0])+(fruits[right][0]−fruits[left][0])
如果上面两个式子均大于 k,就说明 fruits[left][0] 太远了,需要增加 left。
对于 right,它必须小于 n,且满足
fruits[right][0]≤startPos+k
移动 left 和 right 的同时,维护窗口内的水果数量 s,同时用 s 更新答案的最大值。’
例题11 — 2555.两个线段获得的最多奖品(动态规划+前缀和)
在 X轴 上有一些奖品。给你一个整数数组 prizePositions
,它按照 非递减 顺序排列,其中 prizePositions[i]
是第 i
件奖品的位置。数轴上一个位置可能会有多件奖品。再给你一个整数 k
。
你可以同时选择两个端点为整数的线段。每个线段的长度都必须是 k
。你可以获得位置在任一线段上的所有奖品(包括线段的两个端点)。注意,两个线段可能会有相交。
- 比方说
k = 2
,你可以选择线段[1, 3]
和[2, 4]
,你可以获得满足1 <= prizePositions[i] <= 3
或者2 <= prizePositions[i] <= 4
的所有奖品 i 。
请你返回在选择两个最优线段的前提下,可以获得的 最多 奖品数目。
示例 1:
输入:prizePositions = [1,1,2,2,3,3,5], k = 2 输出:7 解释:这个例子中,你可以选择线段 [1, 3] 和 [3, 5] ,获得 7 个奖品。
示例 2:
输入:prizePositions = [1,2,3,4], k = 0 输出:2 解释:这个例子中,一个选择是选择线段[3, 3]
和[4, 4]
,获得 2 个奖品。
提示:
1 <= prizePositions.length <= 105
1 <= prizePositions[i] <= 109
0 <= k <= 109
prizePositions
有序非递减。
class Solution:def maximizeWin(self, prizePositions: List[int], k: int) -> int:n = len(prizePositions)#如果奖品数组长度比两个线段长度小或等,说明一定可以全覆盖,可以简化计算直接返回if 2 * k + 1 >= prizePositions[n - 1] - prizePositions[0]:return nans = left = 0#用前缀和与动态规划的思想,假设mx[i]是前i部分线段可以取得的最大值mx = [0] * (n +1)for right, p in enumerate(prizePositions):#确保滑窗长度合法while p - prizePositions[left] > k:left += 1#mx[left]是左侧滑窗的值ans = max(ans, mx[left] + right - left + 1)#更新mx,利用动态规划思想,要么不取新的点,要么取,就看谁大mx[right + 1] = max(mx[right], right - left + 1)return ans
#思路
这题稍微有点变化,虽然也是离散点值,但难度不在滑窗遍历上,而在于如何安排两个可能连续又重合或者独立的线段。很明显因为不一定连续,所以不能把其合并成一个滑窗来计算。但是,换种角度想,两个滑窗其实是一样的长度,也就是说,前者遍历过的结果在后者也适用,就联想到前缀和并用动态规划更新。并且我们贪心一点想,一定是两个滑窗各自独立才会覆盖最大范围,从概率角度出发,这样一定是取到最大值。
参考了一份题解的思路,大家可以配合代码注释理解
两条线段一左一右。考虑枚举右(第二条线段),同时维护左(第一条线段)能覆盖的最多奖品个数。
贪心地想,两条线段不相交肯定比相交更好,覆盖的奖品可能更多。
设第二条线段右端点在 prizePositions[right] 时,最远(最小)覆盖的奖品的位置为 prizePositions[left]。
我们需要计算在 prizePositions[left] 左侧的第一条线段最多可以覆盖多少个奖品。这可以保证两条线段不相交。
定义 mx[i+1] 表示第一条线段右端点 ≤prizePositions[i] 时,最多可以覆盖多少个奖品。特别地,定义 mx[0]=0。
如何计算 mx?
考虑动态规划:
线段右端点等于 prizePositions[i] 时,可以覆盖最多的奖品,即 i−left +1。其中 left 表示右端点覆盖奖品 prizePositions[i] 时,最左边的被线段覆盖的奖品。
线段右端点小于 prizePositions[i] 时,可以覆盖最多的奖品,这等价于右端点 ≤prizePositions[i−1] 时,最多可以覆盖多少个奖品,即 mx[i]。注:这里可以说明为什么状态要定义成 mx[i+1] 而不是 mx[i],这可以避免当 i=0 时出现 i−1=−1 这种情况。
二者取最大值,得
mx[i+1]=max(mx[i],i−left +1)
上式也可以理解为 i−left +1 的前缀最大值。
如何计算两条线段可以覆盖的奖品个数?
第二条线段覆盖的奖品个数为 right−left+1。
第一条线段覆盖的奖品个数为线段右端点 ≤prizePositions[left−1] 时,最多覆盖的奖品个数,即 mx[left]。
综上,两条线段可以覆盖的奖品个数为
mx[left]+right−left+1
枚举 right 的过程中,取上式的最大值,即为答案。
我们遍历了所有的奖品作为第二条线段的右端点,通过 mx[left] 保证第一条线段与第二条线段不相交,且第一条线段覆盖了第二条线段左侧的最多奖品。那么这样遍历后,算出的答案就一定是所有情况中的最大值。
⚠注意:可以在计算第二条线段的滑动窗口的同时,更新和第一条线段有关的 mx。这是因为两条线段一样长,第二条线段移动到 right 时所覆盖的奖品个数,也是第一条线段移动到 right 时所覆盖的奖品个数。
小优化:如果 2k+1≥prizePositions[n−1]−prizePositions[0],说明所有奖品都可以被覆盖,直接返回 n。例如 prizePositions=[0,1,2,3], k=1,那么第一条线段覆盖 0 和 1,第二条线段覆盖 2 和 3,即可覆盖所有奖品。
例题12 — 2009.使数组连续的最少操作数(困难)
相关企业
提示
给你一个整数数组 nums
。每一次操作中,你可以将 nums
中 任意 一个元素替换成 任意 整数。
如果 nums
满足以下条件,那么它是 连续的 :
nums
中所有元素都是 互不相同 的。nums
中 最大 元素与 最小 元素的差等于nums.length - 1
。
比方说,nums = [4, 2, 5, 3]
是 连续的 ,但是 nums = [1, 2, 3, 5, 6]
不是连续的 。
请你返回使 nums
连续 的 最少 操作次数。
示例 1:
输入:nums = [4,2,5,3] 输出:0 解释:nums 已经是连续的了。
示例 2:
输入:nums = [1,2,3,5,6] 输出:1 解释:一个可能的解是将最后一个元素变为 4 。 结果数组为 [1,2,3,5,4] ,是连续数组。
示例 3:
输入:nums = [1,10,100,1000] 输出:3 解释:一个可能的解是: - 将第二个元素变为 2 。 - 将第三个元素变为 3 。 - 将第四个元素变为 4 。 结果数组为 [1,2,3,4] ,是连续数组。
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 109
class Solution:def minOperations(self, nums: List[int]) -> int:n = len(nums)#给出排序后的新数组arrarr = sorted(set(nums))left = ans = 0#这里注意是在排序后的数组上操作for i, c in enumerate(arr):#对nums的值进行判断,滑窗内连续的数长度是否满足小于n#测的是计算的值while arr[left] < c - n + 1:left += 1#更新ans的值,用的是arr的索引说明此时有几个数#更新的是实际的数ans = max(ans, i - left + 1)#扣掉最大原本就可以连续的部分,剩下的就是需要调整的部分return n - ans
#思路
经典的“正难则反”问题,和前面的逆向思维题目一样的思想。因为操作次数与顺序无关,可以对数组进行排序操作,此时不难看出,需要改变的均是数组两次的值,而中间的全是连续数组,可以使用滑窗,故此可以联想到对最大不用操作的求解。但是和传统滑窗不一样的点在于,滑窗更新的两个值,一个下标一个数值,都有不一样的含义。数值表示连续数的范围目前有几个数,属于抽象取值,是滑窗移动条件;而下标值表示实际的连续数数量,要拿来做实际计算更新答案。这里选取了一个简洁明了的思路,供大家参考。
“正难则反,考虑最多保留多少个元素不变。
设 x 是修改后的连续数字的最大值,则修改后的连续数字的范围为闭区间 [x−n+1,x],其中 n 是 nums 的长度。在修改前,对于已经在 [x−n+1,x] 中的数,我们无需修改。那么,x 取多少,可以让无需修改的数最多呢?
由于元素的位置不影响答案,且要求所有元素互不相同,我们可以将 nums 从小到大排序,并去掉重复元素。设 a 为 nums 排序去重后的数组。把 a[i] 画在一条数轴上,本题相当于有一个长度为 n 的滑动窗口,我们需要计算窗口内最多可以包含多少个数轴上的点。
定理:只需要枚举 a[i] 作为窗口的右端点。
证明:在窗口从左向右滑动的过程中,如果窗口右端点处没有点,那么继续滑动,在滑到下一个点之前,窗口内包含的点的个数是不会增多的。
为了算出窗口内有多少个点,我们需要知道窗口包含的最左边的点在哪,设这个点的位置是 a[left],则它必须大于等于窗口的左边界,即
a[left]≥a[i]−n+1
此时窗口内有 i−left+1 个点,取其最大值,就得到了最多保留不变的元素个数。最后用 n 减去保留不变的元素个数,就得到了答案。”
配合代码注释观看更佳
二、求最小、最短
题目大多会有最短/最小的要求,或者隐含的要求
例题13 — 209.长度最小的子数组
给定一个含有 n
个正整数的数组和一个正整数 target
。
找出该数组中满足其总和大于等于 target
的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr]
,并返回其长度。如果不存在符合条件的子数组,返回 0
。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3]
是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4] 输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1] 输出:0
提示:
1 <= target <= 109
1 <= nums.length <= 105
1 <= nums[i] <= 104
class Solution:def minSubArrayLen(self, target: int, nums: List[int]) -> int:n = len(nums)ans = n + 1 #也可以设为无穷大res = left = 0for i, x in enumerate(nums):res += x#当res满足条件时就更新ans的值while res >= target:ans = min(ans, i - left + 1)res -= nums[left]left += 1#如果ans值无变化就返回0return ans if ans < n + 1 else 0
#思路
按照题目要求用代码翻译即可,求最小子数组,连续部分一眼看出是滑窗解法,通过窗口不断遍历更新答案。在res第一次大于target的时候就直接while循环更新,就不会错漏。我是顺着思路写的,直接在循环中更新ans的值,也可以在循环外,但代码和逻辑上就复杂些,虽然速度会快。
例题14 — 2904.最短且字典序最小的子字符串
给你一个二进制字符串 s
和一个正整数 k
。
如果 s
的某个子字符串中 1
的个数恰好等于 k
,则称这个子字符串是一个 美丽子字符串 。
令 len
等于 最短 美丽子字符串的长度。
返回长度等于 len
且字典序 最小 的美丽子字符串。如果 s
中不含美丽子字符串,则返回一个 空 字符串。
对于相同长度的两个字符串 a
和 b
,如果在 a
和 b
出现不同的第一个位置上,a
中该位置上的字符严格大于 b
中的对应字符,则认为字符串 a
字典序 大于 字符串 b
。
- 例如,
"abcd"
的字典序大于"abcc"
,因为两个字符串出现不同的第一个位置对应第四个字符,而d
大于c
。
示例 1:
输入:s = "100011001", k = 3 输出:"11001" 解释:示例中共有 7 个美丽子字符串: 1. 子字符串 "100011001" 。 2. 子字符串 "100011001" 。 3. 子字符串 "100011001" 。 4. 子字符串 "100011001" 。 5. 子字符串 "100011001" 。 6. 子字符串 "100011001" 。 7. 子字符串 "100011001" 。 最短美丽子字符串的长度是 5 。 长度为 5 且字典序最小的美丽子字符串是子字符串 "11001" 。
示例 2:
输入:s = "1011", k = 2 输出:"11" 解释:示例中共有 3 个美丽子字符串: 1. 子字符串 "1011" 。 2. 子字符串 "1011" 。 3. 子字符串 "1011" 。 最短美丽子字符串的长度是 2 。 长度为 2 且字典序最小的美丽子字符串是子字符串 "11" 。
示例 3:
输入:s = "000", k = 1 输出:"" 解释:示例中不存在美丽子字符串。
提示:
1 <= s.length <= 100
1 <= k <= s.length
class Solution:def shortestBeautifulSubstring(self, s: str, k: int) -> str:#如果‘1’的和全部加起来都比k小,那就不用进入下面的循环判断了if s.count('1') < k:return ''left = res = 0ans = s #返回的值是字符串不是数字for i, x in enumerate(s):res += int(x) #将字符串转为整型才能数值相加# 在左端点处‘0’应该直接排出滑窗,才能保证最短while res > k or s[left] == '0':res -= int(s[left])left += 1if res == k:temp = s[left: i + 1]# 长度是第一条件,在相等情况下再看字典序,’or‘就是两个条件的分界if len(temp) < len(ans) or len(temp) == len(ans) and temp < ans:ans = tempreturn ans
#思路
由于答案中恰好有 k 个 1,我们可以用滑动窗口找最短的答案。
如果窗口内的 1 的个数超过 k,或者窗口端点是 0,就可以缩小窗口。
注:利用字符串哈希(或者后缀数组等),可以把比较字典序的时间降至 O(nlogn),这样可以做到 O(nlogn) 的时间复杂度。
例题15 — 1234.替换子串得到平衡字符串
有一个只含有 'Q', 'W', 'E', 'R'
四种字符,且长度为 n
的字符串。
假如在该字符串中,这四个字符都恰好出现 n/4
次,那么它就是一个「平衡字符串」。
给你一个这样的字符串 s
,请通过「替换一个子串」的方式,使原字符串 s
变成一个「平衡字符串」。
你可以用和「待替换子串」长度相同的 任何 其他字符串来完成替换。
请返回待替换子串的最小可能长度。
如果原字符串自身就是一个平衡字符串,则返回 0
示例 1:
输入:s = "QWER" 输出:0 解释:s 已经是平衡的了。
示例 2:
输入:s = "QQWE" 输出:1 解释:我们需要把一个 'Q' 替换成 'R',这样得到的 "RQWE" (或 "QRWE") 是平衡的。
示例 3:
输入:s = "QQQW" 输出:2 解释:我们可以把前面的 "QQ" 替换成 "ER"。
示例 4:
输入:s = "QQQQ" 输出:3 解释:我们可以替换后 3 个 'Q',使 s = "QWER"。
提示:
1 <= s.length <= 10^5
s.length
是4
的倍数s
中只含有'Q'
,'W'
,'E'
,'R'
四种字符
class Solution:def balancedString(self, s: str) -> int:m = len(s) // 4# 计算每种字符有几个,会形成一个字典dicts = Counter(s)# 当字典里面有四个字符且最大值或者最小值为m时就已经符合条件了if len(dicts) == 4 and max(dicts.values()) == m:return 0ans = len(s) + 1left = 0for i, x in enumerate(s):dicts[x] -= 1#循环判断,当最大的字典值小于等于m时有可能是正确答案while max(dicts.values()) <= m:ans = min(ans, i - left + 1)#要恢复之前变化的字典值在移动滑窗左端点前dicts[s[left]] += 1left += 1return ans
#思路
根据题意,如果在待替换子串之外的任意字符的出现次数超过 m= n/4
,那么无论怎么替换,都无法使这个字符在整个字符串中的出现次数为 m。
反过来说,如果在待替换子串之外的任意字符的出现次数都不超过 m,那么可以通过替换,使 s 为平衡字符串,即每个字符的出现次数均为 m。
这可以用滑动窗口实现.
对于本题,设子串的左右端点为 left 和 right,枚举 right,如果子串外的任意字符的出现次数都不超过 m,则说明从 left 到 right 的这段子串可以是待替换子串,用其长度 right−left+1 更新答案的最小值,并向右移动 left,缩小子串长度。
例题16 — 2875.无限数组的最短子数组
给你一个下标从 0 开始的数组 nums
和一个整数 target
。
下标从 0 开始的数组 infinite_nums
是通过无限地将 nums 的元素追加到自己之后生成的。
请你从 infinite_nums
中找出满足 元素和 等于 target
的 最短 子数组,并返回该子数组的长度。如果不存在满足条件的子数组,返回 -1
。
示例 1:
输入:nums = [1,2,3], target = 5 输出:2 解释:在这个例子中 infinite_nums = [1,2,3,1,2,3,1,2,...] 。 区间 [1,2] 内的子数组的元素和等于 target = 5 ,且长度 length = 2 。 可以证明,当元素和等于目标值 target = 5 时,2 是子数组的最短长度。
示例 2:
输入:nums = [1,1,1,2,3], target = 4 输出:2 解释:在这个例子中 infinite_nums = [1,1,1,2,3,1,1,1,2,3,1,1,...]. 区间 [4,5] 内的子数组的元素和等于 target = 4 ,且长度 length = 2 。 可以证明,当元素和等于目标值 target = 4 时,2 是子数组的最短长度。
示例 3:
输入:nums = [2,4,6,8], target = 3 输出:-1 解释:在这个例子中 infinite_nums = [2,4,6,8,2,4,6,8,...] 。 可以证明,不存在元素和等于目标值 target = 3 的子数组。
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 105
1 <= target <= 109
class Solution:def minSizeSubarray(self, nums: List[int], target: int) -> int:n = len(nums)left = res = 0ans = 2 * n + 1#计算需要遍历的部分temp = target % sum(nums)#因为取的余数,所以最多不会使用超过两倍长的数组#滑窗常规写法for i in range(2 * n):res += nums[i % n]while res > temp:res -= nums[left % n]left += 1#只有相等才更新if res == temp:#这里可以保证当temp = 0的时候,ans的值也会更新为0ans = min(ans, i - left + 1)#记得-1的情况和加上k个完整数组长度return ans + target // sum(nums) * n if ans < 2 * n + 1 else -1
#思路
这题难点不在滑窗,在于怎么处理最后的边界。如果循环暴力从头遍历到尾,会浪费很多空间和时间。如果只是常数级遍历,可能会漏掉很多情况。所以就需要先讲target进行数学变换,再滑窗求解。下面是具体思路,仅供参考。
三、结尾
这是滑窗部分的第二节学习笔记,题目不少,大家可以根据需要自取学习。下一篇可能要到七月份再出了。有问题欢迎评论区提出。上述所有代码思路都不唯一,只是个人觉得还不错的解法,大家可以自己思考或者去题解区寻找更优解或者更容易理解的解法。