[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 if `s = "LEETCODE"` then `"L"`, `"T"`,`"C"`,`"O"`,`"D"` are the unique characters since they appear only once in `s`, therefore `countUniqueChars(s) = 5`.

On this problem given a string s we need to return the sum of countUniqueChars(t) where t is a substring of s. Notice that some substrings can be repeated so on this case you have to count the repeated ones too.

Since the answer can be very large, return the answer modulo 10 ^ 9 + 7.

Example 1:

Input: s = "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:

Input: s = "ABA"
Output: 8
Explanation: The same as example 1, except `countUniqueChars`("ABA") = 1.

Example 3:

Input: s = "LEETCODE"
Output: 92

Constraints:

  • 0 <= s.length <= 10^4
  • s contain upper-case English letters only.

這道題給了我們一個字符串S,要統計其所有的子串中不同字符的個數之和,這里的子串是允許重復的,而且說結果需要對一個超大數取余,這暗示了返回值可能會很大,這樣的話對於純暴力的解法,比如遍歷所有可能的子串並統計不同字符的個數的這種解法肯定是不行的。這道題還真是一點沒有辱沒其 Hard 標簽,確實是一道很有難度的題,不太容易想出正確解法。還好有 [李哥 lee215 的帖子](https://leetcode.com/problems/unique-letter-string/discuss/128952/One-pass-O(N)-Straight-Forward),一個帖子的點贊數超過了整個第一頁所有其他帖子的點贊數之和,簡直是刷題界的 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 的帖子](https://leetcode.com/problems/unique-letter-string/discuss/158378/Concise-DP-O(n)-solution)。使用的是動態規划 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/count-unique-characters-of-all-substrings-of-a-given-string/

https://leetcode.com/problems/count-unique-characters-of-all-substrings-of-a-given-string/discuss/158378/Concise-DP-O(n)-solution

https://leetcode.com/problems/count-unique-characters-of-all-substrings-of-a-given-string/discuss/128952/C%2B%2BJavaPython-One-pass-O(N)

https://leetcode.com/problems/count-unique-characters-of-all-substrings-of-a-given-string/discuss/129021/O(N)-Java-Solution-DP-Clear-and-easy-to-Understand


[LeetCode All in One 題目講解匯總(持續更新中...)](https://www.cnblogs.com/grandyang/p/4606334.html)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM