LeetCode算法日记 - Day 88: 环绕字符串中唯一的子字符串

目录

[1. 环绕字符串中唯一的子字符串](#1. 环绕字符串中唯一的子字符串)

[1.1 题目解析](#1.1 题目解析)

[1.2 解法](#1.2 解法)

[1.3 代码实现](#1.3 代码实现)


1. 环绕字符串中唯一的子字符串

https://leetcode.cn/problems/unique-substrings-in-wraparound-string/description/
定义字符串 base 为一个 "abcdefghijklmnopqrstuvwxyz" 无限环绕的字符串,所以 base 看起来是这样的:

  • "...zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd....".

给你一个字符串 s ,请你统计并返回 s 中有多少 不同 非空子串 也在 base 中出现。

示例 1:

复制代码
输入:s = "a"
输出:1
解释:字符串 s 的子字符串 "a" 在 base 中出现。

示例 2:

复制代码
输入:s = "cac"
输出:2
解释:字符串 s 有两个子字符串 ("a", "c") 在 base 中出现。

示例 3:

复制代码
输入:s = "zab"
输出:6
解释:字符串 s 有六个子字符串 ("z", "a", "b", "za", "ab", and "zab") 在 base 中出现。

提示:

  • 1 <= s.length <= 105
  • s 由小写英文字母组成

1.1 题目解析

题目本质

统计字符串中有多少不同的连续子串符合字母表循环顺序。本质是"连续性判断 + 去重统计"问题,核心在于识别符合 base 规则的子串(按字母顺序连续,z 可接 a),并统计不重复的个数。

常规解法

最直观的想法是枚举所有子串,逐个检查是否符合连续规则。例如 s = "zab",枚举 "z"、"za"、"zab"、"a"、"ab"、"b",每个子串都检查相邻字符是否连续。使用 HashSet 去重,最后返回集合大小。

问题分析

枚举所有子串需要 O(n²) 时间,每个子串检查连续性需要 O(n),总复杂度 O(n³)。对于长度 10⁵ 的字符串会超时。更关键的是,大量子串会重复存储,例如 "abc" 包含的 "a"、"ab" 会在其他位置重复计数,HashSet 虽然去重但效率低。

思路转折

要想高效 → 必须找到去重的规律 → 观察子串特征。

关键发现:如果存在长度为 k 的连续子串以字符 'c' 结尾,那么以 'c' 结尾的所有更短的连续子串(长度 1 到 k-1)都已经被包含了。例如 "abc" 以 'c' 结尾且长度 3,则包含了 "c"(长度1)和 "bc"(长度2)。因此,只需记录每个字符结尾的最长连续长度,就能自动去重。用动态规划计算每个位置的连续长度,再用哈希表记录 26 个字母各自的最大长度,求和即得答案。

1.2 解法

算法思想: 动态规划 + 按字符去重。定义 dpi 表示以位置 i 结尾的最长连续子串长度,如果 si-1 和 si 连续(满足字母顺序或 z→a),则 dpi = dpi-1 + 1,否则 dpi = 1。用 hashc 记录以字符 c 结尾的最长连续长度。最终答案是所有 hashc 的和,因为长度为 k 的连续串包含了 k 个不同的子串。

状态转移方程:

java 复制代码
dp[i] = dp[i-1] + 1  如果 s[i-1]+1 == s[i] 或 (s[i-1]=='z' && s[i]=='a')

dp[i] = 1            否则

hash[c] = max(hash[c], dp[i])  其中 c = s[i]

i)初始化: 创建 dpn 数组全部填充为 1(每个字符本身是长度为 1 的连续串),创建 hash26 数组记录 26 个字母的最大连续长度

**ii)计算连续长度:**从 i=1 开始遍历字符串,判断 si 和 si-1 是否连续:

  • 若 chi-1 + 1 == chi(正常字母顺序,如 a→b)
  • 或 chi-1 == 'z' && chi == 'a'(循环边界)
  • 则 dpi = dpi-1 + 1,表示连续长度增加

**iii)记录最大值:**遍历所有位置 i,用 hashch\[i - 'a'] 保存以字符 chi 结尾的最长连续长度,取 Math.max(hashch\[i-'a'], dpi) 确保记录最大值

**iv)统计答案:**遍历 hash 数组(26 个字母),累加所有最大连续长度,得到不重复子串总数

