力扣828-统计子串中的唯一字符

  |  

摘要: 用前缀和维护前缀的某种状态

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


各位好,在文章 【模板】前缀和与差分 中,我们串讲了前缀和的算法原理和代码模板、在 差分的应用 中,我们学习了用差分的思想解决数列上的一些问题的方法。

在文章 频数前缀和:用前缀和的思想快速计算区间上各个值的计数信息 中,我们了解了用前缀和的思想快速求区间上的某个指标(最常见的是计数信息)的做法。

本文我们继续看一个通过预处理前缀的某种状态信息,然后快速计算我们所求的指标的问题。

这个问题的解法隐含了算法中两种常用的思想:

  • 将贡献拆分,也就是分别计算各个元素对答案的贡献,然后再整合到一起形成总的答案,参考文章 分别计算各元素对答案的贡献的思想
  • 预处理+中心扩散法的处理流程,其中预处理的信息一般是以 $i$ 为中心点向左和向右可以扩散到的位置,一般都是前缀和后缀的某种信息,参考文章 中心扩散法

$1 题目

我们定义了一个函数 countUniqueChars(s) 来统计字符串 s 中的唯一字符,并返回唯一字符的个数。

例如:s = “LEETCODE” ,则其中 “L”, “T”,”C”,”O”,”D” 都是唯一字符,因为它们只出现一次,所以 countUniqueChars(s) = 5 。

本题将会给你一个字符串 s ,我们需要返回 countUniqueChars(t) 的总和,其中 t 是用前缀和的思想快速计算区间上的指标 s 的子字符串。注意,某些子字符串可能是重复的,但你统计时也必须算上这些重复的子字符串(也就是说,你必须统计 s 的所有子字符串中的唯一字符)。

由于答案可能非常大,请将结果 mod 1e9 + 7 后再返回。

提示:

1
2
0 <= s.length <= 10^4
s 只包含大写英文字符

示例 1:

输入: “ABC”
输出: 10
解释: 所有可能的子串为:”A”,”B”,”C”,”AB”,”BC” 和 “ABC”。
其中,每一个子串都由独特字符构成。
所以其长度总和为:1 + 1 + 1 + 2 + 2 + 3 = 10
示例 2:

输入: “ABA”
输出: 8
解释: 除了 countUniqueChars(“ABA”) = 1 之外,其余与示例 1 相同。
示例 3:

输入:s = “LEETCODE”
输出:92

$2 题解

算法: 预处理前后缀信息 + 中心扩散法

首先将贡献拆分,分别计算每个 s[i] 对答案的贡献,也就是对每个元素 s[i],问:s[i] 在几个子串中属于唯一字符。

高效回答这一问的做法是预处理前后缀信息 + 中心扩散法。

首先预处理前后缀信息:

1
2
L[i] := [0..i-1] 中距离 s[i] 最近的相同字符的下标
R[i] := [i+1..n-1] 中距离 s[i] 最近的相同字符的下标

处理完成后,即可走中心扩散的流程,枚举每个 $i$ 作为中心,根据两侧最远能扩展到的位置,更新其对答案的贡献:

1
ans += (i - L[i]) * (R[i] - i)

代码 (C++)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public:
int uniqueLetterString(string s) {
int n = s.size();
vector<int> L(n, -1), R(n, n);
vector<int> mapping(26, -1);
for(int i = 0; i < n; ++i)
{
L[i] = mapping[s[i] - 'A'];
mapping[s[i] - 'A'] = i;
}
mapping.assign(26, n);
for(int i = n - 1; i >= 0; --i)
{
R[i] = mapping[s[i] - 'A'];
mapping[s[i] - 'A'] = i;
}
const int MOD = 1e9 + 7;
using ll = long long;
int ans = 0;
for(int i = 0; i < n; ++i)
{
ans = (ans + (ll)(i - L[i]) * (ll)(R[i] - i)) % MOD;
}
return ans;
}
};

Share