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