【答疑必看】至少有K个重复字符的最长子串:枚举 + 双指针|Java 刷题打卡

简介: 【答疑必看】至少有K个重复字符的最长子串:枚举 + 双指针|Java 刷题打卡

网络异常,图片无法展示
|


题目描述



这是 LeetCode 上的395. 至少有K个重复字符的最长子串,难度为 Medium


给你一个字符串 s 和一个整数 k ,请你找出 s 中的最长子串, 要求该子串中的每一字符出现次数都不少于 k 。


返回这一子串的长度。


示例 1:


输入:s = "aaabb", k = 3
输出:3
解释:最长子串为 "aaa" ,其中 'a' 重复了 3 次。
复制代码


示例 2:


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


提示:


  • 1 <= s.length <= 10^4104
  • s 仅由小写英文字母组成
  • 1 <= k <= 10^5105


枚举 + 双指针



其实看到这道题,我第一反应是「二分」,直接「二分」答案。


但是往下分析就能发现「二分」不可行,因为不具有二段性质。


也就是假设有长度 t 的一段区间满足要求的话,t + 1 长度的区间是否「一定满足」或者「一定不满足」呢?


显然并不一定,是否满足取决于 t + 1 个位置出现的字符在不在原有区间内。


举个🌰吧,假设我们已经画出来一段长度为 t 的区间满足要求(且此时 k > 1),那么当我们将长度扩成 t + 1 的时候(无论是往左扩还是往右扩):


  • 如果新位置的字符在原有区间出现过,那必然还是满足出现次数大于 k,这时候 t + 1 的长度满足要求
  • 如果新位置的字符在原有区间没出现过,那新字符的出现次数只有一次,这时候 t + 1 的长度不满足要求


因此我们无法是使用「二分」,相应的也无法直接使用「滑动窗口」思路的双指针。


双指针其实也是利用了二段性质,当一个指针确定在某个位置,另外一个指针能够落在某个明确的分割点,使得左半部分满足,右半部分不满足。


那么还有什么性质可以利用呢?这时候要留意数据范围「数值小」的内容。


题目说明了只包含小写字母(26 个,为有限数据),我们可以枚举最大长度所包含的字符类型数量,答案必然是 [1, 26],即最少包含 1 个字母,最多包含 26 个字母。


你会发现,当确定了长度所包含的字符种类数量时,区间重新具有了二段性质。


当我们使用双指针的时候:


  1. 右端点往右移动必然会导致字符类型数量增加(或不变)
  2. 左端点往右移动必然会导致字符类型数量减少(或不变)


当然,我们还需要记录有多少字符符合要求(出现次数不少于 k),当区间内所有字符都符合时更新答案。


代码:


class Solution {
    public int longestSubstring(String s, int k) {
        int ans = 0;
        int n = s.length();
        char[] cs = s.toCharArray();
        int[] cnt = new int[26];
        for (int p = 1; p <= 26; p++) {
            Arrays.fill(cnt, 0);
            // tot 代表 [j, i] 区间所有的字符种类数量;sum 代表满足「出现次数不少于 k」的字符种类数量
            for (int i = 0, j = 0, tot = 0, sum = 0; i < n; i++) {
                int u = cs[i] - 'a';
                cnt[u]++;
                // 如果添加到 cnt 之后为 1,说明字符总数 +1
                if (cnt[u] == 1) tot++;
                // 如果添加到 cnt 之后等于 k,说明该字符从不达标变为达标,达标数量 + 1
                if (cnt[u] == k) sum++;
                // 当区间所包含的字符种类数量 tot 超过了当前限定的数量 p,那么我们要删除掉一些字母,即「左指针」右移
                while (tot > p) {
                    int t = cs[j++] - 'a';
                    cnt[t]--;
                    // 如果添加到 cnt 之后为 0,说明字符总数-1
                    if (cnt[t] == 0) tot--;
                    // 如果添加到 cnt 之后等于 k - 1,说明该字符从达标变为不达标,达标数量 - 1
                    if (cnt[t] == k - 1) sum--;
                }
                // 当所有字符都符合要求,更新答案
                if (tot == sum) ans = Math.max(ans, i - j + 1);
            }
        }
        return ans;
    }
}
复制代码


  • 时间复杂度:枚举 26 种可能性,每种可能性会扫描一遍数组。复杂度为 O(n)O(n)
  • 空间复杂度:O(n)O(n)


答疑



为什么要先枚举 26 种可能性(答案所包含的字符种类数量)? 本质是什么?


题解只是简单说了一下是为了恢复区间的二段性,但主要目的是为了恢复双指针的单调性


双指针单调性:为当双指针的一侧(例如右侧指针)确定时,双指针的另外一侧(例如左侧指针)能够明确落在某个分割点,使得一边成立,另外一边不成立。