易错点

  • 去重逻辑理解: 为什么 hash 存最大值就能去重?因为长度为 k 的以 'c' 结尾的连续串,自动包含了长度 1 到 k 的所有以 'c' 结尾的子串(如 "abc" 包含 "c"、"bc"、"abc")。不同位置的同一字符,保留最长的即可覆盖所有情况
  • **循环边界:**最后求和时应该循环 26 次(i < 26),而不是 n 次,因为 hash 数组长度固定为 26,循环 n 次会在 n > 26 时数组越界
  • z→a 的判断: 别忘了处理循环边界 chi-1 == 'z' && chi == 'a',这也算连续。注意需要用括号括起来:(chi-1 == 'z' && chi == 'a')
  • **dp 初始化:**所有 dpi 初始化为 1,因为单个字符本身就是长度为 1 的有效子串
  • 为什么累加 hash: hashc = k 表示以字符 c 结尾有 k 个不同的连续子串(长度从 1 到 k),所以直接累加 hash 值就是总数

1.3 代码实现

java 复制代码
class Solution {
    public int findSubstringInWraproundString(String s) {
        int n = s.length();
        char[] ch = s.toCharArray();
        
        // dp[i]: 以位置 i 结尾的最长连续长度
        int[] dp = new int[n];
        Arrays.fill(dp, 1);
        
        // hash[c]: 以字符 c 结尾的最长连续长度
        int[] hash = new int[26];
        
        // 计算每个位置的连续长度
        for (int i = 1; i < n; i++) {
            if (ch[i-1] + 1 == ch[i] || (ch[i-1] == 'z' && ch[i] == 'a')) {
                dp[i] = dp[i-1] + 1;
            }
        }
        
        // 记录每个字符的最大连续长度
        for (int i = 0; i < n; i++) {
            hash[ch[i] - 'a'] = Math.max(hash[ch[i] - 'a'], dp[i]);
        }
        
        // 累加所有字符的贡献
        int ret = 0;
        for (int i = 0; i < 26; i++) {
            ret += hash[i];
        }
        
        return ret;
    }
}

复杂度分析

  • **时间复杂度: O(n),**三次线性遍历:计算 dp 数组 O(n),更新 hash 数组 O(n),累加结果 O(26) = O(1)
  • **空间复杂度: O(n),**dp 数组占用 O(n) 空间,hash 数组占用 O(26) = O(1) 空间
相关推荐
用户老李8 小时前
卡尔曼滤波自学教程 10:无迹卡尔曼滤波(UKF)
算法
FinelyYang9 小时前
考试系统简答题 AI 阅卷:覆盖度驱动、异步评分与人机协同落地实践
java·大数据·人工智能
hhhjhl9 小时前
Yup 跨端三方库仓颉适配全流程
java·开发语言
用户3126874877209 小时前
Java 对象内存布局:压缩指针、对齐填充与 JOL 实测
java
东华果汁哥9 小时前
【Spring AI 实战】从零开始调用大模型接口,构建 Java AI 应用
java·人工智能·spring
深圳市益普科技有限公司10 小时前
半导体良率管理YMS:良率与设备参数的相关性,先拉哪几组数据
人工智能·python·算法
挖井观天10 小时前
《Java数据结构》第一讲:顺序表
数据结构
Song_RsHt10 小时前
Redis 与微服务,什么场景下无可替代
java·redis·后端·学习·微服务
学长毕设10 小时前
基于SpringBoot的建筑材料管理系统-附源码
java·spring boot·后端
摸爬滚打的小李11 小时前
关于 Arthas
java·后端