A character is unique in string S
if it occurs exactly once in it.html
For example, in string S = "LETTER"
, the only unique characters are "L"
and "R"
.git
Let's define UNIQ(S)
as the number of unique characters in string S
.github
For example, UNIQ("LETTER") = 2
.数组
Given a string S
with only uppercases, calculate the sum of UNIQ(substring)
over all non-empty substrings of S
.ide
If there are two or more equal substrings at different positions in S
, we consider them different.code
Since the answer can be very large, return the answer modulo 10 ^ 9 + 7
.htm
Example 1:blog
Input: "ABC" Output: 10 Explanation: All possible substrings are: "A","B","C","AB","BC" and "ABC". Evey substring is composed with only unique letters. Sum of lengths of all substring is 1 + 1 + 1 + 2 + 2 + 3 = 10
Example 2:ci
Input: "ABA" Output: 8 Explanation: The same as example 1, except uni("ABA") = 1.
Note: 0 <= S.length <= 10000
.leetcode
这道题给了咱们一个字符串S,要统计其全部的子串中不一样字符的个数之和,这里的子串是容许重复的,并且说结果须要对一个超大数取余,这暗示了返回值可能会很大,这样的话对于纯暴力的解法,好比遍历全部可能的子串并统计不一样字符的个数的这种解法确定是不行的。这道题还真是一点没有辱没其 Hard 标签,确实是一道颇有难度的题,不太容易想出正确解法。还好有 李哥 lee215 的帖子,一个帖子的点赞数超过了整个第一页全部其余帖子的点赞数之和,简直是刷题界的 Faker,你李哥永远是你李哥。这里就按照李哥的帖子来说解吧,首先来看一个字符串 CACACCAC,若想让第二个A成为子串中的惟一,那么必需要知道其先后两个相邻的A的位置,好比 CA(CACC)AC,括号中的子串 CACC 中A就是惟一的存在,一样,对于 CAC(AC)CAC,括号中的子串 AC 中A也是惟一的存在。这样就能够观察出来,只要左括号的位置在第一个A和第二个A之间(共有2个位置),右括号在第二个A和第三个A之间(共有3个位置),这样第二个A在6个子串中成为那个惟一的存在。换个角度来讲,只有6个子串可让第二个A做为单独的存在从而在结果中贡献。这是个很关键的转换思路,与其关注每一个子串中的单独字符个数,不如换个角度,对于每一个字符,统计其能够在多少个子串中成为单独的存在,一样能够获得正确的结果。这样的话,每一个字母出现的位置就很重要了,因为上面的分析说了,只要知道三个位置,就能够求出中间的字母的贡献值,为了节省空间,只保留每一个字母最近两次的出现位置,这样加上当前位置i,就能够知道前一个字母的贡献值了。这里使用一个长度为 26x2 的二维数组 idx,由于题目中限定了只有26个大写字母。这里只保留每一个字母的前两个出现位置,均初始化为 -1。而后遍历S中每一个字母,对于每一个字符减去A,就是其对应位置,此时将前一个字母的贡献值累加到结果 res 中,假如当前字母是首次出现,也不用担忧,前两个字母的出现位置都是 -1,相减后为0,因此累加值仍是0。而后再更新 idx 数组的值。因为每次都是计算该字母前一个位置的贡献值,因此最后还须要一个 for 循环去计算每一个字母最后出现位置的贡献值,此时因为身后没有该字母了,就用位置N来代替便可,参见代码以下:
解法一:
class Solution { public: int uniqueLetterString(string S) { int res = 0, n = S.size(), M = 1e9 + 7; vector<vector<int>> idx(26, vector<int>(2, -1)); for (int i = 0; i < n; ++i) { int c = S[i] - 'A'; res = (res + (i - idx[c][1]) * (idx[c][1] - idx[c][0]) % M) % M; idx[c][0] = idx[c][1]; idx[c][1] = i; } for (int c = 0; c < 26; ++c) { res = (res + (n - idx[c][1]) * (idx[c][1] - idx[c][0]) % M) % M; } return res; } };
咱们也能够换一种解法,使得其更加简洁一些,思路稍微有些不一样,这里参考了 大神 meng789987 的帖子。使用的是动态规划 Dynmaic Programming 的思想,用一个一维数组 dp,其中 dp[i] 表示以 S[i] 为结尾的全部子串中的单独字母个数之和,这样只要把 [0, n-1] 范围内全部的 dp[i] 累加起来就是最终的结果了。更新 dp[i] 的方法关键也是要看重复的位置,好比当前是 AB 的话,此时 dp[1]=3,由于以B结尾的子串是 B 和 AB,共有3个单独字母。若此时再后面加上个C的话,因为没有重复出现,则以C结尾的子串 C,BC,ABC 共有6个单独字母,即 dp[2]=6,怎么由 dp[1] 获得呢?首先新加的字母自己就是子串,因此必定是能够贡献1的,而后因为以前都没有C出现,则以前的每一个子串中C均可以贡献1,而本来的A和B的贡献值也将保留,因此总共就是 dp[2] = 1+dp[1]+2 = 6。但若新加的字母是A的话,就比较 tricky 了,首先A自己也是子串,有稳定的贡献1,因为以前已经有A的出现了,因此只要知道了以前A的位置,那么中间部分是没有A的,即子串 B 中没有A,A能够贡献1,可是对于以前的有A的子串,好比 AB,此时新加的A不但不能贡献,反而还会伤害以前A的贡献值,即变成 ABA 了后,不但第二个A不能贡献,连第一个A以前的贡献值也要减去,此时 dp[2] = 1+dp[1]+(2-1)-(1-0) = 4。其中2是当前A的位置,1是前一个A的位置加1,0是再前一个A的位置加1。讲到这里应该就比较清楚了吧,这里仍是要知道每一个字符的前两次出现的位置,这里用两个数组 first 和 second,不过须要注意的是,这里保存的是位置加1。又由于每一个 dp 值只跟其前一个 dp 值有关,因此为了节省空间,并不须要一个 dp 数组,而是只用一个变量 cur 进行累加便可,记得每次循环都要把 cur 存入结果 res 中。那么每次 cur 的更新方法就是前一个 cur 值加上1,再加上当前字母产生的贡献值,减去当前字母抵消的贡献值,参见代码以下:
解法二:
class Solution { public: int uniqueLetterString(string S) { int res = 0, n = S.size(), cur = 0, M = 1e9 + 7; vector<int> first(26), second(26); for (int i = 0; i < n; ++i) { int c = S[i] - 'A'; cur = cur + 1 + i - first[c] * 2 + second[c]; res = (res + cur) % M; second[c] = first[c]; first[c] = i + 1; } return res; } };
Github 同步地址:
https://github.com/grandyang/leetcode/issues/828
参考资料:
https://leetcode.com/problems/unique-letter-string/
https://leetcode.com/problems/unique-letter-string/discuss/158378/Concise-DP-O(n)-solution
https://leetcode.com/problems/unique-letter-string/discuss/128952/One-pass-O(N)-Straight-Forward