Given a list of words, we may encode it by writing a reference string S
and a list of indexes A
.
For example, if the list of words is ["time", "me", "bell"]
, we can write it as S = "time#bell#"
and indexes = [0, 2, 5]
.
Then for each index, we will recover the word by reading from the reference string from that index until we reach a "#"
character.
What is the length of the shortest reference string S possible that encodes the given words?
Example:
Input: words =["time", "me", "bell"]
Output: 10 Explanation: S ="time#bell#" and indexes = [0, 2, 5
].
Note:
1 <= words.length <= 2000
.1 <= words[i].length <= 7
.- Each word has only lowercase letters.
這道題給了我們一個單詞陣列,讓我們對其編碼,不同的單詞之間加入#號,每個單詞的起點放在一個座標陣列內,終點就是#號,能合併的單詞要進行合併,問輸入字串的最短長度。題意不難理解,難點在於如何合併單詞,我們觀察題目的那個例子,me和time是能夠合併的,只要標清楚其實位置,time的起始位置是0,me的起始位置是2,那麼根據#號位置的不同就可以順利的取出me和time。需要注意的是,如果me換成im,或者tim的話,就不能合併了,因為我們是要從起始位置到#號之前所有的字元都要取出來。搞清楚了這一點之後,我們在接著觀察,由於me是包含在time中的,所以我們處理的順序應該是先有time#,然後再看能否包含me,而不是先生成了me#之後再處理time,所以我們可以得出結論,應該先處理長單詞,那麼就給單詞陣列按長度排序一下就行,自己重寫一個comparator就行。然後我們遍歷陣列,對於每個單詞,我們都在編碼字串查詢一下,如果沒有的話,直接加上這個單詞,再加一個#號,如果有的話,就可以得到出現的位置。比如在time#中查詢me,得到found=2,然後我們要驗證該單詞後面是否緊跟著一個#號,所以我們直接訪問found+word.size()這個位置,如果不是#號,說明不能合併,我們還是要加上這個單詞和#號。最後返回編碼字串的長度即可,參見程式碼如下:
解法一:
class Solution { public: int minimumLengthEncoding(vector<string>& words) { string str = ""; sort(words.begin(), words.end(), [](string& a, string& b){return a.size() > b.size();}); for (string word : words) { int found = str.find(word); if (found == string::npos || str[found + word.size()] != '#') { str += word + "#"; } } return str.size(); } };
我們再來看一種不用自定義comparator的方法,根據之前的分析,我們知道其實是在找單詞的字尾,比如me就是time的字尾。我們希望將能合併的單詞排在一起,比較好處理,而字尾又不好排序。那麼我們就將其轉為字首,做法就是給每個單詞翻轉一下,time變成emit,me變成em,這樣我們只要用預設的字母順序排,就可以得到em,emit的順序,那麼能合併的單詞就放到一起了,而且一定是當前的合併到後面一個,那麼就好做很多了。我們只要判讀當前單詞是否是緊跟著的單詞的字首,是的話就加0,不是的話就要加上當前單詞的長度並再加1,多加的1是#號。判斷字首的方法很簡單,直接在後面的單詞中取相同長度的字首比較就行了。由於我們每次都要取下一個單詞,為了防止越界,只處理到倒數第二個單詞,那麼就要把最後一個單詞的長度加入結果res,並再加1即可,參見程式碼如下:
解法二:
class Solution { public: int minimumLengthEncoding(vector<string>& words) { int res = 0, n = words.size(); for (int i = 0; i < n; ++i) reverse(words[i].begin(), words[i].end()); sort(words.begin(), words.end()); for (int i = 0; i < n - 1; ++i) { res += (words[i] == words[i + 1].substr(0, words[i].size())) ? 0 : words[i].size() + 1; } return res + words.back().size() + 1; } };
接下來的這種方法也很巧妙,用了一個HashSet,將所有的單詞先放到這個HashSet中。原理是對於每個單詞,我們遍歷其所有的字尾,比如time,那麼就遍歷ime,me,e,然後看HashSet中是否存在這些字尾,有的話就刪掉,那麼HashSet中的me就會被刪掉,這樣保證了留下來的單詞不可能再合併了,最後再加上每個單詞的長度到結果res,並且同時要加上#號的長度,參見程式碼如下:
解法三:
class Solution { public: int minimumLengthEncoding(vector<string>& words) { int res = 0; unordered_set<string> st(words.begin(), words.end()); for (string word : st) { for (int i = 1; i < word.size(); ++i) { st.erase(word.substr(i)); } } for (string word : st) res += word.size() + 1; return res; } };
參考資料:
https://leetcode.com/problems/short-encoding-of-words/