|
2 | 2 |
|
3 | 3 | 这是 LeetCode 上的 **[828. 统计子串中的唯一字符](https://leetcode.cn/problems/count-unique-characters-of-all-substrings-of-a-given-string/solution/by-ac_oier-922k/)** ,难度为 **困难**。
|
4 | 4 |
|
5 |
| -Tag : 「模拟」、「数学」 |
| 5 | +Tag : 「模拟」、「数学」、「线性 DP」 |
6 | 6 |
|
7 | 7 |
|
8 | 8 |
|
@@ -110,6 +110,59 @@ function uniqueLetterString(s: string): number {
|
110 | 110 |
|
111 | 111 | ---
|
112 | 112 |
|
| 113 | +### 线性 DP |
| 114 | + |
| 115 | +另外一个实现思路是利用「动态规划」思想。 |
| 116 | + |
| 117 | +定义 $f[i]$ 为考虑以 $s[i]$ 为结尾的所有子串中的唯一字符个数。 |
| 118 | + |
| 119 | +不失一般性考虑 $f[i]$ 该如何转移:以 $s[i]$ 为结尾的子串包括在所有以 $s[i - 1]$ 为结尾的子串结尾添加一个字符而来,以及 $s[i]$ 字符本身组成的新子串。 |
| 120 | + |
| 121 | +首先我们令 $f[i] = f[i - 1]$,同时使用 $b[x]$ 记录字符 $x$ 前一次出现的下标,使用 $a[x]$ 记录字符 $x$ 在上上次出现的下标,然后假设当前处理的字符为 $c = s[i]$,考虑 $s[i]$ 对 $f[i]$ 的影响(注意 $s[i]$ 始终为子串右端点): |
| 122 | + |
| 123 | +* 在子串左端点下标范围在 $[b[c] + 1, i]$ 的子串中,$s[i]$ 必然只出现一次(满足唯一字符要求),即可增加 $i - b[c]$ 个唯一字符 $s[i]$; |
| 124 | +* 在子串左端点下标范围在 $[a[c] + 1, b[c]]$ 的子串中,原本位于 $b[c]$ 的字符在新子串中出现次数变为 $2$ 次(不再满足唯一字符要求),即需减少 $b[c] - a[c]$ 个唯一字符 $s[i]$。 |
| 125 | + |
| 126 | +综上,我们有状态转移方程:$f[i] = f[i - 1] + (i - b[s[i]]) - (b[s[i]] - a[s[i]])$ |
| 127 | + |
| 128 | +实现上,由于 $f[i]$ 只依赖于 $f[i - 1]$,因此我们真的无须创建动规数组,而只需要使用单个变量 `cur` 来记录当前处理到的 $f[i]$ 即可,累积所有的 $f[i]$ 即是答案。 |
| 129 | + |
| 130 | +Java 代码: |
| 131 | +```Java |
| 132 | +class Solution { |
| 133 | + public int uniqueLetterString(String s) { |
| 134 | + int n = s.length(), ans = 0, cur = 0; |
| 135 | + int[] a = new int[26], b = new int[26]; |
| 136 | + Arrays.fill(a, -1); Arrays.fill(b, -1); |
| 137 | + for (int i = 0; i < n; i++) { |
| 138 | + int u = s.charAt(i) - 'A'; |
| 139 | + cur += i - b[u] - (b[u] - a[u]); |
| 140 | + ans += cur; |
| 141 | + a[u] = b[u]; b[u] = i; |
| 142 | + } |
| 143 | + return ans; |
| 144 | + } |
| 145 | +} |
| 146 | +``` |
| 147 | +TypeScript 代码: |
| 148 | +```TypeScript |
| 149 | +function uniqueLetterString(s: string): number { |
| 150 | + let n = s.length, ans = 0, cur = 0 |
| 151 | + const a = new Array<number>(26).fill(-1), b = new Array<number>(26).fill(-1) |
| 152 | + for (let i = 0; i < n; i++) { |
| 153 | + const u = s.charCodeAt(i) - 65 |
| 154 | + cur += i - b[u] - (b[u] - a[u]) |
| 155 | + ans += cur |
| 156 | + a[u] = b[u]; b[u] = i |
| 157 | + } |
| 158 | + return ans |
| 159 | +}; |
| 160 | +``` |
| 161 | +* 时间复杂度:$O(n)$ |
| 162 | +* 空间复杂度:$O(C)$,其中 $C = 26$ 为字符集大小 |
| 163 | + |
| 164 | +--- |
| 165 | + |
113 | 166 | ### 最后
|
114 | 167 |
|
115 | 168 | 这是我们「刷穿 LeetCode」系列文章的第 `No.828` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。
|
|
0 commit comments