所有子字符串美丽值之和 图解题解
这道题到底在问什么
- 输入
- s = "aabcb"
- 输出
- 5(美丽值非零的子串是 "aab" "aabc" "aabcb" "abcb" "bcb",各为 1)
- 输入
- 单看一段 "abaacc"
- 输出
- 美丽值 = 3 减 1 = 2(a 出现 3 次最多,b 出现 1 次最少)
先想最直接的笨办法
记牢这套动作:钉住左端、右端逐格往右扩、每加一个字符更新计数面板、用最高频减最低频得到这段的美丽值累加。下面每一帧都在套它,左端一个一个换,右端从左端一路扩到字符串末尾。(动画第 3 步)
最优解:为什么这么做
一句话答案:LeetCode 1781 所有子字符串美丽值之和:枚举每个左端、右端逐格右扩,边扩边维护 26 个字母的计数,每步用最高频减最低频累加,省掉从头重数。时间 O(n²·26)、空间 O(1)。
美丽值到底怎么算,这题要把哪些子串的美丽值相加
先说美丽值怎么来。一个字符串的美丽值,是它里面出现次数最多的字符的次数,减去出现次数最少、但至少出现过一次的字符的次数。题目给你字符串 s,要求把 s 的每一个连续子字符串的美丽值都算出来,再全部加起来。拿题面 s='aabcb' 来说,答案是 5;再看一段 'abaacc',里面 a 出现 3 次最多、b 出现 1 次最少,它的美丽值就是 3 减 1 等于 2。
把每个子串都重新数一遍字符,代价高在哪
长度 n 的字符串,连续子串大约有 n²/2 个,这个数量绕不过去——答案就是要对每一个子串各算一次美丽值。真正贵的是每段的处理:如果对着一个子串,从它的左端到右端把字符重新数一遍,再取最高频和最低频,单个子串就要花和它长度成正比的时间,全部加起来是 O(n³)。字符串稍微一长,这层重复计数就把时间拖垮了。
右端每多接一个字符,为什么计数只需要动一格
相邻两个子串只差最右边那一个字符。把左端钉住不动,右端每往右挪一格,新的子串只比上一个子串在右边多出一个字符,前面那些字符的计数一个都没变。于是不必从头重数,只要把新进来的这个字符的计数加一,就得到了新子串的完整计数——这种每步只改动新增那一格、别的照旧的做法叫增量维护。每扩一格后,在当前计数里取最高频减最低频,就是这一段的美丽值。要留意最低频只在真正出现过的字符里取,计数还是 0 的字母不能算进来。
固定左端、右端逐格扩,整个流程怎么跑
外层从下标 0 开始,依次让每个位置当一次子串的左端;每换一个新左端,就把计数清空。内层让右端从当前左端出发,一格一格扩到字符串末尾,每扩到一个字符就把它的计数加一,再取当前计数里的最高频减最低频,累加进答案。左端走遍所有位置,所有子串就都被覆盖了一遍,累加出来的就是总美丽值。
aabcb 里哪几段子串真有美丽值
左端钉在下标 0:子串 'a'、'aa' 里字符次数都相等,美丽值都是 0;扩到 'aab' 时 a 两次、b 一次,2 减 1 得 1;'aabc'、'aabcb' 里仍是最高 2、最低 1,各得 1,这一轮共加 3。左端挪到下标 1:'a'、'ab'、'abc' 全是 0,'abcb' 里 b 两次、a 和 c 各一次,得 1。左端到下标 2:'b'、'bc' 是 0,'bcb' 得 1。左端在下标 3、4 起头的 'c'、'cb'、'b' 都是 0。有贡献的正好是 'aab' 'aabc' 'aabcb' 'abcb' 'bcb' 五段,加起来是 5。
求最低频,先跳过计数为 0 的字母
这套做法时间是 O(n²·26)、空间 O(1),计数最多占 26 个格子。有几处容易出错。用定长 26 的数组存计数时,那些没在子串里出现的字母还挂着 0,求最低频要跳过这些 0,只在计数大于 0 的字母里取最小;忘了跳过,最低频永远是 0,美丽值就错等于最高频,总和整个偏大。右端扩格必须增量加一,图省事从头重数会把复杂度推回 O(n³)。还有单字符、以及整段同一个字符的子串,它们的美丽值本来就是 0,照常参与累加即可,加 0 不影响结果,特意挑出来跳过反而多此一举。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记牢这套动作:钉住左端、右端逐格往右扩、每加一个字符更新计数面板、用最高频减最低频得到这段的美丽值累加。下面每一帧都在套它,左端一个一个换,右端从左端一路扩到字符串末尾。
- 4换一个新的左端。把左指针钉在下标 0 的 "a" 上,计数面板先清空。接下来右指针会从这里一格一格往右挪,把每一个以下标 0 开头的子串都过一遍。现在面板还是空的,先看好起跑线。
- 5右指针扩到下标 0,把 "a" 计进面板,现在 "a" 出现 1 次。当前子串是 "a",计数面板是 a×1。出现过的字符次数都相等,最高频和最低频一样,这段美丽值是 0,总和不动,还是 0。
- 6右指针扩到下标 1,把 "a" 计进面板,现在 "a" 出现 2 次。当前子串是 "aa",计数面板是 a×2。出现过的字符次数都相等,最高频和最低频一样,这段美丽值是 0,总和不动,还是 0。
- 7右指针扩到下标 2,把 "b" 计进面板,现在 "b" 出现 1 次。当前子串是 "aab",计数面板是 a×2, b×1。出现过的字符里最高频是 2 次、最低频是 1 次,美丽值就是 2 减 1 等于 1,把它累加进总和,现在总和 = 1。
- 8右指针扩到下标 3,把 "c" 计进面板,现在 "c" 出现 1 次。当前子串是 "aabc",计数面板是 a×2, b×1, c×1。出现过的字符里最高频是 2 次、最低频是 1 次,美丽值就是 2 减 1 等于 1,把它累加进总和,现在总和 = 2。
- 9右指针扩到下标 4,把 "b" 计进面板,现在 "b" 出现 2 次。当前子串是 "aabcb",计数面板是 a×2, b×2, c×1。出现过的字符里最高频是 2 次、最低频是 1 次,美丽值就是 2 减 1 等于 1,把它累加进总和,现在总和 = 3。
- 10换一个新的左端。把左指针钉在下标 1 的 "a" 上,计数面板先清空。接下来右指针会从这里一格一格往右挪,把每一个以下标 1 开头的子串都过一遍。现在面板还是空的,先看好起跑线。
- 11右指针扩到下标 1,把 "a" 计进面板,现在 "a" 出现 1 次。当前子串是 "a",计数面板是 a×1。出现过的字符次数都相等,最高频和最低频一样,这段美丽值是 0,总和不动,还是 3。
- 12右指针扩到下标 2,把 "b" 计进面板,现在 "b" 出现 1 次。当前子串是 "ab",计数面板是 a×1, b×1。出现过的字符次数都相等,最高频和最低频一样,这段美丽值是 0,总和不动,还是 3。
- 13右指针扩到下标 3,把 "c" 计进面板,现在 "c" 出现 1 次。当前子串是 "abc",计数面板是 a×1, b×1, c×1。出现过的字符次数都相等,最高频和最低频一样,这段美丽值是 0,总和不动,还是 3。
- 14右指针扩到下标 4,把 "b" 计进面板,现在 "b" 出现 2 次。当前子串是 "abcb",计数面板是 a×1, b×2, c×1。出现过的字符里最高频是 2 次、最低频是 1 次,美丽值就是 2 减 1 等于 1,把它累加进总和,现在总和 = 4。
- 15换一个新的左端。把左指针钉在下标 2 的 "b" 上,计数面板先清空。接下来右指针会从这里一格一格往右挪,把每一个以下标 2 开头的子串都过一遍。现在面板还是空的,先看好起跑线。
- 16右指针扩到下标 2,把 "b" 计进面板,现在 "b" 出现 1 次。当前子串是 "b",计数面板是 b×1。出现过的字符次数都相等,最高频和最低频一样,这段美丽值是 0,总和不动,还是 4。
- 17右指针扩到下标 3,把 "c" 计进面板,现在 "c" 出现 1 次。当前子串是 "bc",计数面板是 b×1, c×1。出现过的字符次数都相等,最高频和最低频一样,这段美丽值是 0,总和不动,还是 4。
- 18右指针扩到下标 4,把 "b" 计进面板,现在 "b" 出现 2 次。当前子串是 "bcb",计数面板是 b×2, c×1。出现过的字符里最高频是 2 次、最低频是 1 次,美丽值就是 2 减 1 等于 1,把它累加进总和,现在总和 = 5。
- 19换一个新的左端。把左指针钉在下标 3 的 "c" 上,计数面板先清空。接下来右指针会从这里一格一格往右挪,把每一个以下标 3 开头的子串都过一遍。现在面板还是空的,先看好起跑线。
- 20右指针扩到下标 3,把 "c" 计进面板,现在 "c" 出现 1 次。当前子串是 "c",计数面板是 c×1。出现过的字符次数都相等,最高频和最低频一样,这段美丽值是 0,总和不动,还是 5。
- 21右指针扩到下标 4,把 "b" 计进面板,现在 "b" 出现 1 次。当前子串是 "cb",计数面板是 c×1, b×1。出现过的字符次数都相等,最高频和最低频一样,这段美丽值是 0,总和不动,还是 5。
- 22换一个新的左端。把左指针钉在下标 4 的 "b" 上,计数面板先清空。接下来右指针会从这里一格一格往右挪,把每一个以下标 4 开头的子串都过一遍。现在面板还是空的,先看好起跑线。
- 23右指针扩到下标 4,把 "b" 计进面板,现在 "b" 出现 1 次。当前子串是 "b",计数面板是 b×1。出现过的字符次数都相等,最高频和最低频一样,这段美丽值是 0,总和不动,还是 5。
- 24五个左端都把右端扩到底了。回放一下:一共 15 个子串,美丽值不为 0 的正好是 "aab" "aabc" "aabcb" "abcb" "bcb" 这 5 段,每段都是 a 或 b 多一个、另一个字符少一个,最高频减最低频等于 1。其余子串要么全是同一个字符、要么每种字符都只出现一次,美丽值都是 0。把它们加起来,总和正好是 5。全程只做了计数加一、扫面板取最高最低、累加这三种小操作。
⚠️ 容易写错的地方
✗ 错:用定长 26 数组求最低频时,把计数为 0 的字母也算进去了
✓ 对:最低频只在「出现过的字符」里取,必须加「v 大于 0」的判断跳过 0
没在子串里出现的字母,它在数组里的计数是 0;如果把这些 0 也拿去比最小,最低频就永远是 0,美丽值会错等于最高频,整个答案偏大
✗ 错:内层右端每扩一格,都把当前子串的计数从头重新数一遍
✓ 对:右端每扩一格,只把新进来的那个字符计数加一,增量维护
新子串只比上一个子串在右边多一个字符,其它计数都没变;从头重建会多出一层和子串长度成正比的开销,总复杂度从 n 平方升到 n 的三次方
✗ 错:以为要把美丽值为 0 的子串挑出来跳过
✓ 对:全同字符、单字符这类子串美丽值本来就是 0,照常参与,只是加 0
题目求的是所有子串美丽值之和,加 0 不影响结果;特意去判断反而多此一举,真正贡献的是最高频和最低频拉开差距的那些子串
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def beautySum(self, s: str) -> int:
ans, n = 0, len(s)
for i in range(n):
cnt = Counter()
for j in range(i, n):
cnt[s[j]] += 1
ans += max(cnt.values()) - min(cnt.values())
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int beautySum(string s) {
int ans = 0;
int n = s.size();
int cnt[26];
for (int i = 0; i < n; ++i) {
memset(cnt, 0, sizeof cnt);
for (int j = i; j < n; ++j) {
++cnt[s[j] - 'a'];
int mi = 1000, mx = 0;
for (int& v : cnt) {
if (v > 0) {
mi = min(mi, v);
mx = max(mx, v);
}
}
ans += mx - mi;
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int beautySum(String s) {
int ans = 0;
int n = s.length();
for (int i = 0; i < n; ++i) {
int[] cnt = new int[26];
for (int j = i; j < n; ++j) {
++cnt[s.charAt(j) - 'a'];
int mi = 1000, mx = 0;
for (int v : cnt) {
if (v > 0) {
mi = Math.min(mi, v);
mx = Math.max(mx, v);
}
}
ans += mx - mi;
}
}
return ans;
}
}复杂度
时间
O(n²·Σ)
n 是字符串长度,Σ 是字符集大小 26。外层左端 n 个,内层右端最多 n 个,合起来枚举了大约 n 平方个子串;每扩一格都要扫一遍 26 个计数取最高频和最低频,26 是常数。Python 用 Counter 求 max 和 min 是在出现过的字符上,最多也是 26 个,同阶
空间
O(Σ) = O(1)
计数结构最多装 26 个字母的计数,不随字符串变长而增加,峰值就是这 26 个格子,是常数级
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 所有子字符串美丽值之和 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
右端每扩一格,为什么可以不把子串的计数从头重建?+
因为新子串只比上一个子串在右边多了一个字符,其余字符的计数完全没变。只要把新进来那个字符的计数加一,就是新子串的完整计数,是常数时间。要是每扩一格都从左端重新数一遍,内层就多出一层和子串长度成正比的开销,总复杂度会从 O(n²) 升到 O(n³)。
这道题还能不能做到比 O(n²) 更快?+
很难。答案要对全部子串求和,而长度 n 的字符串本身就有约 n²/2 个子串,每一段都得贡献自己的美丽值,光是走遍这些子串就已经是 n² 量级。这里已经把每段的处理压到常数——只加一个字符、再扫一遍 26 个计数,所以 O(n²) 基本就是这类「对所有子串某个统计量求和」问题的下界。
为什么最低频只能在出现过的字符里取?+
美丽值的定义里,最低频指的是出现过、但次数最少的那个字符。如果用定长 26 数组存计数,没出现的字母计数是 0;把这些 0 也拿去比最小,最低频就恒等于 0,美丽值会退化成只等于最高频,答案偏大。所以取最小时要跳过计数为 0 的字母,只在大于 0 的计数里比。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 所有子字符串美丽值之和 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。