我们要恢复双指针的单调性,才能解决「不知道指针应该往哪个方向移动」的问题。


完整的思考过程如下:


首先我们知道答案子串的左边界左侧的字符以及右边界右侧的字符一定不会出现在子串中,否则就不会是最优解


如果我们只从该性质出发的话,朴素解法应该是使用一个滑动窗口,不断的调整滑动窗口的左右边界,使其满足「左边界左侧的字符以及右边界右侧的字符一定不会出现在窗口中」,这实际上就是双指针解法,但是如果不先敲定(枚举)出答案所包含的字符数量的话,这里的双指针是不具有单调性的


换句话说,只利用这一性质是没法完成逻辑的。


这时候我们面临的问题是:性质是正确的,但是还无法直接利用


因此我们需要先利用字符数量有限性(可枚举)作为切入点,使得「答案子串的左边界左侧的字符以及右边界右侧的字符一定不会出现在子串中」这一性质在双指针的实现下具有单调性。也就是题解说的让区间重新具有二段性质


然后遍历 26 种可能性(答案所包含的字符种类数量),对每种可能性应用滑动窗口(由上述性质确保正确),可以得到每种可能性的最大值(局部最优),由所有可能性的最大值可以得出答案(全局最优)


点评



这道题的突破口分析其实和 1178. 猜字谜 类似。


解决思路:当我们采用常规的分析思路发现无法进行时,要去关注一下数据范围中「数值小」的值。因为数值小其实是代表了「可枚举」,往往是解题或者降低复杂度的一个重要(甚至是唯一)的突破口。


要学着习惯这种「突破口」分析思路哦 ~


最后



这是我们「刷穿 LeetCode」系列文章的第 No.* 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先将所有不带锁的题目刷完。


在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。


为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour…


在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

相关文章
|
前端开发 JavaScript Java
【前端学java】Java中的接口和枚举概念(8)
【8月更文挑战第9天】Java中的接口和枚举概念(8)
117 4
|
10月前
|
安全 Java 测试技术
🎉Java零基础:全面解析枚举的强大功能
【10月更文挑战第19天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
247 60
|
10月前
|
Java
java小工具util系列4:基础工具代码(Msg、PageResult、Response、常量、枚举)
java小工具util系列4:基础工具代码(Msg、PageResult、Response、常量、枚举)
131 24
|
12月前
|
安全 Java 索引
Java——反射&枚举
本文介绍了Java反射机制及其应用,包括获取Class对象、构造方法、成员变量和成员方法。反射允许在运行时动态操作类和对象,例如创建对象、调用方法和访问字段。文章详细解释了不同方法的使用方式及其注意事项,并展示了如何通过反射获取类的各种信息。此外,还介绍了枚举类型的特点和使用方法,包括枚举的构造方法及其在反射中的特殊处理。
197 9
Java——反射&枚举
|
12月前
|
Java
java小工具util系列4:基础工具代码(Msg、PageResult、Response、常量、枚举)
java小工具util系列4:基础工具代码(Msg、PageResult、Response、常量、枚举)
179 5
|
12月前
|
安全 Java 开发者
Java 枚举(enum)详解
Java 中的枚举(`enum`)是一种特殊的数据类型,用于定义一组固定的常量,提升代码的类型安全性和可读性。枚举使用 `enum` 关键字定义,支持方法和构造函数,具有类型安全、单例、自动序列化等特点,并且可以遍历和用于 `switch` 语句中。实际应用包括状态机、指令集、类型标识等场景。枚举使代码更加清晰易维护。
911 1
Java枚举使用的基本案例
这篇文章是关于Java枚举的基本使用,通过一个指令下发的代码案例,展示了如何定义枚举、使用枚举以及如何通过枚举实现指令的匹配和处理。
|
Java 开发者
在Java编程中,if-else与switch作为核心的条件控制语句,各有千秋。if-else基于条件分支,适用于复杂逻辑;而switch则擅长处理枚举或固定选项列表,提供简洁高效的解决方案
在Java编程中,if-else与switch作为核心的条件控制语句,各有千秋。if-else基于条件分支,适用于复杂逻辑;而switch则擅长处理枚举或固定选项列表,提供简洁高效的解决方案。本文通过技术综述及示例代码,剖析两者在性能上的差异。if-else具有短路特性,但条件增多时JVM会优化提升性能;switch则利用跳转表机制,在处理大量固定选项时表现出色。通过实验对比可见,switch在重复case值处理上通常更快。尽管如此,选择时还需兼顾代码的可读性和维护性。理解这些细节有助于开发者编写出既高效又优雅的Java代码。
180 2
|
安全 Java 编译器
【Java】内部类、枚举、泛型
【Java】内部类、枚举、泛型