Given a list of words
, list of single letters
(might be repeating) and score
of every character.
Return the maximum score of any valid set of words formed by using the given letters (words[i]
cannot be used two or more times).
It is not necessary to use all characters in letters
and each letter can only be used once. Score of letters 'a'
, 'b'
, 'c'
, ... ,'z'
is given by score[0]
, score[1]
, ... , score[25]
respectively.
Example 1:
Input: words = ["dog","cat","dad","good"], letters = ["a","a","c","d","d","d","g","o","o"], score = [1,0,9,5,0,0,3,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0] Output: 23 Explanation: Score a=1, c=9, d=5, g=3, o=2 Given letters, we can form the words "dad" (5+1+5) and "good" (3+2+2+5) with a score of 23. Words "dad" and "dog" only get a score of 21.
Example 2:
Input: words = ["xxxz","ax","bx","cx"], letters = ["z","a","b","c","x","x","x"], score = [4,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,10] Output: 27 Explanation: Score a=4, b=4, c=4, x=5, z=10 Given letters, we can form the words "ax" (4+5), "bx" (4+5) and "cx" (4+5) with a score of 27. Word "xxxz" only get a score of 25.
Example 3:
Input: words = ["leetcode"], letters = ["l","e","t","c","o","d"], score = [0,0,1,1,1,0,0,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,0,0,0,0] Output: 0 Explanation: Letter "e" can only be used once.
Constraints:
1 <= words.length <= 14
1 <= words[i].length <= 15
1 <= letters.length <= 100
letters[i].length == 1
score.length == 26
0 <= score[i] <= 10
words[i]
, letters[i]
contains only lower case English letters.这道题说是给了一个单词数组 words,还有个字母数组 letters,以及每个字母的分数数组 score,现在让用 letters 中的字母去组成 words 中的单词,每个字母只能用一次,不必使用所有的字母,问可以得到的最大分数是多少,每个拼出的单词的分数是其所有的字母的分数之和。题目中限定了只有小写字母,即 26 个,所以 score 数组的大小也是 26 个,可以直接根据字母取其分数值。对于给定的字母数组 letters,里面可能会存在大量的重复字母,为了便于使用,需要统计每个字母的个数,可以用一个大小为 26 的 cnt 数组来统计。由于不知道给定的字母能拼出多少个单词,但最终能拼出的单词集一定是给定单词集的子集,需要注意的是也不是拼出的单词越多越好,而是需要最终的得分最大,所以只能尽可能的去计算每一种情况,从而找到得分最大的情况。
跟之前那道 Subsets 有点类似,不过更加复杂一些。这里使用回溯 Backtracking 的方法来做,递归函数需要4个参数,原单词数组 words,统计字母个数数组 cnts,字母分数数组 score,还有当前遍历的位置 idx。从 idx 的位置开始往后遍历,对于当前遍历到的单词,遍历其每一个字母,并且将 cnt 对应的字母个数减1,当小于0了的话,说明此时无法组成当前单词,标记 isValid 为 false,同时将当前单词的总得分存入变量 sum 中。处理完当前单词后,若 isValid 为 true,说明字母是够用的,则对下一个位置的单词调用递归,将返回值加到 sum 中,这样 sum 就是合法的情况,用其来更新结果 res。之后要记得恢复状态,将当前单词的字母统计个数加回来,参见代码如下:
解法一:
class Solution { public: int maxScoreWords(vector<string>& words, vector<char>& letters, vector<int>& score) { vector<int> cnt(26); for (char c : letters) ++cnt[c - 'a']; return dfs(words, cnt, score, 0); } int dfs(vector<string>& words, vector<int>& cnt, vector<int>& score, int idx) { int res = 0; for (int i = idx; i < words.size(); ++i) { int sum = 0; bool isValid = true; for (char c : words[i]) { if (--cnt[c - 'a'] < 0) { isValid = false; } sum += score[c - 'a']; } if (isValid) { sum += dfs(words, cnt, score, i + 1); res = max(res, sum); } for (char c : words[i]) { ++cnt[c - 'a']; } } return res; } };
我们还可以写的更简洁一些,在递归函数中少用一个 for 循环,开始直接判断当前 idx 是否大于等于 words 的长度,是的话直接返回0。否则先跳过当单词,直接对下个位置调用递归,得到返回值 skipGain,然后再来计算当前的得分 gain。这里为了避免回溯的恢复状态步骤,直接新建了一个字母统计个数数组 cnt1,然后就是遍历当前单词的字母,减少对应字母的个数,若小于0了,isValid 标记为 false,将字母分数加到 gain 中。若最终 isValid 为 true,则分数为 gain 加上对下一个位置调用递归的返回的值(注意这次调用和开头得到的 skipGain 是不同,因为传入的 cnt 数组不同,这次调用已经减去当前单词的字母个数),若 isValid 为 false,则分数为0。最终返回的分数还要跟 skipGain 相比,取较大值返回即可,参见代码如下:
解法二:
class Solution { public: int maxScoreWords(vector<string>& words, vector<char>& letters, vector<int>& score) { vector<int> cnt(26); for (char c : letters) ++cnt[c - 'a']; return dfs(words, cnt, score, 0); } int dfs(vector<string>& words, vector<int>& cnt, vector<int>& score, int idx) { if (idx >= words.size()) return 0; int skipGain = dfs(words, cnt, score, idx + 1), gain = 0; bool isValid = true; vector<int> cnt1 = cnt; for (char c : words[idx]) { if (--cnt1[c - 'a'] < 0) isValid = false; gain += score[c - 'a']; } return max(skipGain, isValid ? gain + dfs(words, cnt1, score, idx + 1) : 0); } };
最后来看一种迭代的解法,这里是遍历 words 的所有的子集,用了 bitmask 的方法,若 words 里有n个单词,那么其子集的个数是 2^n
,对应的正好是从 0 到 2^n-1
的二进制表示,0位表示不用对应位单词,1表示选用当前单词。比如 words 是 ["a", "b", "c"] 的话,那么 101 就表示子集是 ["a", "c"],这样就可以遍历所有的子集了。开始还是先统计 letters 中的字母个数,放入到 count 中,然后 mask 从0遍历到 2^n-1
,对于每个子集,复制一个 count 数组,然后遍历n个位置,从0遍历到 n-1,或者从 n-1 遍历到0都可以,为的是找到二进制数对应的1位,通过 (mask >> i) & 1
来判断,遍历需要拼的单词的字母,减去对应的字母个数,若小于0了,标记 isValid,并 break 掉。每遍历完子集中的一个单词,都检查一下 isValid,若为0就 break 掉循环,最终该子集中的单词遍历完了之后,若 isValid 为1,用 sum 来更新结果 res 即可,参见代码如下:
解法三:
class Solution { public: int maxScoreWords(vector<string>& words, vector<char>& letters, vector<int>& score) { int res = 0, n = words.size(), m = 1 << n; vector<int> count(26); for (char c : letters) ++count[c - 'a']; for (int mask = 0; mask < m; ++mask) { int sum = 0, isValid = 1; vector<int> cnt = count; for (int i = n - 1; i >= 0; --i) { if ((mask >> i) & 1) { for (char c : words[i]) { if (--cnt[c - 'a'] < 0) { isValid = 0; break; } sum += score[c - 'a']; } } if (!isValid) break; } if (isValid) res = max(res, sum); } return res; } };
Github 同步地址:
https://github.com/grandyang/leetcode/issues/1255
类似题目:
Subsets
参考资料:
https://leetcode.com/problems/maximum-score-words-formed-by-letters/
https://leetcode.com/problems/maximum-score-words-formed-by-letters/discuss/426045/C%2B%2B-DFS-(optional-memo)
https://leetcode.com/problems/maximum-score-words-formed-by-letters/discuss/505887/C%2B%2B-Memory-efficient-simple-bitmasking-solution
https://leetcode.com/problems/maximum-score-words-formed-by-letters/discuss/425129/java-backtrack-similar-to-78.-Subsets-1ms-beats-100
LeetCode All in One 题目讲解汇总(持续更新中...)