[LeetCode]828. Count Unique Characters of All Substrings of a Given String 动态规划转移方程详解
作者:互联网
题目描述
LeetCode原题链接:828. Count Unique Characters of All Substrings of a Given String
Let's define a function countUniqueChars(s)
that returns the number of unique characters on s
.
- For example, calling
countUniqueChars(s)
ifs = "LEETCODE"
then"L"
,"T"
,"C"
,"O"
,"D"
are the unique characters since they appear only once ins
, thereforecountUniqueChars(s) = 5
.
Given a string s
, return the sum of countUniqueChars(t)
where t
is a substring of s
.
Notice that some substrings can be repeated so in this case you have to count the repeated ones too.
Example 1:
Input: s = "ABC" Output: 10 Explanation: All possible substrings are: "A","B","C","AB","BC" and "ABC". Every substring is composed with only unique letters. Sum of lengths of all substring is 1 + 1 + 1 + 2 + 2 + 3 = 10
Example 2:
Input: s = "ABA"
Output: 8
Explanation: The same as example 1, except countUniqueChars
("ABA") = 1.
Example 3:
Input: s = "LEETCODE" Output: 92
Constraints:
1 <= s.length <= 105
s
consists of uppercase English letters only.
思路分析
关于子字符串的动态规划题目一般dp[i]都表示以s[i]结尾的子字符串的某最小值/最大值,这一题也不例外。我们用dp[i]表示以字符s[i]结尾的所有子字符串的unique characters之和。那么最终结果就是dp数组每一项累加的结果。
那么如何寻找状态转移方程呢?每当遍历到一个新的位置时,会有两种状况:1)当前位置的子母之前没有出现过;2)当前位置的子母之前出现过。让我们分别来看。
情况一,如果当前位置的子母之前没有出现过,比如对于字符串ABC,当i=1时,以B结尾的子字符串有B, AB,那么dp[1] = 1 + 2 = 3;当i=2时,因为C之前没有出现过,所以对上一位置的每个子字符串 (B, AB) 的unique characters个数可以分别贡献1 (BC, ABC),再加上C本身就是一个子串,所以dp[2] = 1 + dp[1] + 2 = 6。
情况二,如果当前位置的子母之前出现过,比如对于字符串EABDCA,当i=4时,子串EABDC的unique characters之和为15;当i=5时,会发现A在之前已经出现过了(i=1位置)。那么在上一个A出现之后的位置,即(1, 4]范围内的所有子串是不包含A的,对于这些子串,和情况一相同,新增的A仍然可以为unique characters贡献1;而对于[包含了一个A的那些子串,比如EABDC,新增一个A会另之前出现的A不能再被计入unique charaters,EABDC的unique characters数为5,新增一个A后(EABDCA)unique characters数反而变成了4,A在这里没有起到贡献作用反而抵消了1;不过,如果新增的A作为单独子串看待的话,还是可以贡献1的。于是,dp[5] = 1 + dp[4] + (5 - 2) - (2 - 0) = 17。后面两个括号内的内容分别是新增i=5位置的A贡献的unique charaters数(等于i=5位置之前不含第一个A的子串个数)和抵消的unique charaters数(等于i=5位置之前包含了第一个A的子串个数),两者都是通过位置下标计算得到的;贡献的个数是当前位置减去上一个A出现位置(i = 1)的后一位置,抵消的个数是上一个A出现位置后一位减去上上一个A出现位置后一位(例子中因为之前只出现过一个A,所以上上一个A出现的位置可以看作0)。补充一下,因为子串一定是连续的,所以对于[0,i]范围内以s[i]为结尾的长度大于1的子串(即不包含s[i]自身单独形成的子串)个数就等于下标位置i。
可见,在遍历过程中我们还需要记录每个字母前两次出现的位置。题目中限制了子母范围是26个大写子母,为了提高效率和方便计算,我们可以用两个长度为26的数组pre[26]和prePre[26]分别记录某一子母上一次出现位置的后一位置和上上一次出现位置的后一位置。
综合前面的分析,我们可以得出状态转移方程为:
如果还是不大好理解,我们来看一个例子,对于字符串GACEABDCA:
到这里应该就很清晰了吧,如果还是困惑,可以自己举几个不同的例子手动演算一下。
代码示例(JS)
1 var uniqueLetterString = function(s) { 2 let pre = new Array(26), prePre = new Array(26); 3 pre.fill(0); 4 prePre.fill(0); 5 let res = 0, cur = 0; 6 for(let i = 0; i < s.length; i++) { 7 let index = s[i].charCodeAt(0) - 'A'.charCodeAt(0); 8 cur = cur + 1 + (i - pre[index]) - (pre[index] - prePre[index]); 9 res += cur; 10 prePre[index] = pre[index]; 11 pre[index] = i + 1; 12 } 13 return res; 14 };
标签:Count,子串,Given,String,index,位置,characters,unique,dp 来源: https://www.cnblogs.com/barryyeee/p/16339850.html