当前位置: 首页 > article >正文

【滑动窗口】至少有k个重复字符的最长子串

前言

编程语言: Java/go实现
滑动窗口的一道标注medium:no -> 实际hard的题, 本题题解还能用滑动窗口解是我没想到的。
本题应用滑动窗口的条件是通过题目给定限制猜解法, 然后枚举常数次行为来控制变量, 进而构建了单调性的条件, 应用了滑动窗口。
没有单调性转化构建处单调性, 子串问题是连续的, 故而应用了滑动窗口这个技巧。



题目:395. 至少有 K 个重复字符的最长子串

给定一个字符串 s 和一个整数 k,找出 s 中长度最长的子串,要求子串中的每个字符出现的次数均不小于 k。如果不存在这样的子串,则返回 0。

思路:滑动窗口解法

因为蒟蒻,不太会动态规划,分治递归也没看题解。滑动窗口写了一份代码。求轻喷……我是菜鸟呜呜呜……

解题思路

这道题既要控制字符种类,又要保证每个子串中的每个字符出现次数大于等于 k
一看就是 Hard 题披着 中等 的衣服……

不过题目范围给定了 s 仅由小写英文字母组成,因此可以按照字符种类(‘a’ -> ‘z’)穷举,相当于控制变量法。

  • 一个种类的字符子串求一个最长长度结果。
  • 两个种类字符的子串求一个最长长度结果。
  • ……以此类推,直至求出 26 种字符的结果。

然后依次 PK,最终得到整个函数的返回值。


为什么联想到滑动窗口呢?

求解子数组或者子串的最长长度,根据经验可以联想到滑动窗口的解法。
其实是单调性分析出来的。


以示例 2 为例分析

示例 2:
输入s = "ababbc", k = 2
输出5
解释:最长子串为 "ababb" ,其中 'a' 重复了 2 次, 'b' 重复了 3 次。

分析步骤
  1. 一种字符的情况
    满足一种字符且该字符大于等于 k=2 次,字符串 "bb" 符合,长度为 2
  2. 两种字符的情况
    满足两种字符且两个字符大于等于 2 次,子串 "ababb" 符合,长度为 5
  3. 三种字符的情况
    满足三种字符且三个字符大于等于 2 次,没有子串符合,因为 'c' 至多出现一次,没有一个正确的结构,不参与比较。
  4. 枚举结束
    将 3 种情况的结果进行 PK,决出最大的子串长度 5

核心思想总结

  • 说明:当子串包含两种字符 ab 时,它们满足 >=k 的条件,且子串长度最长。
  • 具体实现:题目要求子串满足字符种类和每个字符 >=k 两种复合条件处理,因此以滑动窗口结合字符种类穷举即可。

解题过程

题目函数如下:

class Solution {
    public int longestSubstring(String s, int k) {
        
    }
}

以下是我的 Java 代码实现:


代码实现

首先搞一个哈希表。由于只有小写字母,因此开一个长度为 26 的数组空间。利用 字符-'a' 将小写字母的 ASCII 值映射到 0, 1, 2, ... 25

比如

  • 'a': 97 - 'a' => 0 下标处。
  • 'z': 'z' - 'a' => 25

相信大佬们都会。这个数组哈希表主要是统计字符串中字母的种类数。

int[] hash = new int[26];
int cnt;  // 记录字符种类数

既然要统计字符种类,为什么不用布尔数组呢?
答案:因为下面还要复用这个 hash 数组。


实现滑动窗口

1. 外层循环

cnt 是总的字符种类数,要从 1 种字符种类枚举到 cnt 种类型,因此需要写个外层循环。

2. 内层滑动窗口

复用 hash 数组:

  • key:字符种类。
  • value:该字符出现的次数。

定义 type 为字符类型数,从 1 -> cnt

3. 核心过程

滑动窗口具体操作如下:

  • 定义 l, r = 0, 0 为滑动窗口的同向双指针。
  • 定义 kindssatisfy
    • kinds 是当前窗口的字符种类数。
    • satisfy 是当前窗口内满足条件(即字符个数 >=k)的字符数。
  • 初始化 hash 数组和上述变量。

窗口扩展

  1. 右指针扩展

    • r 位置的字符映射到哈希表的位置,使其自增 1。
    • 如果该字符是第一次出现在窗口内(hash[index] == 1),种类数 kinds++
    • 如果当前字符满足个数 >=k,则满足条件的字符数 satisfy++
  2. 左指针收缩

    • 如果当前 kinds 大于此时枚举字符数 type,开始循环挪动左指针。
    • 如果 hash[index2] 数量由 1 -> 0,说明该字符被移除,kinds--
    • 如果数量是 k -> k-1,满足条件的字符数 satisfy--
    • 循环逻辑直至 kinds == type

合法窗口更新答案

