# LeetCode 2416. Sum of Prefix Scores of Strings https://leetcode.com/problems/sum-of-prefix-scores-of-strings/description/ ## 題目大意 給定一個大小為 `n` 的字串陣列 `words`,其中每個字串都是非空的 我們定義字串 `term` 的分數為滿足 `term` 是 `words[i]` 的前綴字串的字串數量 例如,若 `words = ["a", "ab", "abc", "cab"]`,則 `"ab"` 的分數是 2,因為 `"ab"` 是 `"ab"` 和 `"abc"` 的前綴字串 回傳一個大小為 `n` 的陣列 `answer`,其中 `answer[i]` 是 `words[i]` 的每個非空前綴字串的分數總和 注意: 1. 字串被認為是它自己的前綴字串 2. 字串只會有小寫字母 ## 思考 ### Algo 1 這題看到 prefix of string 我們多少應該會想到 Trie 沒錯,我們可以使用 Trie 的概念來解這題 不過我們沒有需要**查找一個詞**,我們只需要累計出他的分數就好 ```cpp! class TrieNode { public: array<TrieNode *, 26> children; int cnt; TrieNode() : cnt(0) { children.fill(nullptr); } }; class Trie { public: Trie() : root(new TrieNode()) {} void insert(const string &word) { TrieNode *node = root; for (char c : word) { const int i = c - 'a'; if (!node->children[i]) node->children[i] = new TrieNode(); node = node->children[i]; ++node->cnt; } } int getScore(const string &word) { TrieNode *node = root; int score = 0; for (char c : word) { const int i = c - 'a'; if (!node->children[i]) break; node = node->children[i]; score += node->cnt; } return score; } private: TrieNode *root; }; class Solution { public: vector<int> sumPrefixScores(vector<string> &words) { Trie trie; for (const auto &word : words) { trie.insert(word); } vector<int> ans; ans.reserve(words.size()); for (const auto &word : words) { ans.emplace_back(trie.getScore(word)); } return ans; } }; ``` ### Algo 2 使用 Trie 能更便利我們聯想到解法 不過我們其實還能從中再得到一些啟發 Trie 做的其實可以理解成基於前綴的分類 那其實,我們依據字典順序做排序,不也是能達到分類嗎? 例如 `words = ["abc","ab","bc","b"]` 經過排序後應該會是 `sorted_words = ["ab", "abc", "b", "bc"]` 你會發現前綴有 `"ab"` 的字串會被排在一起 這就衍伸出第二種演算法 我們使用 DP 的方式來解題 假設有兩個字串 $s_{i-1}$ 跟 $s_i$ 他們在字典順序上是相鄰的 令 $dp[i,j]$ 表示到第 $j$ 個字母為止 $s_{i-1}$ 跟 $s_i$ 的共同前綴長 則我們可以得到: $$ dp[i,j] = \begin{cases} 0 &\text{if } s_{i-1}[j] \neq s_i[j] \\ dp[i-1,j] + 1 & \text{if } s_{i-1}[j] = s_i[j] \end{cases} $$ 但這樣就算把所有 `j` 的情況都掃過一輪,我們仍未解決這個題目,需要反轉順序後再執行一遍上述檢查 簡單理解就是目前我們只有由前往後看,還需要由後往前看回來,得到的 `ans[i]` 才會是雙向互看的結果 否則會發現有些情況會漏算 最後不要忘了,**字串被認為是它自己的前綴字串** 😉 ```cpp! class Solution { public: vector<int> sumPrefixScores(vector<string> &words) { const int n = words.size(); vector<int> ans(n); vector<pair<string, int>> wordPairs; for (int i = 0; i < n; ++i) { wordPairs.emplace_back(words[i], i); } sort(wordPairs.begin(), wordPairs.end()); calculateScores(wordPairs, ans, n); reverse(wordPairs.begin(), wordPairs.end()); calculateScores(wordPairs, ans, n); for (const auto &wp : wordPairs) { ans[wp.second] += wp.first.size(); } return ans; } private: void calculateScores(const vector<pair<string, int>> &wordPairs, vector<int> &ans, int n) { vector<int> dp(wordPairs.begin()->first.size()); for (int i = 1; i < n; ++i) { const string &prev = wordPairs[i - 1].first; const string &curr = wordPairs[i].first; int idx = wordPairs[i].second; vector<int> ndp(curr.size()); const int m = min(prev.size(), curr.size()); for (int j = 0; j < m; ++j) { if (curr[j] != prev[j]) break; ndp[j] = dp[j] + 1; ans[idx] += ndp[j]; } dp = move(ndp); } } }; ``` algo 2 本質上還是跟 algo 1 做類似的事情,卻換來了是時間空間上的節省 雖然比較不好懂,但以執行結果來說,algo 2 的成績明顯好很多 algo 1 的缺點很明顯,我們的資料結構不但會花更多的 Memory ,而且我們需要花時間建立,之後才能再遍歷一次字串算成績 這樣的做法屬於依賴資料結構的特性 ![image](https://hackmd.io/_uploads/r1iZ5pW00.png)