LeetCode Go · 滑动窗口(3) ​
来源与许可
题目版权归 LeetCode;题解来自 Doocs/leetcode,按 CC BY-SA 4.0 共享。补充 Go 代码来自灵茶山艾府、halfrost,保留 MIT 许可及逐题署名。本站提取 Go、调整分类及折叠排版;标注“本站整理”的 Go 解法依据题意与参考思路补写,按 CC BY-SA 4.0 共享。收录日期:2026-10-31;主题源版本:91ef8c241a5e。
本卷 30 题 · 点击题目展开,先独立思考,再查看解法。
2743. 计算没有重复字符的子字符串数量 🔒 ​
题目、示例与约束
给定你一个只包含小写英文字母的字符串 s 。如果一个子字符串不包含任何字符至少出现两次(换句话说,它不包含重复字符),则称其为 特殊 子字符串。你的任务是计算 特殊 子字符串的数量。例如,在字符串 "pop" 中,子串 "po" 是一个特殊子字符串,然而 "pop" 不是 特殊 子字符串(因为 'p' 出现了两次)。
返回 特殊 子字符串的数量。
子字符串 是指字符串中连续的字符序列。例如,"abc" 是 "abcd" 的一个子字符串,但 "acd" 不是。
示例 1:
输入:s = "abcd" 输出:10 解释:由于每个字符只出现一次,每个子串都是特殊子串。长度为 1 的子串有 4 个,长度为 2 的有 3 个,长度为 3 的有 2 个,长度为 4 的有 1 个。所以一共有 4 + 3 + 2 + 1 = 10 个特殊子串。
示例 2:
输入:s = "ooo" 输出:3 解释:任何长度至少为 2 的子串都包含重复字符。所以我们要计算长度为 1 的子串的数量,即 3 个。
示例 3:
输入:s = "abab" 输出:7 解释:特殊子串如下(按起始位置排序): 长度为 1 的特殊子串:"a", "b", "a", "b" 长度为 2 的特殊子串:"ab", "ba", "ab" 并且可以证明没有长度至少为 3 的特殊子串。所以答案是4 + 3 = 7。
提示:
1 <= s.length <= 105s只包含小写英文字母。
方法一:计数 + 双指针 · Go 实现
思考
统计无重复字符的子串个数。枚举端点再查重是平方,在 时不可接受。
右端点纳入字符后,若该字符计数超过 ,左端点右移直至再次互异。此时以右端点结尾的合法子串个数等于窗口长度,累加即可。
我们用两个指针 和 分别表示当前子串的左右边界,用一个长度为 的数组 统计当前子串中每个字符出现的次数。我们从左到右遍历字符串,每次遍历到位置 时,将 出现的次数加一,然后判断 是否出现了至少两次,如果是,那么我们需要将 出现的次数减一,并将 右移一位,直到 出现的次数不超过一次为止。这样一来,我们就得到以 结尾的最长特殊子串的长度,即 ,那么以 结尾的特殊子串的数量就是 。最后我们将每个位置结尾的特殊子串的数量累加起来,即为答案。
时间复杂度 ,空间复杂度 。其中 是字符串 的长度;而 是字符集的大小,这里字符集为小写英文字母,因此 。
func numberOfSpecialSubstrings(s string) (ans int) {
j := 0
cnt := [26]int{}
for i, c := range s {
k := c - 'a'
cnt[k]++
for cnt[k] > 1 {
cnt[s[j]-'a']--
j++
}
ans += i - j + 1
}
return
}
2747. 统计没有收到请求的服务器数目 ​
题目、示例与约束
给你一个整数 n ,表示服务器的总数目,再给你一个下标从 0 开始的 二维 整数数组 logs ,其中 logs[i] = [server_id, time] 表示 id 为 server_id 的服务器在 time 时收到了一个请求。
同时给你一个整数 x 和一个下标从 0 开始的整数数组 queries 。
请你返回一个长度等于 queries.length 的数组 arr ,其中 arr[i] 表示在时间区间 [queries[i] - x, queries[i]] 内没有收到请求的服务器数目。
注意时间区间是个闭区间。
示例 1:
输入:n = 3, logs = [[1,3],[2,6],[1,5]], x = 5, queries = [10,11] 输出:[1,2] 解释: 对于 queries[0]:id 为 1 和 2 的服务器在区间 [5, 10] 内收到了请求,所以只有服务器 3 没有收到请求。 对于 queries[1]:id 为 2 的服务器在区间 [6,11] 内收到了请求,所以 id 为 1 和 3 的服务器在这个时间段内没有收到请求。
示例 2:
输入:n = 3, logs = [[2,4],[2,1],[1,2],[3,1]], x = 2, queries = [3,4] 输出:[0,1] 解释: 对于 queries[0]:区间 [1, 3] 内所有服务器都收到了请求。 对于 queries[1]:只有 id 为 3 的服务器在区间 [2,4] 内没有收到请求。
提示:
1 <= n <= 1051 <= logs.length <= 1051 <= queries.length <= 105logs[i].length == 21 <= logs[i][0] <= n1 <= logs[i][1] <= 1061 <= x <= 105x < queries[i] <= 106
方法一:离线查询 + 排序 + 双指针 · Go 实现
思考
对每个时刻 ,统计在 内没有日志的服务器数。在线对每个询问扫日志是 。
询问离线按右端点排序,日志按时间排序。双指针把落入窗口的日志加入计数器、把过期的删去,窗口内出现过的服务器种类数用哈希表维护,答案为 减该种类数。
我们可以将所有的查询按照时间从小到大排序,然后按照时间顺序依次处理每个查询。
对于每个查询 ,其窗口左边界为 ,我们需要统计在窗口 内有多少个服务器收到了请求。我们用双指针 和 分别维护窗口的左右边界,初始时 。每一次,如果 指向的日志的时间小于等于 ,我们就将其加入到窗口中,然后将 向右移动一位。如果 指向的日志的时间小于 ,我们就将其从窗口中移除,然后将 向右移动一位。在移动的过程中,我们需要统计窗口中有多少个不同的服务器,这可以使用哈希表来实现。移动结束后,当前时间区间中没有收到请求的服务器数目就是 减去哈希表中不同的服务器数目。
时间复杂度 ,空间复杂度 。其中 和 分别是数组 的长度和服务器的数量,而 是数组 的长度。
func countServers(n int, logs [][]int, x int, queries []int) []int {
sort.Slice(logs, func(i, j int) bool { return logs[i][1] < logs[j][1] })
m := len(queries)
qs := make([][2]int, m)
for i, q := range queries {
qs[i] = [2]int{q, i}
}
sort.Slice(qs, func(i, j int) bool { return qs[i][0] < qs[j][0] })
cnt := map[int]int{}
ans := make([]int, m)
j, k := 0, 0
for _, q := range qs {
r, i := q[0], q[1]
l := r - x
for k < len(logs) && logs[k][1] <= r {
cnt[logs[k][0]]++
k++
}
for j < len(logs) && logs[j][1] < l {
cnt[logs[j][0]]--
if cnt[logs[j][0]] == 0 {
delete(cnt, logs[j][0])
}
j++
}
ans[i] = n - len(cnt)
}
return ans
}
2760. 最长奇偶子数组 ​
题目、示例与约束
给你一个下标从 0 开始的整数数组 nums 和一个整数 threshold 。
请你从 nums 的子数组中找出以下标 l 开头、下标 r 结尾 (0 <= l <= r < nums.length) 且满足以下条件的 最长子数组 :
nums[l] % 2 == 0- 对于范围
[l, r - 1]内的所有下标i,nums[i] % 2 != nums[i + 1] % 2 - 对于范围
[l, r]内的所有下标i,nums[i] <= threshold
以整数形式返回满足题目要求的最长子数组的长度。
注意:子数组 是数组中的一个连续非空元素序列。
示例 1:
输入:nums = [3,2,5,4], threshold = 5 输出:3 解释:在这个示例中,我们选择从 l = 1 开始、到 r = 3 结束的子数组 => [2,5,4] ,满足上述条件。 因此,答案就是这个子数组的长度 3 。可以证明 3 是满足题目要求的最大长度。
示例 2:
输入:nums = [1,2], threshold = 2 输出:1 解释: 在这个示例中,我们选择从 l = 1 开始、到 r = 1 结束的子数组 => [2] 。 该子数组满足上述全部条件。可以证明 1 是满足题目要求的最大长度。
示例 3:
输入:nums = [2,3,4,5], threshold = 4 输出:3 解释: 在这个示例中,我们选择从 l = 0 开始、到 r = 2 结束的子数组 => [2,3,4] 。 该子数组满足上述全部条件。 因此,答案就是这个子数组的长度 3 。可以证明 3 是满足题目要求的最大长度。
提示:
1 <= nums.length <= 1001 <= nums[i] <= 1001 <= threshold <= 100
方法一:枚举 · Go 实现
思考
最长子数组须从偶数起手、相邻奇偶交替,且每个元素不超过 。枚举左端再向右延伸,在 下可以接受。
对每个满足条件的左端 ,向右走到第一处奇偶相同或越阈值的位置,用 更新答案。
我们在 范围内枚举所有 ,如果 满足 并且 ,那么我们就从 开始,查找第一个不满足条件的 ,那么此时以 作为左端点的最长奇偶子数组的长度为 ,取所有 的最大值作为答案即可。
时间复杂度 ,其中 是数组 的长度。空间复杂度 。
func longestAlternatingSubarray(nums []int, threshold int) (ans int) {
n := len(nums)
for l := range nums {
if nums[l]%2 == 0 && nums[l] <= threshold {
r := l + 1
for r < n && nums[r]%2 != nums[r-1]%2 && nums[r] <= threshold {
r++
}
ans = max(ans, r-l)
}
}
return
}
方法二:枚举优化 · Go 实现
思考
方法一会从已判定失败的内部下标重新开窗口,重复扫描同一段。合法段互不相交,找到一段后把左端直接接到 ,整趟变为线性。
我们注意到,题目实际上会把数组划分成不相交的若干个满足条件的子数组,我们只需要找到这些子数组中最长的一个即可。因此,在枚举 和 时,我们不需要回退,只需要从左往右遍历一遍即可。
时间复杂度 ,其中 是数组 的长度。空间复杂度 。
func longestAlternatingSubarray(nums []int, threshold int) (ans int) {
for l, n := 0, len(nums); l < n; {
if nums[l]%2 == 0 && nums[l] <= threshold {
r := l + 1
for r < n && nums[r]%2 != nums[r-1]%2 && nums[r] <= threshold {
r++
}
ans = max(ans, r-l)
l = r
} else {
l++
}
}
return
}
2762. 不间断子数组 ​
题目、示例与约束
给你一个下标从 0 开始的整数数组 nums 。nums 的一个子数组如果满足以下条件,那么它是 不间断 的:
i,i + 1,...,j表示子数组中的下标。对于所有满足i <= i1, i2 <= j的下标对,都有0 <= |nums[i1] - nums[i2]| <= 2。
请你返回 不间断 子数组的总数目。
子数组是一个数组中一段连续 非空 的元素序列。
示例 1:
输入:nums = [5,4,2,4] 输出:8 解释: 大小为 1 的不间断子数组:[5], [4], [2], [4] 。 大小为 2 的不间断子数组:[5,4], [4,2], [2,4] 。 大小为 3 的不间断子数组:[4,2,4] 。 没有大小为 4 的不间断子数组。 不间断子数组的总数目为 4 + 3 + 1 = 8 。 除了这些以外,没有别的不间断子数组。
示例 2:
输入:nums = [1,2,3] 输出:6 解释: 大小为 1 的不间断子数组:[1], [2], [3] 。 大小为 2 的不间断子数组:[1,2], [2,3] 。 大小为 3 的不间断子数组:[1,2,3] 。 不间断子数组的总数目为 3 + 2 + 1 = 6 。
提示:
1 <= nums.length <= 1051 <= nums[i] <= 109
方法一:有序列表 + 双指针 · Go 实现
思考
子数组不间断当且仅当最大值与最小值之差不超过 ,统计这样的子数组个数。枚举端点再求最值是平方,长度 不可行。
右端点纳入新元素后,有序列表中的最大最小之差若超过 ,就弹出左端元素直至合法。此时以右端结尾的合法子数组个数等于表的大小。
我们可以用双指针 和 维护当前子数组的左右端点,用一个有序列表维护当前子数组的所有元素。
遍历数组 ,对于当前遍历到的数字 ,我们将其加到有序列表中,如果此时有序列表中的最大值与最小值的差值大于 ,那么我们循环右移指针 ,不断将 从有序列表中移出,直到列表为空或者有序列表元素的最大差值不大于 。此时不间断的子数组数目为 ,我们将其添加到答案中。
遍历结束,返回答案即可。
时间复杂度 ,空间复杂度 。其中 是数组 的长度。
func continuousSubarrays(nums []int) (ans int64) {
i := 0
tm := treemap.NewWithIntComparator()
for j, x := range nums {
if v, ok := tm.Get(x); ok {
tm.Put(x, v.(int)+1)
} else {
tm.Put(x, 1)
}
for {
a, _ := tm.Min()
b, _ := tm.Max()
if b.(int)-a.(int) > 2 {
if v, _ := tm.Get(nums[i]); v.(int) == 1 {
tm.Remove(nums[i])
} else {
tm.Put(nums[i], v.(int)-1)
}
i++
} else {
break
}
}
ans += int64(j - i + 1)
}
}
2781. 最长合法子字符串的长度 ​
题目、示例与约束
给你一个字符串 word 和一个字符串数组 forbidden 。
如果一个字符串不包含 forbidden 中的任何字符串,我们称这个字符串是 合法 的。
请你返回字符串 word 的一个 最长合法子字符串 的长度。
子字符串 指的是一个字符串中一段连续的字符,它可以为空。
示例 1:
输入:word = "cbaaaabc", forbidden = ["aaa","cb"] 输出:4 解释:总共有 11 个合法子字符串:"c", "b", "a", "ba", "aa", "bc", "baa", "aab", "ab", "abc" 和 "aabc"。最长合法子字符串的长度为 4 。 其他子字符串都要么包含 "aaa" ,要么包含 "cb" 。
示例 2:
输入:word = "leetcode", forbidden = ["de","le","e"] 输出:4 解释:总共有 11 个合法子字符串:"l" ,"t" ,"c" ,"o" ,"d" ,"tc" ,"co" ,"od" ,"tco" ,"cod" 和 "tcod" 。最长合法子字符串的长度为 4 。 所有其他子字符串都至少包含 "de" ,"le" 和 "e" 之一。
提示:
1 <= word.length <= 105word只包含小写英文字母。1 <= forbidden.length <= 1051 <= forbidden[i].length <= 10forbidden[i]只包含小写英文字母。
方法一:哈希表 + 双指针 · Go 实现
思考
合法子串不含 中的任一串为子串,求最长长度。禁用串长度不超过 ,但 长达 ,枚举全部子串不可行。
右端点 向右时,只需检查以 结尾、长度不超过 且仍落在当前左端右侧的后缀是否落在禁用集合中;一旦命中就把左端收到禁串之后。窗口长度的最大值即为答案。
我们用哈希表 记录所有禁止的字符串,然后用双指针 和 遍历字符串 ,其中 和 分别表示当前合法子字符串的左右边界。
接下来,我们枚举子字符串的右端点 ,判断此时 是否合法,如果不合法,那么我们更新 。接下来更新答案 。
时间复杂度 ,空间复杂度 。其中 和 分别表示字符串 和 的长度。
func longestValidSubstring(word string, forbidden []string) (ans int) {
s := map[string]bool{}
for _, x := range forbidden {
s[x] = true
}
n := len(word)
for i, j := 0, 0; j < n; j++ {
for k := j; k > max(j-10, i-1); k-- {
if s[word[k:j+1]] {
i = k + 1
break
}
}
ans = max(ans, j-i+1)
}
return
}
2799. 统计完全子数组的数目 ​
题目、示例与约束
给你一个由 正 整数组成的数组 nums 。
如果数组中的某个子数组满足下述条件,则称之为 完全子数组 :
- 子数组中 不同 元素的数目等于整个数组不同元素的数目。
返回数组中 完全子数组 的数目。
子数组 是数组中的一个连续非空序列。
示例 1:
输入:nums = [1,3,1,2,2] 输出:4 解释:完全子数组有:[1,3,1,2]、[1,3,1,2,2]、[3,1,2] 和 [3,1,2,2] 。
示例 2:
输入:nums = [5,5,5,5] 输出:10 解释:数组仅由整数 5 组成,所以任意子数组都满足完全子数组的条件。子数组的总数为 10 。
提示:
1 <= nums.length <= 10001 <= nums[i] <= 2000
方法一:哈希表 + 枚举 · Go 实现
思考
完全子数组须包含原数组的全部不同元素。枚举左右端点后对每个窗口建集合,在 下可以接受。
先求出全局种类数 ,再枚举左端、向右扩张集合,一旦种类数达到 就计入一个完全子数组。
我们先用哈希表统计数组中不同元素的数目,记为 。
接下来,我们枚举子数组的左端点下标 ,并维护一个集合 ,用于存储子数组中的元素。每次向右移动右端点下标 时,我们将 加入集合 中,并判断集合 的大小是否等于 。如果等于 ,则说明当前子数组是完全子数组,我们将答案增加 。
枚举结束后,返回答案即可。
时间复杂度 ,空间复杂度 。其中 是数组的长度。
func countCompleteSubarrays(nums []int) (ans int) {
s := map[int]bool{}
for _, x := range nums {
s[x] = true
}
cnt := len(s)
for i := range nums {
s = map[int]bool{}
for _, x := range nums[i:] {
s[x] = true
if len(s) == cnt {
ans++
}
}
}
return
}
方法二:哈希表 + 双指针 · Go 实现
思考
方法一对每个左端重建集合,重复统计同一窗口。种类数随右端单调不减:右端纳入新元素后,一旦窗口已完全,以该右端为结尾、左端不超过当前 的子数组都完全,个数为 ;再右移左端并维护计数。
与方法一类似,我们可以使用哈希表统计数组中不同元素的数目,记为 。
接下来,我们使用双指针维护一个滑动窗口,滑动窗口的右端点下标为 ,左端点下标为 。
每次固定左端点下标 ,然后向右移动右端点下标 ,当滑动窗口中的元素种类数等于 时,这意味着从左端点下标 到右端点下标 以及右侧的所有子数组都是完全子数组,我们将答案增加 ,其中 是数组的长度。然后我们将左端点下标 右移一位,继续上述过程。
时间复杂度 ,空间复杂度 。其中 是数组的长度。
func countCompleteSubarrays(nums []int) (ans int) {
d := map[int]int{}
for _, x := range nums {
d[x] = 1
}
cnt := len(d)
i, n := 0, len(nums)
d = map[int]int{}
for j, x := range nums {
d[x]++
for len(d) == cnt {
ans += n - j
d[nums[i]]--
if d[nums[i]] == 0 {
delete(d, nums[i])
}
i++
}
}
return
}
2841. 几乎唯一子数组的最大和 ​
题目、示例与约束
给你一个整数数组 nums 和两个正整数 m 和 k 。
请你返回 nums 中长度为 k 的 几乎唯一 子数组的 最大和 ,如果不存在几乎唯一子数组,请你返回 0 。
如果 nums 的一个子数组有至少 m 个互不相同的元素,我们称它是 几乎唯一 子数组。
子数组指的是一个数组中一段连续 非空 的元素序列。
示例 1:
输入:nums = [2,6,7,3,1,7], m = 3, k = 4 输出:18 解释:总共有 3 个长度为 k = 4 的几乎唯一子数组。分别为 [2, 6, 7, 3] ,[6, 7, 3, 1] 和 [7, 3, 1, 7] 。这些子数组中,和最大的是 [2, 6, 7, 3] ,和为 18 。
示例 2:
输入:nums = [5,9,9,2,4,5,4], m = 1, k = 3 输出:23 解释:总共有 5 个长度为 k = 3 的几乎唯一子数组。分别为 [5, 9, 9] ,[9, 9, 2] ,[9, 2, 4] ,[2, 4, 5] 和 [4, 5, 4] 。这些子数组中,和最大的是 [5, 9, 9] ,和为 23 。
示例 3:
输入:nums = [1,2,1,2,1,2,1], m = 3, k = 3 输出:0 解释:输入数组中不存在长度为k = 3的子数组含有至少m = 3个互不相同元素的子数组。所以不存在几乎唯一子数组,最大和为 0 。
提示:
1 <= nums.length <= 2 * 1041 <= m <= k <= nums.length1 <= nums[i] <= 109
方法一:滑动窗口 + 哈希表 · Go 实现
思考
长度为 且不同元素不少于 的子数组中,要求和最大。固定长度适合滑动窗口:用哈希表维护窗口内频次与元素和,种类数达到 时更新答案。
我们可以遍历数组 ,维护一个大小为 的窗口,用哈希表 统计窗口中每个元素的出现次数,用变量 统计窗口中所有元素的和。如果 中不同元素的个数大于等于 ,那么我们就更新答案 。
遍历结束后,返回答案即可。
时间复杂度 ,空间复杂度 。其中 是数组的长度。
func maxSum(nums []int, m int, k int) int64 {
cnt := map[int]int{}
var s int64
for _, x := range nums[:k] {
cnt[x]++
s += int64(x)
}
var ans int64
if len(cnt) >= m {
ans = s
}
for i := k; i < len(nums); i++ {
cnt[nums[i]]++
cnt[nums[i-k]]--
if cnt[nums[i-k]] == 0 {
delete(cnt, nums[i-k])
}
s += int64(nums[i]) - int64(nums[i-k])
if len(cnt) >= m {
ans = max(ans, s)
}
}
return ans
}
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 <= 1051 <= nums[i] <= 1051 <= target <= 109
方法一:前缀和 + 哈希表 · Go 实现
思考
数组无限拼接,目标和可为若干整圈加上一段最短子数组(或一段补到整圈的最短缺口)。先用整圈个数消化 ,剩余部分在一轮前缀和上用哈希表查找等于剩余值或等于 剩余值的最短段。元素不超过 时前缀和仍会超过 ,哈希键要用 64 位整数。32 位键会把相差 的前缀和看成同一个位置,从而返回一段并不存在的子数组。
我们先算出数组 的元素总和,记为 。
如果 ,那么我们可以将 减去 ,这样就可以将 减小到 的范围内。那么此时子数组的长度为 ,其中 是数组 的长度。
接下来,我们只需要在数组 中,找出长度最短的且元素和等于 的子数组,或者长度最短的且前缀和加上后缀和等于 ,即子数组元素和等于 的子数组,记长度为 。我们可以通过前缀和加哈希表的方法,找出这样的子数组。
如果找到了这样的子数组,那么最终的答案就是 。否则,答案就是 。
时间复杂度 ,空间复杂度 。其中 是数组 的长度。
func minSizeSubarray(nums []int, target int) int {
s := 0
for _, x := range nums {
s += x
}
n := len(nums)
a := 0
if target > s {
a = n * (target / s)
target -= target / s * s
}
if target == s {
return n
}
pos := map[int]int{0: -1}
pre := 0
b := 1 << 30
for i, x := range nums {
pre += x
if j, ok := pos[pre-target]; ok {
b = min(b, i-j)
}
if j, ok := pos[pre-(s-target)]; ok {
b = min(b, n-(i-j))
}
pos[pre] = i
}
if b == 1<<30 {
return -1
}
return a + b
}
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 <= 1001 <= k <= s.length
方法一:枚举 · Go 实现
思考
美丽子串含恰好 个 。 ,枚举全部 个子串并统计 的个数可行。对每个合法子串,按「更短优先,长度相同则字典序更小」更新答案即可。
实现中左端点 固定后,右端点从 起扫,因至少需要 个字符才能放下 个 。
我们可以枚举所有子字符串 ,其中 ,并检查它们是否是美丽子字符串。如果是,我们就更新答案。
时间复杂度 ,空间复杂度 。其中 是字符串 的长度。
func shortestBeautifulSubstring(s string, k int) (ans string) {
n := len(s)
for i := 0; i < n; i++ {
for j := i + k; j <= n; j++ {
t := s[i:j]
cnt := 0
for _, c := range t {
if c == '1' {
cnt++
}
}
if cnt == k && (ans == "" || j-i < len(ans) || (j-i == len(ans) && t < ans)) {
ans = t
}
}
}
return
}
方法二:双指针 · Go 实现
思考
枚举在 时尚可,但对每个子串调用 会重复扫描同一区间。美丽子串对应窗口内 的个数恰为 ,且去掉前导 不会减少 的个数,因而最短候选必无前导零。
用双指针维护窗口:右端纳入字符,当 的个数超过 或左端为 时收缩左端。每当个数恰为 ,再与当前最优串比较长度与字典序。
我们也可以用两个指针维护一个滑动窗口,其中指针 指向滑动窗口的左端点,指针 指向滑动窗口的右端点。初始时 。另外,我们用变量 记录滑动窗口中的 的个数。
我们首先将指针 向右移动,将 加入到滑动窗口中,并更新 。如果 ,或者 并且 ,我们就循环将指针 往右移动,并且更新 。
当 时,我们就找到了一个美丽子字符串。我们将它与当前的答案进行比较,并更新答案。
时间复杂度 ,空间复杂度 。其中 是字符串 的长度。
func shortestBeautifulSubstring(s string, k int) (ans string) {
i, j, cnt := 0, 0, 0
n := len(s)
for j < n {
cnt += int(s[j] - '0')
for cnt > k || (i < j && s[i] == '0') {
cnt -= int(s[i] - '0')
i++
}
j++
t := s[i:j]
if cnt == k && (ans == "" || j-i < len(ans) || (j-i == len(ans) && t < ans)) {
ans = t
}
}
return
}
2953. 统计完全子字符串 ​
题目、示例与约束
给你一个字符串 word 和一个整数 k 。
如果 word 的一个子字符串 s 满足以下条件,我们称它是 完全字符串:
s中每个字符 恰好 出现k次。- 相邻字符在字母表中的顺序 至多 相差
2。也就是说,s中两个相邻字符c1和c2,它们在字母表中的位置相差 至多 为2。
请你返回 word 中 完全 子字符串的数目。
子字符串 指的是一个字符串中一段连续 非空 的字符序列。
示例 1:
输入:word = "igigee", k = 2 输出:3 解释:完全子字符串需要满足每个字符恰好出现 2 次,且相邻字符相差至多为 2 :igigee, igigee, igigee 。
示例 2:
输入:word = "aaabbbccc", k = 3 输出:6 解释:完全子字符串需要满足每个字符恰好出现 3 次,且相邻字符相差至多为 2 :aaabbbccc, aaabbbccc, aaabbbccc, aaabbbccc, aaabbbccc, aaabbbccc 。
提示:
1 <= word.length <= 105word只包含小写英文字母。1 <= k <= word.length
方法一:枚举字符种类数 + 滑动窗口 · Go 实现
思考
完全子串要求每种出现的字符恰 次,且相邻字符码差不超过 。后一条件把原串切成若干互不跨越的段,答案为各段之和。段内字符种类 ,可枚举种类数 ,窗口长固定为 。
窗口内用 与 维护「出现次数的次数」, 时计入。段与窗口都是线性滑动, 。
根据题目描述中的条件 ,我们可以发现,一个完全字符串中,相邻两个字符之差不超过 。因此,我们遍历字符串 ,可以利用双指针把 分割成若干个子字符串,这些子字符串中的字符种类数不超过 ,且相邻字符之差不超过 。接下来,我们只需要在每个子字符串中,统计每个字符都出现 次的子字符串的个数即可。
我们定义一个函数 ,它的功能是统计字符串 中每个字符都出现 次的子字符串的个数。由于 中的字符种类数不超过 ,因此我们可以枚举每个字符种类数 ,其中 ,那么每个字符种类数为 的子字符串的长度为 。
我们可以用一个数组或哈希表 维护一个长度为 的滑动窗口中每个字符出现的次数,用另一个哈希表 维护每个次数出现的次数。如果 ,即有 个字符都出现了 次,那么我们就找到了一个满足条件的子字符串。我们可以用双指针维护这个滑动窗口,每次移动右指针时,我们将右指针指向的字符出现的次数加一,并更新 数组;每次移动左指针时,我们将左指针指向的字符出现的次数减一,并更新 数组。在每次移动指针后,我们都判断 是否等于 ,如果等于则说明我们找到了一个满足条件的子字符串。
时间复杂度 ,空间复杂度 ,其中 是字符串 的长度;而 是字符集的大小,本题中字符集为小写英文字母,因此 。
func countCompleteSubstrings(word string, k int) (ans int) {
n := len(word)
f := func(s string) (ans int) {
m := len(s)
for i := 1; i <= 26 && i*k <= m; i++ {
l := i * k
cnt := [26]int{}
for j := 0; j < l; j++ {
cnt[int(s[j]-'a')]++
}
freq := map[int]int{}
for _, x := range cnt {
if x > 0 {
freq[x]++
}
}
if freq[k] == i {
ans++
}
for j := l; j < m; j++ {
a := int(s[j] - 'a')
b := int(s[j-l] - 'a')
freq[cnt[a]]--
cnt[a]++
freq[cnt[a]]++
freq[cnt[b]]--
cnt[b]--
freq[cnt[b]]++
if freq[k] == i {
ans++
}
}
}
return
}
for i := 0; i < n; {
j := i + 1
for j < n && abs(int(word[j])-int(word[j-1])) <= 2 {
j++
}
ans += f(word[i:j])
i = j
}
return
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
2958. 最多 K 个重复元素的最长子数组 ​
题目、示例与约束
给你一个整数数组 nums 和一个整数 k 。
一个元素 x 在数组中的 频率 指的是它在数组中的出现次数。
如果一个数组中所有元素的频率都 小于等于 k ,那么我们称这个数组是 好 数组。
请你返回 nums 中 最长好 子数组的长度。
子数组 指的是一个数组中一段连续非空的元素序列。
示例 1:
输入:nums = [1,2,3,1,2,3,1,2], k = 2 输出:6 解释:最长好子数组是 [1,2,3,1,2,3] ,值 1 ,2 和 3 在子数组中的频率都没有超过 k = 2 。[2,3,1,2,3,1] 和 [3,1,2,3,1,2] 也是好子数组。 最长好子数组的长度为 6 。
示例 2:
输入:nums = [1,2,1,2,1,2,1,2], k = 1 输出:2 解释:最长好子数组是 [1,2] ,值 1 和 2 在子数组中的频率都没有超过 k = 1 。[2,1] 也是好子数组。 最长好子数组的长度为 2 。
示例 3:
输入:nums = [5,5,5,5,5,5,5], k = 4 输出:4 解释:最长好子数组是 [5,5,5,5] ,值 5 在子数组中的频率没有超过 k = 4 。 最长好子数组的长度为 4 。
提示:
1 <= nums.length <= 1051 <= nums[i] <= 1091 <= k <= nums.length
方法一:双指针 · Go 实现
思考
求每个值出现次数均 的最长子数组。 ,不能枚举端点。频率约束对右端单调:右端右移只可能违约,左端右移只可能修复。
哈希表计数,右指针纳入 后若 则收缩左端。窗口合法时更新长度。
我们可以用两个指针 和 分别表示子数组的左右端点,初始时两个指针都指向数组的第一个元素。
接下来,我们遍历数组 中的每个元素 ,对于每个元素 ,我们将 的出现次数加一,然后判断当前子数组是否满足要求。如果当前子数组不满足要求,我们就将指针 右移一位,并将 的出现次数减一,直到当前子数组满足要求为止。然后我们更新答案 。继续遍历,直到 到达数组的末尾。
时间复杂度 ,空间复杂度 。其中 是数组 的长度。
func maxSubarrayLength(nums []int, k int) (ans int) {
cnt := make(map[int]int)
for l, r := 0, 0; r < len(nums); r++ {
cnt[nums[r]]++
for cnt[nums[r]] > k {
cnt[nums[l]]--
l++
}
ans = max(ans, r-l+1)
}
return
}
2962. 统计最大元素出现至少 K 次的子数组 ​
题目、示例与约束
给你一个整数数组 nums 和一个 正整数 k 。
请你统计有多少满足 「 nums 中的 最大 元素」至少出现 k 次的子数组,并返回满足这一条件的子数组的数目。
子数组是数组中的一个连续元素序列。
示例 1:
输入:nums = [1,3,2,3,3], k = 2 输出:6 解释:包含元素 3 至少 2 次的子数组为:[1,3,2,3]、[1,3,2,3,3]、[3,2,3]、[3,2,3,3]、[2,3,3] 和 [3,3] 。
示例 2:
输入:nums = [1,4,2,1], k = 3 输出:0 解释:没有子数组包含元素 4 至少 3 次。
提示:
1 <= nums.length <= 1051 <= nums[i] <= 1061 <= k <= 105
方法一:双指针 · Go 实现
思考
子数组须包含全局最大值至少 次。 ,不能枚举。固定左端后,使窗口内 出现次数达到 的最小右端单调,其后所有右端都合法。
双指针维护 ,左端每进一步就尽可能右移 ,把 加入答案,再扣掉离开窗口的 。
不妨记数组中的最大值为 。
我们用两个指针 和 维护一个滑动窗口,使得 之间的子数组中,有 个元素等于 。如果我们固定左端点 ,那么所有大于等于 的右端点都满足条件,一共有 个。
因此,我们枚举左端点 ,用指针 维护右端点,用一个变量 记录当前窗口中等于 的元素个数,当 大于等于 时,我们就找到了满足条件的子数组,将答案增加 。然后我们更新 ,继续枚举下一个左端点。
时间复杂度 ,其中 是数组 的长度。空间复杂度 。
func countSubarrays(nums []int, k int) (ans int64) {
mx := slices.Max(nums)
n := len(nums)
cnt, j := 0, 0
for _, x := range nums {
for ; j < n && cnt < k; j++ {
if nums[j] == mx {
cnt++
}
}
if cnt < k {
break
}
ans += int64(n - j + 1)
if x == mx {
cnt--
}
}
return
}
3013. 将数组分成最小总代价的子数组 II ​
题目、示例与约束
给你一个下标从 0 开始长度为 n 的整数数组 nums 和两个 正 整数 k 和 dist 。
一个数组的 代价 是数组中的 第一个 元素。比方说,[1,2,3] 的代价为 1 ,[3,4,1] 的代价为 3 。
你需要将 nums 分割成 k 个 连续且互不相交 的子数组,满足 第二 个子数组与第 k 个子数组中第一个元素的下标距离 不超过 dist 。换句话说,如果你将 nums 分割成子数组 nums[0..(i1 - 1)], nums[i1..(i2 - 1)], ..., nums[ik-1..(n - 1)] ,那么它需要满足 ik-1 - i1 <= dist 。
请你返回这些子数组的 最小 总代价。
示例 1:
输入:nums = [1,3,2,6,4,2], k = 3, dist = 3 输出:5 解释:将数组分割成 3 个子数组的最优方案是:[1,3] ,[2,6,4] 和 [2] 。这是一个合法分割,因为 ik-1 - i1 等于 5 - 2 = 3 ,等于 dist 。总代价为 nums[0] + nums[2] + nums[5] ,也就是 1 + 2 + 2 = 5 。 5 是分割成 3 个子数组的最小总代价。
示例 2:
输入:nums = [10,1,2,2,2,1], k = 4, dist = 3 输出:15 解释:将数组分割成 4 个子数组的最优方案是:[10] ,[1] ,[2] 和 [2,2,1] 。这是一个合法分割,因为 ik-1 - i1 等于 3 - 1 = 2 ,小于 dist 。总代价为 nums[0] + nums[1] + nums[2] + nums[3] ,也就是 10 + 1 + 2 + 2 = 15 。 分割 [10] ,[1] ,[2,2,2] 和 [1] 不是一个合法分割,因为 ik-1 和 i1 的差为 5 - 1 = 4 ,大于 dist 。 15 是分割成 4 个子数组的最小总代价。
示例 3:
输入:nums = [10,8,18,9], k = 3, dist = 1 输出:36 解释:将数组分割成 3 个子数组的最优方案是:[10] ,[8] 和 [18,9] 。这是一个合法分割,因为 ik-1 - i1 等于 2 - 1 = 1 ,等于 dist 。总代价为 nums[0] + nums[1] + nums[2] ,也就是 10 + 8 + 18 = 36 。 分割 [10] ,[8,18] 和 [9] 不是一个合法分割,因为 ik-1 和 i1 的差为 3 - 1 = 2 ,大于 dist 。 36 是分割成 3 个子数组的最小总代价。
提示:
3 <= n <= 1051 <= nums[i] <= 1093 <= k <= nk - 2 <= dist <= n - 2
方法一:有序集合 · Go 实现
思考
与 I 不同, 与窗口跨度 可变且 。第一段仍固定带走 ,其余 个段首必须落在长度为 的窗口内。
每个窗口要求其中最小的 个数之和。朴素对每个窗口排序过慢。
用两个有序集合分别保存窗口内最小的 个数与其余数,并维护前者的和。窗口滑动时按大小决定插入左或右集合,再平衡两边的基数。
题目需要我们将数组 分割成 个连续且不相交的子数组,并且第二个子数组的第一个元素与第 个子数组的第一个元素的下标距离不超过 ,这等价于让我们从 下标为 的元素开始,找到一个窗口大小为 的子数组,求其中前 的最小元素之和。我们将 减 ,这样我们只需要求出 个最小元素之和,再加上 即可。
我们可以使用两个有序集合 和 分别维护大小为 的窗口元素,其中 维护最小的 个元素,而 维护窗口的剩余元素。我们维护一个变量 表示 与 中元素之和。初始时,我们将前 个元素之和累加到 中,并将下标为 的所有元素加入到 中。如果 的大小大于 ,我们循环将 中的最大元素移动到 中,直到 的大小等于 ,过程中更新 的值。
那么此时初始答案 。
接下来我们从 开始遍历 ,对于每一个元素 ,我们需要将 从 或 中移除,然后将 加入到 或 中。如果 小于 中的最大元素,我们将 加入到 中,否则加入到 中。如果此时 的大小小于 ,我们将 中的最小元素移动到 中,直到 的大小等于 。如果此时 的大小大于 ,我们将 中的最大元素移动到 中,直到 的大小等于 。过程中更新 的值,并更新 。
最终答案即为 。
时间复杂度 ,空间复杂度 。其中 为数组 的长度。
func minimumCost(nums []int, k int, dist int) int64 {
k--
l := redblacktree.New[int, int]()
r := redblacktree.New[int, int]()
merge := func(st *redblacktree.Tree[int, int], x, v int) {
c, _ := st.Get(x)
if c+v == 0 {
st.Remove(x)
} else {
st.Put(x, c+v)
}
}
s := nums[0]
for _, x := range nums[1 : dist+2] {
s += x
merge(l, x, 1)
}
size := dist + 1
l2r := func() {
x := l.Right().Key
merge(l, x, -1)
s -= x
size--
merge(r, x, 1)
}
r2l := func() {
x := r.Left().Key
merge(r, x, -1)
merge(l, x, 1)
s += x
size++
}
for size > k {
l2r()
}
ans := s
for i := dist + 2; i < len(nums); i++ {
x := nums[i-dist-1]
if _, ok := l.Get(x); ok {
merge(l, x, -1)
s -= x
size--
} else {
merge(r, x, -1)
}
y := nums[i]
if y < l.Right().Key {
merge(l, y, 1)
s += y
size++
} else {
merge(r, y, 1)
}
for size < k {
r2l()
}
for size > k {
l2r()
}
ans = min(ans, s)
}
return int64(ans)
}
3023. 在无限流中寻找模式 I 🔒 ​
题目、示例与约束
给定一个二进制数组 pattern 和一个类 InfiniteStream 的对象 stream 表示一个下标从 0 开始的二进制位无限流。
类 InfiniteStream 包含下列函数:
int next():从流中读取 一个 二进制位 (是0或1)并返回。
返回 第一个使得模式匹配流中读取的二进制位的 开始下标。例如,如果模式是 [1, 0],第一个匹配是流中的高亮部分 [0, 1, 0, 1, ...]。
示例 1:
输入: stream = [1,1,1,0,1,1,1,...], pattern = [0,1] 输出: 3 解释: 模式 [0,1] 的第一次出现在流中高亮 [1,1,1,0,1,...],从下标 3 开始。
示例 2:
输入: stream = [0,0,0,0,...], pattern = [0] 输出: 0 解释: 模式 [0] 的第一次出现在流中高亮 [0,...],从下标 0 开始。
示例 3:
输入: stream = [1,0,1,1,0,1,1,0,1,...], pattern = [1,1,0,1] 输出: 2 解释: 模式 [1,1,0,1] 的第一次出现在流中高亮 [1,0,1,1,0,1,...],从下标 2 开始。
提示:
1 <= pattern.length <= 100pattern只包含0或1。stream只包含0或1。- 生成的输入使模式的开始下标在流的前
105个二进制位中。
方法一:位运算 + 滑动窗口 · Go 实现
思考
模式长度不超过 ,流可能很长,不能先读完再匹配。朴素对每个起点比较最坏可达 次比较。
长度不超过 ,可把模式拆成两段,各压进一个 位整数。流上维护同样长度的滑动位窗。
每次读入一位,右半溢出的那一位送入左半。窗口长度够了之后比较两个整数是否同时相等。
我们注意到,数组 的长度不超过 ,因此,我们可以用两个 位的整数 和 来表示 左右两半的二进制数。
接下来,我们遍历数据流,同样维护两个 位的整数 和 表示当前 长度的窗口的二进制数。如果当前达到了窗口的长度,我们比较 和 是否相等,以及 和 是否相等,如果是,我们返回当前数据流的索引即可。
时间复杂度 ,其中 和 分别是数据流和 的元素个数。空间复杂度 。
/**
* Definition for an infinite stream.
* type InfiniteStream interface {
* Next() int
* }
*/
func findPattern(stream InfiniteStream, pattern []int) int {
a, b := 0, 0
m := len(pattern)
half := m >> 1
mask1 := (1 << half) - 1
mask2 := (1 << (m - half)) - 1
for i := 0; i < half; i++ {
a |= pattern[i] << (half - 1 - i)
}
for i := half; i < m; i++ {
b |= pattern[i] << (m - 1 - i)
}
x, y := 0, 0
for i := 1; ; i++ {
v := stream.Next()
y = y<<1 | v
v = (y >> (m - half)) & 1
y &= mask2
x = x<<1 | v
x &= mask1
if i >= m && a == x && b == y {
return i - m
}
}
}
3037. 在无限流中寻找模式 II 🔒 ​
题目、示例与约束
给定一个二进制数组 pattern 和一个类 InfiniteStream 的对象 stream 表示一个下标从 0 开始的二进制位无限流。
类 InfiniteStream 包含下列函数:
int next():从流中读取 一个 二进制位 (是0或1)并返回。
返回 第一个使得模式匹配流中读取的二进制位的 开始下标。例如,如果模式是 [1, 0],第一个匹配是流中的高亮部分 [0, 1, 0, 1, ...]。
示例 1:
输入: stream = [1,1,1,0,1,1,1,...], pattern = [0,1] 输出: 3 解释: 模式 [0,1] 的第一次出现在流中高亮 [1,1,1,0,1,...],从下标 3 开始。
示例 2:
输入: stream = [0,0,0,0,...], pattern = [0] 输出: 0 解释: 模式 [0] 的第一次出现在流中高亮 [0,...],从下标 0 开始。
示例 3:
输入: stream = [1,0,1,1,0,1,1,0,1,...], pattern = [1,1,0,1] 输出: 2 解释: 模式 [1,1,0,1] 的第一次出现在流中高亮 [1,0,1,1,0,1,...],从下标 2 开始。
提示:
1 <= pattern.length <= 104pattern只包含0或1。stream只包含0或1。- 生成的输入使模式的开始下标在流的前
105个二进制位中。
方法一:Go 解法(本站整理) · Go 实现
根据题意整理:源文只有 KMP 思考,没有非空实现。先计算模式的最长相等真前后缀长度 pi;顺序读流,失配时沿 pi 回退匹配长度而不回退流,首次匹配满 m 位便返回起始下标。m 为模式长度、s 为读入位数,时间 O(m+s),空间 O(m)。平台 InfiniteStream 提供 Next() int,入口 findPattern(stream *InfiniteStream, pattern []int) int;题目保证首次匹配存在且起点在前 10^5 位。测试 mock 在多读时直接报错。
出处:Doocs 原题与题解,固定提交 91ef8c241a5eaabf96f9ca8d45cfb1a338bcb011;题意、参考及本 Go 改写采用 CC BY-SA 4.0。
// 题意与参考:Doocs LeetCode,CC BY-SA 4.0。
// https://github.com/doocs/leetcode/blob/91ef8c241a5eaabf96f9ca8d45cfb1a338bcb011/solution/3000-3099/3037.Find%20Pattern%20in%20Infinite%20Stream%20II/README.md
// Go 实现与测试沿用 CC BY-SA 4.0;来源性质见配套说明。
func findPattern(stream *InfiniteStream, pattern []int) int {
m := len(pattern)
pi := make([]int, m)
for i, j := 1, 0; i < m; i++ {
for j > 0 && pattern[i] != pattern[j] {
j = pi[j-1]
}
if pattern[i] == pattern[j] {
j++
}
pi[i] = j
}
for i, j := 0, 0; ; i++ {
bit := stream.Next()
for j > 0 && bit != pattern[j] {
j = pi[j-1]
}
if bit == pattern[j] {
j++
}
if j == m {
return i - m + 1
}
}
}
3090. 每个字符最多出现两次的最长子字符串 ​
题目、示例与约束
给你一个字符串 s ,请找出满足每个字符最多出现两次的最长子字符串,并返回该子字符串的 最大 长度。
示例 1:
输入: s = "bcbbbcba"
输出: 4
解释:
以下子字符串长度为 4,并且每个字符最多出现两次:"bcbbbcba"。
示例 2:
输入: s = "aaaa"
输出: 2
解释:
以下子字符串长度为 2,并且每个字符最多出现两次:"aaaa"。
提示:
2 <= s.length <= 100s仅由小写英文字母组成。
方法一:双指针 · Go 实现
思考
子串中每个字符至多出现两次。 ,枚举子串可行,但约束是典型的滑动窗口。
右端纳入字符后,若某计数超过 ,左端必须右移直到该计数回到 。窗口合法时更新最大长度。
哈希计数配合双指针一遍扫描。
我们用两个指针 和 来维护一个滑动窗口,用一个数组 来记录窗口中每个字符的出现次数。
每一次,我们将指针 对应的字符 加入窗口,然后判断 是否大于 ,如果大于 ,则将指针 循环右移,直到 小于等于 。此时,我们更新答案 。
最终,我们返回答案 。
时间复杂度 ,其中 为字符串 的长度。空间复杂度 ,其中 为字符集,本题中 。
func maximumLengthSubstring(s string) (ans int) {
l := 0
cnt := [26]int{}
for r, c := range s {
idx := int(c - 'a')
cnt[idx]++
for cnt[idx] > 2 {
cnt[s[l]-'a']--
l++
}
ans = max(ans, r-l+1)
}
return
}
3095. 或值至少为 K 的最短子数组 I ​
题目、示例与约束
给你一个 非负 整数数组 nums 和一个整数 k 。
如果一个数组中所有元素的按位或运算 OR 的值 至少 为 k ,那么我们称这个数组是 特别的 。
请你返回 nums 中 最短特别非空 子数组的长度,如果特别子数组不存在,那么返回 -1 。
示例 1:
输入:nums = [1,2,3], k = 2
输出:1
解释:
子数组 [3] 的按位 OR 值为 3 ,所以我们返回 1 。
注意,[2] 也是一个特别子数组。
示例 2:
输入:nums = [2,1,8], k = 10
输出:3
解释:
子数组 [2,1,8] 的按位 OR 值为 11 ,所以我们返回 3 。
示例 3:
输入:nums = [1,2], k = 0
输出:1
解释:
子数组 [1] 的按位 OR 值为 1 ,所以我们返回 1 。
提示:
1 <= nums.length <= 500 <= nums[i] <= 500 <= k < 64
方法一:双指针 + 计数 · Go 实现
思考
最短子数组的按位或 。 ,三重循环可过,但或运算对右端单调不减,适合双指针。
右端纳入只会把某些位变成 ;左端移出时,只有该位计数降到 才能从或值中去掉。
维护 位计数与当前或值 ,右扩左缩,记录满足 的最短长度。
我们可以发现,如果我们固定子数组的左端点,随着右端点向右移动,子数组的按位或值只会增大,不会减小。因此我们可以使用双指针的方法,维护一个满足条件的子数组。
具体地,我们使用两个指针 和 分别表示子数组的左右端点,初始时两个指针都位于数组的第一个元素。用一个变量 表示子数组的按位或值,初始时 的值为 。我们还需要维护一个长度为 的数组 ,表示子数组中每个元素的二进制表示中每一位的出现次数。
在每一步操作中,我们将 向右移动一位,更新 和 。如果 的值大于等于 ,我们不断更新子数组的最小长度,并将 向右移动一位,直到 的值小于 。在这个过程中,我们也需要更新 和 。
最后,我们返回最小长度,如果不存在满足条件的子数组,则返回 。
时间复杂度 ,空间复杂度 ,其中 和 分别是数组的长度和数组中元素的最大值。
func minimumSubarrayLength(nums []int, k int) int {
n := len(nums)
cnt := [32]int{}
ans := n + 1
s, i := 0, 0
for j, x := range nums {
s |= x
for h := 0; h < 32; h++ {
if x>>h&1 == 1 {
cnt[h]++
}
}
for ; s >= k && i <= j; i++ {
ans = min(ans, j-i+1)
for h := 0; h < 32; h++ {
if nums[i]>>h&1 == 1 {
cnt[h]--
if cnt[h] == 0 {
s ^= 1 << h
}
}
}
}
}
if ans == n+1 {
return -1
}
return ans
}
3097. 或值至少为 K 的最短子数组 II ​
题目、示例与约束
给你一个 非负 整数数组 nums 和一个整数 k 。
如果一个数组中所有元素的按位或运算 OR 的值 至少 为 k ,那么我们称这个数组是 特别的 。
请你返回 nums 中 最短特别非空 子数组的长度,如果特别子数组不存在,那么返回 -1 。
示例 1:
输入:nums = [1,2,3], k = 2
输出:1
解释:
子数组 [3] 的按位 OR 值为 3 ,所以我们返回 1 。
示例 2:
输入:nums = [2,1,8], k = 10
输出:3
解释:
子数组 [2,1,8] 的按位 OR 值为 11 ,所以我们返回 3 。
示例 3:
输入:nums = [1,2], k = 0
输出:1
解释:
子数组 [1] 的按位 OR 值为 1 ,所以我们返回 1 。
提示:
1 <= nums.length <= 2 * 1050 <= nums[i] <= 1090 <= k <= 109
方法一:双指针 + 计数 · Go 实现
思考
题意与 I 相同,但 ,枚举子数组不再可行。
I 中「或值随右端单调、用位计数支持左端删除」的做法已是 ,直接套用即可。
同一套双指针加 位计数维护当前或值。
我们可以发现,如果我们固定子数组的左端点,随着右端点向右移动,子数组的按位或值只会增大,不会减小。因此我们可以使用双指针的方法,维护一个满足条件的子数组。
具体地,我们使用两个指针 和 分别表示子数组的左右端点,初始时两个指针都位于数组的第一个元素。用一个变量 表示子数组的按位或值,初始时 的值为 。我们还需要维护一个长度为 的数组 ,表示子数组中每个元素的二进制表示中每一位的出现次数。
在每一步操作中,我们将 向右移动一位,更新 和 。如果 的值大于等于 ,我们不断更新子数组的最小长度,并将 向右移动一位,直到 的值小于 。在这个过程中,我们也需要更新 和 。
最后,我们返回最小长度,如果不存在满足条件的子数组,则返回 。
时间复杂度 ,空间复杂度 ,其中 和 分别是数组的长度和数组中元素的最大值。
相似题目:
func minimumSubarrayLength(nums []int, k int) int {
n := len(nums)
cnt := [32]int{}
ans := n + 1
s, i := 0, 0
for j, x := range nums {
s |= x
for h := 0; h < 32; h++ {
if x>>h&1 == 1 {
cnt[h]++
}
}
for ; s >= k && i <= j; i++ {
ans = min(ans, j-i+1)
for h := 0; h < 32; h++ {
if nums[i]>>h&1 == 1 {
cnt[h]--
if cnt[h] == 0 {
s ^= 1 << h
}
}
}
}
}
if ans == n+1 {
return -1
}
return ans
}
3191. 使二进制数组全部等于 1 的最少操作次数 I ​
题目、示例与约束
给你一个二进制数组 nums 。
你可以对数组执行以下操作 任意 次(也可以 0 次):
- 选择数组中 任意连续 3 个元素,并将它们 全部反转 。
反转 一个元素指的是将它的值从 0 变 1 ,或者从 1 变 0 。
请你返回将 nums 中所有元素变为 1 的 最少 操作次数。如果无法全部变成 1 ,返回 -1 。
示例 1:
输入:nums = [0,1,1,1,0,0]
输出:3
解释:
我们可以执行以下操作:
- 选择下标为 0 ,1 和 2 的元素并反转,得到
nums = [1,0,0,1,0,0]。 - 选择下标为 1 ,2 和 3 的元素并反转,得到
nums = [1,1,1,0,0,0]。 - 选择下标为 3 ,4 和 5 的元素并反转,得到
nums = [1,1,1,1,1,1]。
示例 2:
输入:nums = [0,1,1,1]
输出:-1
解释:
无法将所有元素都变为 1 。
提示:
3 <= nums.length <= 1050 <= nums[i] <= 1
方法一:顺序遍历 + 模拟 · Go 实现
思考
每次翻转长度为 的窗口。从左遇到 若不立刻翻,左侧这个 再无机会被更靠右的窗口覆盖。
贪心在每个 处翻转 ;若 越界则不可能。
顺序扫描,遇 则异或后两位并计数。扫完若曾越界返回 ,否则返回次数。
我们注意到,数组中的第一个为 的位置,一定需要进行一次反转操作,否则无法将其变为 。因此,我们可以顺序遍历数组,每次遇到 ,就将其后两个元素进行反转操作,累计一次操作次数。
遍历结束后,返回答案即可。
时间复杂度 ,其中 为数组 的长度。空间复杂度 。
func minOperations(nums []int) (ans int) {
for i, x := range nums {
if x == 0 {
if i+2 >= len(nums) {
return -1
}
nums[i+1] ^= 1
nums[i+2] ^= 1
ans++
}
}
return
}
3206. 交替组 I ​
题目、示例与约束
给你一个整数数组 colors ,它表示一个由红色和蓝色瓷砖组成的环,第 i 块瓷砖的颜色为 colors[i] :
colors[i] == 0表示第i块瓷砖的颜色是 红色 。colors[i] == 1表示第i块瓷砖的颜色是 蓝色 。
环中连续 3 块瓷砖的颜色如果是 交替 颜色(也就是说中间瓷砖的颜色与它 左边 和 右边 的颜色都不同),那么它被称为一个 交替 组。
请你返回 交替 组的数目。
注意 ,由于 colors 表示一个 环 ,第一块 瓷砖和 最后一块 瓷砖是相邻的。
示例 1:
输入:colors = [1,1,1]
输出:0
解释:

示例 2:
输入:colors = [0,1,0,0,1]
输出:3
解释:

交替组包括:



提示:
3 <= colors.length <= 1000 <= colors[i] <= 1
方法一:一次遍历 · Go 实现
思考
且组长度固定为 ,枚举每个起点再检查三格是否交替即可。环上越界需要取模,写法略繁。
将环展开为长度 的线性扫描后,用 记录当前交替段长度,相邻同色则重置。仅当 且 时计入,从而每个原位置作为组的右端点恰好统计一次,额外空间 。
我们令 ,表示交替组的长度为 。
为了方便处理,我们可以将环展开成一个长度为 的数组,然后从左到右遍历这个数组,用一个变量 记录当前交替组的长度,如果遇到了相同的颜色,就将 重置为 ,否则将 加一。如果 ,并且当前位置 大于等于 ,那么就找到了一个交替组,答案加一。
遍历结束后,返回答案即可。
时间复杂度 ,其中 为数组 的长度。空间复杂度 。
func numberOfAlternatingGroups(colors []int) (ans int) {
k := 3
n := len(colors)
cnt := 0
for i := 0; i < n<<1; i++ {
if i > 0 && colors[i%n] == colors[(i-1)%n] {
cnt = 1
} else {
cnt++
}
if i >= n && cnt >= k {
ans++
}
}
return
}
3208. 交替组 II ​
题目、示例与约束
给你一个整数数组 colors 和一个整数 k ,colors表示一个由红色和蓝色瓷砖组成的环,第 i 块瓷砖的颜色为 colors[i] :
colors[i] == 0表示第i块瓷砖的颜色是 红色 。colors[i] == 1表示第i块瓷砖的颜色是 蓝色 。
环中连续 k 块瓷砖的颜色如果是 交替 颜色(也就是说除了第一块和最后一块瓷砖以外,中间瓷砖的颜色与它 左边 和 右边 的颜色都不同),那么它被称为一个 交替 组。
请你返回 交替 组的数目。
注意 ,由于 colors 表示一个 环 ,第一块 瓷砖和 最后一块 瓷砖是相邻的。
示例 1:
输入:colors = [0,1,0,1,0], k = 3
输出:3
解释:

交替组包括:



示例 2:
输入:colors = [0,1,0,0,1,0,1], k = 6
输出:2
解释:

交替组包括:


示例 3:
输入:colors = [1,1,0,1], k = 4
输出:0
解释:

提示:
3 <= colors.length <= 1050 <= colors[i] <= 13 <= k <= colors.length
方法一:一次遍历 · Go 实现
思考
组长度变为给定的 ,且 。若对每个起点再扫 格,则时间达 ,无法接受。
与长度为 时相同,交替性是一段连续性质:把环展开为 ,维护当前交替段长度 ,在 且 时计数。每个右端点 判定,总时间线性。
我们可以将环展开成一个长度为 的数组,然后从左到右遍历这个数组,用一个变量 记录当前交替组的长度,如果遇到了相同的颜色,就将 重置为 ,否则将 加一。如果 ,并且当前位置 大于等于 ,那么就找到了一个交替组,答案加一。
遍历结束后,返回答案即可。
时间复杂度 ,其中 为数组 的长度。空间复杂度 。
func numberOfAlternatingGroups(colors []int, k int) (ans int) {
n := len(colors)
cnt := 0
for i := 0; i < n<<1; i++ {
if i > 0 && colors[i%n] == colors[(i-1)%n] {
cnt = 1
} else {
cnt++
}
if i >= n && cnt >= k {
ans++
}
}
return
}
3254. 长度为 K 的子数组的能量值 I ​
题目、示例与约束
给你一个长度为 n 的整数数组 nums 和一个正整数 k 。
一个数组的 能量值 定义为:
- 如果 所有 元素都是依次 连续 且 上升 的,那么能量值为 最大 的元素。
- 否则为 -1 。
你需要求出 nums 中所有长度为 k 的 子数组 的能量值。
请你返回一个长度为 n - k + 1 的整数数组 results ,其中 results[i] 是子数组 nums[i..(i + k - 1)] 的能量值。
示例 1:
输入:nums = [1,2,3,4,3,2,5], k = 3
输出:[3,4,-1,-1,-1]
解释:
nums 中总共有 5 个长度为 3 的子数组:
[1, 2, 3]中最大元素为 3 。[2, 3, 4]中最大元素为 4 。[3, 4, 3]中元素 不是 连续的。[4, 3, 2]中元素 不是 上升的。[3, 2, 5]中元素 不是 连续的。
示例 2:
输入:nums = [2,2,2,2,2], k = 4
输出:[-1,-1]
示例 3:
输入:nums = [3,2,3,2,3,2], k = 2
输出:[-1,3,-1,3,-1]
提示:
1 <= n == nums.length <= 5001 <= nums[i] <= 1051 <= k <= n
方法一:递推 · Go 实现
思考
窗口能量在「元素恰为连续递增」时等于窗口最大,否则为 。 ,每个窗口重扫 格可以过,但相邻窗口的连续性高度重叠。
令 为以 结尾的连续递增长度,则 当且仅当 合法,能量为 。一遍递推后按右端点输出。
我们定义一个数组 ,其中 表示以第 个元素结尾的连续上升子序列的长度。初始时 。
接下来,我们遍历数组 ,计算数组 的值。如果 ,则 ;否则 。
然后,我们在 的范围内遍历数组 ,如果 ,那么答案数组添加 ,否则添加 。
遍历结束后,返回答案数组。
时间复杂度 ,空间复杂度 。其中 表示数组 的长度。
func resultsArray(nums []int, k int) (ans []int) {
n := len(nums)
f := make([]int, n)
f[0] = 1
for i := 1; i < n; i++ {
if nums[i] == nums[i-1]+1 {
f[i] = f[i-1] + 1
} else {
f[i] = 1
}
}
for i := k - 1; i < n; i++ {
if f[i] >= k {
ans = append(ans, nums[i])
} else {
ans = append(ans, -1)
}
}
return
}
3255. 长度为 K 的子数组的能量值 II ​
题目、示例与约束
给你一个长度为 n 的整数数组 nums 和一个正整数 k 。
一个数组的 能量值 定义为:
- 如果 所有 元素都是依次 连续(即
nums[i] + 1 = nums[i + 1],i < n)且 上升 的,那么能量值为 最大 的元素。 - 否则为 -1 。
你需要求出 nums 中所有长度为 k 的 子数组 的能量值。
请你返回一个长度为 n - k + 1 的整数数组 results ,其中 results[i] 是子数组 nums[i..(i + k - 1)] 的能量值。
示例 1:
输入:nums = [1,2,3,4,3,2,5], k = 3
输出:[3,4,-1,-1,-1]
解释:
nums 中总共有 5 个长度为 3 的子数组:
[1, 2, 3]中最大元素为 3 。[2, 3, 4]中最大元素为 4 。[3, 4, 3]中元素 不是 连续的。[4, 3, 2]中元素 不是 上升的。[3, 2, 5]中元素 不是 连续的。
示例 2:
输入:nums = [2,2,2,2,2], k = 4
输出:[-1,-1]
示例 3:
输入:nums = [3,2,3,2,3,2], k = 2
输出:[-1,3,-1,3,-1]
提示:
1 <= n == nums.length <= 1051 <= nums[i] <= 1061 <= k <= n
方法一:递推 · Go 实现
思考
题意与 I 相同,但 ,每个窗口扫描 格不可行。仍用「以 结尾的连续递增长度」判定窗口 。
递推与 I 一致: 则加一,否则置 。右端点处 则输出 。线性时间匹配本题规模。
我们定义一个数组 ,其中 表示以第 个元素结尾的连续上升子序列的长度。初始时 。
接下来,我们遍历数组 ,计算数组 的值。如果 ,则 ;否则 。
然后,我们在 的范围内遍历数组 ,如果 ,那么答案数组添加 ,否则添加 。
遍历结束后,返回答案数组。
时间复杂度 ,空间复杂度 。其中 表示数组 的长度。
func resultsArray(nums []int, k int) (ans []int) {
n := len(nums)
f := make([]int, n)
f[0] = 1
for i := 1; i < n; i++ {
if nums[i] == nums[i-1]+1 {
f[i] = f[i-1] + 1
} else {
f[i] = 1
}
}
for i := k - 1; i < n; i++ {
if f[i] >= k {
ans = append(ans, nums[i])
} else {
ans = append(ans, -1)
}
}
return
}
3258. 统计满足 K 约束的子字符串数量 I ​
题目、示例与约束
给你一个 二进制 字符串 s 和一个整数 k。
如果一个 二进制字符串 满足以下任一条件,则认为该字符串满足 k 约束:
- 字符串中
0的数量最多为k。 - 字符串中
1的数量最多为k。
返回一个整数,表示 s 的所有满足 k 约束 的子字符串的数量。
示例 1:
输入:s = "10101", k = 1
输出:12
解释:
s 的所有子字符串中,除了 "1010"、"10101" 和 "0101" 外,其余子字符串都满足 k 约束。
示例 2:
输入:s = "1010101", k = 2
输出:25
解释:
s 的所有子字符串中,除了长度大于 5 的子字符串外,其余子字符串都满足 k 约束。
示例 3:
输入:s = "11111", k = 1
输出:15
解释:
s 的所有子字符串都满足 k 约束。
提示:
1 <= s.length <= 501 <= k <= s.lengths[i]是'0'或'1'。
方法一:滑动窗口 · Go 实现
思考
-约束要求子串中 的个数与 的个数至少有一个不超过 。 ,枚举子串可以过,但约束对右端点单调:窗口一旦两边都超过 ,左端必须右移。
滑动窗口维护 ,超标则缩左。此时所有以当前右端为右端、左端在 的子串都合法,个数为 。一遍线性。
我们用两个变量 和 分别记录当前窗口内的 和 的个数,用 记录满足 约束的子字符串的个数,用 记录窗口的左边界。
当我们右移窗口时,如果窗口内的 和 的个数都大于 ,我们就需要左移窗口,直到窗口内的 和 的个数都不大于 。此时,窗口内所有以 作为右端点的子字符串都满足 约束,个数为 ,其中 是窗口的右边界。我们将这个个数累加到 中。
最后,我们返回 即可。
时间复杂度 ,其中 是字符串 的长度。空间复杂度 。
func countKConstraintSubstrings(s string, k int) (ans int) {
cnt := [2]int{}
l := 0
for r, c := range s {
cnt[c-'0']++
for ; cnt[0] > k && cnt[1] > k; l++ {
cnt[s[l]-'0']--
}
ans += r - l + 1
}
return
}
3297. 统计重新排列后包含另一个字符串的子字符串数目 I ​
题目、示例与约束
给你两个字符串 word1 和 word2 。
如果一个字符串 x 重新排列后,word2 是重排字符串的 前缀 ,那么我们称字符串 x 是 合法的 。
请你返回 word1 中 合法 子字符串 的数目。
示例 1:
输入:word1 = "bcca", word2 = "abc"
输出:1
解释:
唯一合法的子字符串是 "bcca" ,可以重新排列得到 "abcc" ,"abc" 是它的前缀。
示例 2:
输入:word1 = "abcabc", word2 = "abc"
输出:10
解释:
除了长度为 1 和 2 的所有子字符串都是合法的。
示例 3:
输入:word1 = "abcabc", word2 = "aaabc"
输出:0
解释:
1 <= word1.length <= 1051 <= word2.length <= 104word1和word2都只包含小写英文字母。
方法一:滑动窗口 · Go 实现
思考
子串重排后能覆盖 的全部字符,即子串是 的超多重集合。枚举子串再比较计数在长串上过慢。覆盖关系对窗口单调:一旦满足,再加右端仍满足,缩左端会找到最短覆盖。
维护 种尚未满足的字符。右端纳入使某字符刚好达标则 减一; 时尽量右移左端。此时左端左侧每一个起点与当前右端都能覆盖,答案加上左端下标。一次滑窗。
题目实际上是求在 中,有多少个子串包含了 中的所有字符。我们可以使用滑动窗口来处理。
首先,如果 的长度小于 的长度,那么 中不可能包含 的所有字符,直接返回 。
接下来,我们用一个哈希表或长度为 的数组 来统计 中的字符出现的次数。然后,我们用 来记录还需要多少个字符才能满足条件,初始化为 的长度。
接着,我们用一个滑动窗口 来记录当前窗口中的字符出现的次数。我们用 来记录满足条件的子串的个数,用 来记录窗口的左边界。
遍历 中的每个字符,对于当前字符 ,我们将其加入到 中,如果 的值等于 ,那么说明当前窗口中已经包含了 中的所有字符之一,那么 减一。如果 等于 ,说明当前窗口中包含了 中的所有字符,我们需要缩小窗口的左边界,直到 大于 。具体地,如果 等于 ,那么说明当前窗口中包含了 中的所有字符之一,那么缩小窗口的左边界之后,就不满足条件了,所以 加一,同时 减一。然后,我们将 加一。此时窗口为 ,那么对于任意 , 都是满足条件的子串,一共有 个,我们累加到答案中。
遍历完 中的所有字符之后,我们就得到了答案。
时间复杂度 ,其中 和 分别是 和 的长度。空间复杂度 ,其中 是字符集,这里是小写字母集合,所以空间复杂度是常数级别的。
func validSubstringCount(word1 string, word2 string) (ans int64) {
if len(word1) < len(word2) {
return 0
}
cnt := [26]int{}
need := 0
for _, c := range word2 {
cnt[c-'a']++
if cnt[c-'a'] == 1 {
need++
}
}
win := [26]int{}
l := 0
for _, c := range word1 {
i := int(c - 'a')
win[i]++
if win[i] == cnt[i] {
need--
}
for need == 0 {
i = int(word1[l] - 'a')
if win[i] == cnt[i] {
need++
}
win[i]--
l++
}
ans += int64(l)
}
return
}
3298. 统计重新排列后包含另一个字符串的子字符串数目 II ​
题目、示例与约束
给你两个字符串 word1 和 word2 。
如果一个字符串 x 重新排列后,word2 是重排字符串的 前缀 ,那么我们称字符串 x 是 合法的 。
请你返回 word1 中 合法 子字符串 的数目。
注意 ,这个问题中的内存限制比其他题目要 小 ,所以你 必须 实现一个线性复杂度的解法。
示例 1:
输入:word1 = "bcca", word2 = "abc"
输出:1
解释:
唯一合法的子字符串是 "bcca" ,可以重新排列得到 "abcc" ,"abc" 是它的前缀。
示例 2:
输入:word1 = "abcabc", word2 = "abc"
输出:10
解释:
除了长度为 1 和 2 的所有子字符串都是合法的。
示例 3:
输入:word1 = "abcabc", word2 = "aaabc"
输出:0
解释:
1 <= word1.length <= 1061 <= word2.length <= 104word1和word2都只包含小写英文字母。
方法一:滑动窗口 · Go 实现
思考
题意与 I 相同,仅数据范围更大,必须沿用线性滑窗而不能枚举子串。覆盖仍由「每种字符数量不少于 」刻画,单调性不变。
同一套 与窗口计数:右端纳入、左端在仍覆盖时右移,答案累加当前左端。长度不足时直接 。总时间线性。
题目实际上是求在 中,有多少个子串包含了 中的所有字符。我们可以使用滑动窗口来处理。
首先,如果 的长度小于 的长度,那么 中不可能包含 的所有字符,直接返回 。
接下来,我们用一个哈希表或长度为 的数组 来统计 中的字符出现的次数。然后,我们用 来记录还需要多少个字符才能满足条件,初始化为 的长度。
接着,我们用一个滑动窗口 来记录当前窗口中的字符出现的次数。我们用 来记录满足条件的子串的个数,用 来记录窗口的左边界。
遍历 中的每个字符,对于当前字符 ,我们将其加入到 中,如果 的值等于 ,那么说明当前窗口中已经包含了 中的所有字符之一,那么 减一。如果 等于 ,说明当前窗口中包含了 中的所有字符,我们需要缩小窗口的左边界,直到 大于 。具体地,如果 等于 ,那么说明当前窗口中包含了 中的所有字符之一,那么缩小窗口的左边界之后,就不满足条件了,所以 加一,同时 减一。然后,我们将 加一。此时窗口为 ,那么对于任意 , 都是满足条件的子串,一共有 个,我们累加到答案中。
遍历完 中的所有字符之后,我们就得到了答案。
时间复杂度 ,其中 和 分别是 和 的长度。空间复杂度 ,其中 是字符集,这里是小写字母集合,所以空间复杂度是常数级别的。
func validSubstringCount(word1 string, word2 string) (ans int64) {
if len(word1) < len(word2) {
return 0
}
cnt := [26]int{}
need := 0
for _, c := range word2 {
cnt[c-'a']++
if cnt[c-'a'] == 1 {
need++
}
}
win := [26]int{}
l := 0
for _, c := range word1 {
i := int(c - 'a')
win[i]++
if win[i] == cnt[i] {
need--
}
for need == 0 {
i = int(word1[l] - 'a')
if win[i] == cnt[i] {
need++
}
win[i]--
l++
}
ans += int64(l)
}
return
}
3305. 元音辅音字符串计数 I ​
题目、示例与约束
给你一个字符串 word 和一个 非负 整数 k。
返回 word 的 子字符串 中,每个元音字母('a'、'e'、'i'、'o'、'u')至少 出现一次,并且 恰好 包含 k 个辅音字母的子字符串的总数。
示例 1:
输入:word = "aeioqq", k = 1
输出:0
解释:
不存在包含所有元音字母的子字符串。
示例 2:
输入:word = "aeiou", k = 0
输出:1
解释:
唯一一个包含所有元音字母且不含辅音字母的子字符串是 word[0..4],即 "aeiou"。
示例 3:
输入:word = "ieaouqqieaouqq", k = 1
输出:3
解释:
包含所有元音字母并且恰好含有一个辅音字母的子字符串有:
word[0..5],即"ieaouq"。word[6..11],即"qieaou"。word[7..12],即"ieaouq"。
提示:
5 <= word.length <= 250word仅由小写英文字母组成。0 <= k <= word.length - 5
方法一:问题转换 + 滑动窗口 · Go 实现
思考
,枚举子串再统计可行;若直接维护「恰含 个辅音且五元音齐全」,左右端点会同时受两种约束,窗口单调性不完整。
将「恰好 个辅音」写成「至少 个」减去「至少 个」。后者只含「 」型条件,可用滑动窗口:右端推进后,在五元音齐备且辅音不少于阈值时尽量右移左端。
此时以当前右端为结尾、左端落在 的子串均合法,答案累加 。哈希表只记录五种元音,辅音用计数器 。
我们可以转换为求以下两个问题:
- 求每个元音字母至少出现一次,且至少包含 个辅音字母的子字符串的总数 ;
- 求每个元音字母至少出现一次,且至少包含 个辅音字母的子字符串的总数 。
那么答案就是 。
因此,我们设计一个函数 ,用于统计每个元音字母至少出现一次,且至少包含 个辅音字母的子字符串的总数。
我们可以用一个哈希表 统计每个元音字母的出现次数,用一个变量 统计答案,用一个变量 记录滑动窗口的左边界,用一个变量 记录当前窗口中辅音字母的个数。
遍历字符串,如果当前字符是元音字母,则将其加入哈希表 中,否则将 加一。如果此时 且哈希表 的大小为 ,说明当前窗口满足条件,我们循环移动左边界,直到窗口不满足条件。此时,以右边界 为结尾、且左边界在 范围内的子字符串都满足条件,一共有 个。我们将 加到答案中。继续遍历字符串,直到遍历结束,我们就得到了 。
最后,我们返回 。
时间复杂度 ,其中 是字符串 的长度。空间复杂度 。
func countOfSubstrings(word string, k int) int {
f := func(k int) int {
var ans int = 0
l, x := 0, 0
cnt := make(map[rune]int)
vowel := func(c rune) bool {
return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u'
}
for _, c := range word {
if vowel(c) {
cnt[c]++
} else {
x++
}
for x >= k && len(cnt) == 5 {
d := rune(word[l])
l++
if vowel(d) {
cnt[d]--
if cnt[d] == 0 {
delete(cnt, d)
}
} else {
x--
}
}
ans += l
}
return ans
}
return f(k) - f(k+1)
}
3306. 元音辅音字符串计数 II ​
题目、示例与约束
给你一个字符串 word 和一个 非负 整数 k。
返回 word 的 子字符串 中,每个元音字母('a'、'e'、'i'、'o'、'u')至少 出现一次,并且 恰好 包含 k 个辅音字母的子字符串的总数。
示例 1:
输入:word = "aeioqq", k = 1
输出:0
解释:
不存在包含所有元音字母的子字符串。
示例 2:
输入:word = "aeiou", k = 0
输出:1
解释:
唯一一个包含所有元音字母且不含辅音字母的子字符串是 word[0..4],即 "aeiou"。
示例 3:
输入:word = "ieaouqqieaouqq", k = 1
输出:3
解释:
包含所有元音字母并且恰好含有一个辅音字母的子字符串有:
word[0..5],即"ieaouq"。word[6..11],即"qieaou"。word[7..12],即"ieaouq"。
提示:
5 <= word.length <= 2 * 105word仅由小写英文字母组成。0 <= k <= word.length - 5
方法一:问题转换 + 滑动窗口 · Go 实现
思考
题意与上一题相同,但 ,枚举子串不再可行,必须保持线性扫描。
仍将恰好 个辅音拆成 。两种阈值都是单调条件,一次滑动窗口即可。
窗口内用哈希表维护五种元音、用 维护辅音个数,与 I 的实现一致,复杂度降为 。
我们可以转换为求以下两个问题:
- 求每个元音字母至少出现一次,且至少包含 个辅音字母的子字符串的总数 ;
- 求每个元音字母至少出现一次,且至少包含 个辅音字母的子字符串的总数 。
那么答案就是 。
因此,我们设计一个函数 ,用于统计每个元音字母至少出现一次,且至少包含 个辅音字母的子字符串的总数。
我们可以用一个哈希表 统计每个元音字母的出现次数,用一个变量 统计答案,用一个变量 记录滑动窗口的左边界,用一个变量 记录当前窗口中辅音字母的个数。
遍历字符串,如果当前字符是元音字母,则将其加入哈希表 中,否则将 加一。如果此时 且哈希表 的大小为 ,说明当前窗口满足条件,我们循环移动左边界,直到窗口不满足条件。此时,以右边界 为结尾、且左边界在 范围内的子字符串都满足条件,一共有 个。我们将 加到答案中。继续遍历字符串,直到遍历结束,我们就得到了 。
最后,我们返回 。
时间复杂度 ,其中 是字符串 的长度。空间复杂度 。
func countOfSubstrings(word string, k int) int64 {
f := func(k int) int64 {
var ans int64 = 0
l, x := 0, 0
cnt := make(map[rune]int)
vowel := func(c rune) bool {
return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u'
}
for _, c := range word {
if vowel(c) {
cnt[c]++
} else {
x++
}
for x >= k && len(cnt) == 5 {
d := rune(word[l])
l++
if vowel(d) {
cnt[d]--
if cnt[d] == 0 {
delete(cnt, d)
}
} else {
x--
}
}
ans += int64(l)
}
return ans
}
return f(k) - f(k+1)
}
3318. 计算子数组的 x-sum I ​
题目、示例与约束
给你一个由 n 个整数组成的数组 nums,以及两个整数 k 和 x。
数组的 x-sum 计算按照以下步骤进行:
- 统计数组中所有元素的出现次数。
- 仅保留出现频率最高的前
x种元素。如果两种元素的出现次数相同,则数值 较大 的元素被认为出现次数更多。 - 计算结果数组的和。
注意,如果数组中的不同元素少于 x 个,则其 x-sum 是数组的元素总和。
返回一个长度为 n - k + 1 的整数数组 answer,其中 answer[i] 是 子数组 nums[i..i + k - 1] 的 x-sum。
子数组 是数组内的一个连续 非空 的元素序列。
示例 1:
输入:nums = [1,1,2,2,3,4,2,3], k = 6, x = 2
输出:[6,10,12]
解释:
- 对于子数组
[1, 1, 2, 2, 3, 4],只保留元素 1 和 2。因此,answer[0] = 1 + 1 + 2 + 2。 - 对于子数组
[1, 2, 2, 3, 4, 2],只保留元素 2 和 4。因此,answer[1] = 2 + 2 + 2 + 4。注意 4 被保留是因为其数值大于出现其他出现次数相同的元素(3 和 1)。 - 对于子数组
[2, 2, 3, 4, 2, 3],只保留元素 2 和 3。因此,answer[2] = 2 + 2 + 2 + 3 + 3。
示例 2:
输入:nums = [3,8,7,8,7,5], k = 2, x = 2
输出:[11,15,15,15,12]
解释:
由于 k == x,answer[i] 等于子数组 nums[i..i + k - 1] 的总和。
提示:
1 <= n == nums.length <= 501 <= nums[i] <= 501 <= x <= k <= nums.length
方法一:Go 解法(本站整理) · Go 实现
基于 Doocs 频次排序规则改写。维护滑动窗口频次,按“次数降序、值降序”排序非零项,取前 x 项的 次数×数值 之和。I 的 n≤50,无需参考实现依赖的外部有序集合,用标准库排序即可满足约束。n 为数组长度、D≤50 为值域大小,时间 O(n D log D),空间 O(D),不计输出。入口 findXSum,x 大于窗口不同值数时取全部。
出处:Doocs 原题与题解,固定提交 91ef8c241a5eaabf96f9ca8d45cfb1a338bcb011;题意、参考及本 Go 改写采用 CC BY-SA 4.0。
// 题意与参考:Doocs LeetCode,CC BY-SA 4.0。
// https://github.com/doocs/leetcode/blob/91ef8c241a5eaabf96f9ca8d45cfb1a338bcb011/solution/3300-3399/3318.Find%20X-Sum%20of%20All%20K-Long%20Subarrays%20I/README.md
// Go 实现与测试沿用 CC BY-SA 4.0;来源性质见配套说明。
import "sort"
func findXSum(nums []int, k, x int) []int {
var count [51]int
out := make([]int, 0, len(nums)-k+1)
for i, v := range nums {
count[v]++
if i >= k {
count[nums[i-k]]--
}
if i+1 < k {
continue
}
values := []int{}
for v := 1; v <= 50; v++ {
if count[v] > 0 {
values = append(values, v)
}
}
sort.Slice(values, func(i, j int) bool {
a, b := values[i], values[j]
if count[a] != count[b] {
return count[a] > count[b]
}
return a > b
})
sum := 0
for j := 0; j < x && j < len(values); j++ {
v := values[j]
sum += count[v] * v
}
out = append(out, sum)
}
return out
}
3321. 计算子数组的 x-sum II ​
题目、示例与约束
给你一个由 n 个整数组成的数组 nums,以及两个整数 k 和 x。
数组的 x-sum 计算按照以下步骤进行:
- 统计数组中所有元素的出现次数。
- 仅保留出现频率最高的前
x种元素。如果两种元素的出现次数相同,则数值 较大 的元素被认为出现次数更多。 - 计算结果数组的和。
注意,如果数组中的不同元素少于 x 个,则其 x-sum 是数组的元素总和。
返回一个长度为 n - k + 1 的整数数组 answer,其中 answer[i] 是 子数组 nums[i..i + k - 1] 的 x-sum。
子数组 是数组内的一个连续 非空 的元素序列。
示例 1:
输入:nums = [1,1,2,2,3,4,2,3], k = 6, x = 2
输出:[6,10,12]
解释:
- 对于子数组
[1, 1, 2, 2, 3, 4],只保留元素 1 和 2。因此,answer[0] = 1 + 1 + 2 + 2。 - 对于子数组
[1, 2, 2, 3, 4, 2],只保留元素 2 和 4。因此,answer[1] = 2 + 2 + 2 + 4。注意 4 被保留是因为其数值大于出现其他出现次数相同的元素(3 和 1)。 - 对于子数组
[2, 2, 3, 4, 2, 3],只保留元素 2 和 3。因此,answer[2] = 2 + 2 + 2 + 3 + 3。
示例 2:
输入:nums = [3,8,7,8,7,5], k = 2, x = 2
输出:[11,15,15,15,12]
解释:
由于 k == x,answer[i] 等于子数组 nums[i..i + k - 1] 的总和。
提示:
nums.length == n1 <= n <= 1051 <= nums[i] <= 1091 <= x <= k <= nums.length
方法一:哈希表 + 有序集合 · Go 实现
思考
题意与 I 相同,但 ,每个窗口重算频率不可行,必须在滑动时 维护 top-。
两个有序集合分别存放当前 top- 与其余元素,哈希表记录频率。插入删除仍按「先删旧对、再插新对」的顺序。
用 维护 top- 的加权和,窗口两端进出后调整集合大小至 ,即可线性扫完所有窗口。
我们用一个哈希表 统计窗口中每个元素的出现次数,用一个有序集合 存储窗口中出现次数最多的 个元素,用另一个有序集合 存储剩余的元素。
我们维护一个变量 表示 中元素的和。初始时,我们将前 个元素加入到窗口中,并且更新有序集合 和 ,并且计算 的值。如果 的大小小于 ,并且 不为空,我们就循环将 中的最大元素移动到 中,直到 的大小等于 ,过程中更新 的值。如果 的大小大于 ,我们就循环将 中的最小元素移动到 中,直到 的大小等于 ,过程中更新 的值。此时,我们就可以计算出当前窗口的 ,添加到答案数组中。然后我们将窗口的左边界元素移出,更新 ,并且更新有序集合 和 ,以及 的值。继续遍历数组,直到遍历结束。
时间复杂度 ,空间复杂度 。其中 为数组 的长度。
相似题目:
func findXSum(nums []int, k int, x int) []int64 {
type pair struct{ cnt, v int }
cmpFn := func(a, b pair) int { return cmp.Or(a.cnt-b.cnt, a.v-b.v) }
l := redblacktree.NewWith[pair, struct{}](cmpFn)
r := redblacktree.NewWith[pair, struct{}](cmpFn)
cnt := map[int]int{}
var s int64
add := func(v int) {
if cnt[v] == 0 {
return
}
p := pair{cnt[v], v}
if !l.Empty() && cmpFn(l.Left().Key, p) < 0 {
s += int64(p.cnt) * int64(p.v)
l.Put(p, struct{}{})
} else {
r.Put(p, struct{}{})
}
}
remove := func(v int) {
if cnt[v] == 0 {
return
}
p := pair{cnt[v], v}
if _, ok := l.Get(p); ok {
s -= int64(p.cnt) * int64(p.v)
l.Remove(p)
} else {
r.Remove(p)
}
}
n := len(nums)
ans := make([]int64, 0, n-k+1)
for i, v := range nums {
remove(v)
cnt[v]++
add(v)
j := i - k + 1
if j < 0 {
continue
}
for !r.Empty() && l.Size() < x {
p := r.Right().Key
s += int64(p.cnt) * int64(p.v)
r.Remove(p)
l.Put(p, struct{}{})
}
for l.Size() > x {
p := l.Left().Key
s -= int64(p.cnt) * int64(p.v)
l.Remove(p)
r.Put(p, struct{}{})
}
ans = append(ans, s)
remove(nums[j])
cnt[nums[j]]--
add(nums[j])
}
return ans
}
继续阅读
- 实践案例
- 实践案例
- 实践案例