字符的种类数已经符合要求,查看满足要求的字符数是否等于 type
如果满足条件,则结算:

ans = Math.max(ans, r - l + 1);

最终返回答案 ans


复杂度分析

  • 时间复杂度
    ( O ( n ) ) (O(n)) (O(n)),因为外层循环最多 O ( c n t ) O(cnt) O(cnt),内层循环为滑动窗口 O ( n ) O(n) O(n)cnt 最多是 26,因此 O ( 26 ) × O ( n ) = O ( n ) O(26) \times O(n) = O(n) O(26)×O(n)=O(n)

  • 空间复杂度
    O ( 1 ) O(1) O(1),仅仅用了几个变量,以及一个固定长度的 int[26] 数组作哈希表。


Java 代码

以下是完整代码:

class Solution {
    public int longestSubstring(String s, int k) {
        int n = s.length();
        int ans = 0;

        for (int type = 1; type <= 26; type++) {
            int[] hash = new int[26];
            int l = 0, r = 0, kinds = 0, satisfy = 0;

            while (r < n) {
                int indexR = s.charAt(r) - 'a';
                hash[indexR]++;
                if (hash[indexR] == 1) kinds++;
                if (hash[indexR] == k) satisfy++;

                while (kinds > type) {
                    int indexL = s.charAt(l) - 'a';
                    if (hash[indexL] == k) satisfy--;
                    if (hash[indexL] == 1) kinds--;
                    hash[indexL]--;
                    l++;
                }

                if (kinds == type && kinds == satisfy) {
                    ans = Math.max(ans, r - l + 1);
                }
                r++;
            }
        }

        return ans;
    }
}

Go代码

func longestSubstring(s string, k int) int {
    var hash [26]int
    cnt := 0
    for _,ch := range s{
        if hash[ch-'a']==0{
            cnt += 1
        }
        hash[ch-'a']=1
    }
    ans := 0
    for i:=1;i<=cnt;i++{
        memeset_hash(&hash)
        var l,r,kinds,satisfy int
        for r<len(s){
            index := s[r] - 'a'
            hash[index] += 1
            if hash[index] == 1 {
                kinds += 1
            }
            if hash[index] == k {
                satisfy += 1
            }
            for kinds > i{
                index2 := s[l] - 'a'
                hash[index2] -= 1
                if hash[index2] == 0{
                    kinds -= 1
                }
                if hash[index2] == k-1{
                    satisfy -= 1
                }
                l += 1
            }
            if satisfy == i{
                ans = max(ans, r - l + 1)
            }
            r += 1
        }
    }
    return ans
}

func memeset_hash(hash *[26]int){
    for i := range hash{
        hash[i]=0
    }
}

func max(x, y int) int{
    if x > y{
        return x
    }
    return y
}

总结

通过数据范围进行常数次枚举, 构造单调性,然后结合子串问题应用了滑动窗口的技巧。
本题还有动态规划和分治递归的热门解法, 读者可以查看一下leetcode的相关题解。
希望对您有帮助,😉😉😉…


http://www.kler.cn/a/405416.html

相关文章:

  • 锂电池学习笔记(一) 初识锂电池
  • 介绍一下strncmp(c基础)
  • 【2024亚太杯亚太赛APMCM C题】数学建模竞赛|宠物行业及相关产业的发展分析与策略|建模过程+完整代码论文全解全析
  • OpenGL 进阶系列14 - 曲面细分着色器
  • nginx 配置lua执行shell脚本
  • springboot整合License授权控制
  • 系统思考—跳出症状看全局
  • 【linux013】文件操作命令篇 - less 命令
  • python使用 `importlib.resources` 管理资源文件
  • FPC柔性线路板与智能生活的融合
  • 【电路笔记 TMS320F28335DSP】时钟+看门狗+相关寄存器(功能模块使能、时钟频率配置、看门狗配置)
  • Spark RDD(弹性分布式数据集)的深度理解
  • 向量数据库FAISS之五:原理(LSH、PQ、HNSW、IVF)
  • 基于深度学习的机动车驾驶重量识别预测研究思路,引入注意力,以及实验验证与性能评估
  • STM32 BootLoader 刷新项目 (十一) Flash写操作-命令0x57
  • Unity开发抖音小游戏使用长音频和短音频
  • Docker Compose安装部署PostgreSQL数据库
  • 工商银行湖仓智一体创新应用实践
  • 数据结构——栈、队列
  • GCN分类预测 | 基于图卷积神经网络GCN多特征分类预测(多输入单输出) Matlab代码
  • Rust 的简介
  • 应用监控——springboot admin
  • TypeScript 的发展与基本语法
  • docker搭建私有仓库,实现镜像的推送和拉取
  • 2024 APMCM亚太数学建模C题 - 宠物行业及相关产业的发展分析和策略 完整参考论文(2)
  • ffmpeg视频滤镜:替换部分帧-freezeframes