LeetCode 828. 统计子串中的唯一字符
题目描述


题意分析
字符串只含大写英文字母。对每个非空子串,统计其中恰好出现一次的字符个数,再将结果相加。“唯一”不是不同字符的种类数;内容相同但起止位置不同的子串也要分别计算。
解法:按字符贡献计数(前后出现位置)
核心思路
[!blue]
直接枚举所有子串会重复统计,换成问每次字符出现能为多少个子串贡献 1。固定位置
i,设同字符最近的前、后出现位置分别为left、right。要让s[i]在子串中恰好出现一次,子串必须包含i,同时不能包含这两个最近的同字符。所以左端点可选
left + 1..i,共i - left种;右端点可选i..right - 1,共right - i种。两端独立选择,贡献为(i - left) * (right - i)。子串中每个被统计的唯一字符都对应其中唯一的一次出现,因此把所有位置贡献相加,恰好等于题目要求的总和。从左到右扫描时,当前位置还不知道自己的后继,但已经能确定同字符上一次出现的后继。用
prev[c]、prevPrev[c]记录字符c最近和次近的出现位置;新出现于i时,就为旧的prev[c]结算(prev[c] - prevPrev[c]) * (i - prev[c]),再滚动更新这两个位置。两个位置初始为
-1,使首次出现允许子串从下标 0 开始;尚未出现过的字符不结算。扫描结束后,每种字符只剩最后一次出现尚未结算,把它的后继设为n,便允许右端点延伸到n - 1。代码用宽整数乘法和累加,最后按题目保证的 32 位答案返回。
解题步骤
- 将每个字符的最近两次位置初始化为 -1。
- 遇到字符时,先结算其上一次出现的贡献。
- 将两次位置向前滚动。
- 扫描结束用 n 作为右边界补齐最后一批贡献。
代码实现
class Solution {
public int uniqueLetterString(String s) {
int n = s.length();
int[] prevPrev = new int[26];
int[] prev = new int[26];
Arrays.fill(prevPrev, -1);
Arrays.fill(prev, -1);
long answer = 0;
for (int i = 0; i < n; i++) {
int c = s.charAt(i) - 'A';
if (prev[c] != -1) {
// 当前出现确定上一次的右边界,立即结算那次出现的贡献。
answer += (long) (prev[c] - prevPrev[c]) * (i - prev[c]);
}
// 先保存旧的最近位置,再更新为当前位置。
prevPrev[c] = prev[c];
prev[c] = i;
}
for (int c = 0; c < 26; c++) {
if (prev[c] != -1) {
// 最后一次出现没有真实后继,以字符串末尾之外作为右边界。
answer += (long) (prev[c] - prevPrev[c]) * (n - prev[c]);
}
}
return (int) answer;
}
}
func uniqueLetterString(s string) int {
n := len(s)
prevPrev := make([]int, 26)
prev := make([]int, 26)
for i := 0; i < 26; i++ {
prevPrev[i] = -1
prev[i] = -1
}
var answer int64 = 0
for i := 0; i < n; i++ {
c := int(s[i] - 'A')
if prev[c] != -1 {
// 当前出现确定上一次的右边界,立即结算那次出现的贡献。
answer += int64(prev[c]-prevPrev[c]) * int64(i-prev[c])
}
// 先保存旧的最近位置,再更新为当前位置。
prevPrev[c] = prev[c]
prev[c] = i
}
for c := 0; c < 26; c++ {
if prev[c] != -1 {
// 最后一次出现没有真实后继,以字符串末尾之外作为右边界。
answer += int64(prev[c]-prevPrev[c]) * int64(n-prev[c])
}
}
return int(answer)
}
复杂度分析
- 时间复杂度:$O(n+26)$,逐位置扫描并遍历固定字符集收尾。
- 空间复杂度:$O(26)$,两组位置记录。
关键点总结
[!green]
- 贡献属于一次出现,左右端点选择数相乘。
- 先结算旧位置,再更新记录。
- 没有下一次出现的字符需要统一收尾。
- 每轮结束时,每种字符除了最近一次出现,其余出现的贡献都已结算。
易错点总结
[!yellow]
- 将唯一理解为字符种类数:子串中出现多次的字符不能贡献 1。
- 缺失位置用零初始化:漏掉允许从字符串开头开始的子串。
- 先覆盖最近位置,再保存次近位置:两份记录变成同一位置。
- 漏掉收尾:每个字符的最后一次出现都未计入。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 2262. 字符串的总引力 | 困难 | 原题按子串不同字符数计吸引力,本题只计恰出现一次的字符,位置贡献的边界不同。 |
| 907. 子数组的最小值之和 | 中等 | 同样从枚举所有子数组转为统计每个位置的贡献,本题边界由相同字符的前后出现位置决定。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!