单词数组 words 的 有效编码 由任意助记字符串 s 和下标数组 indices 组成,且满足:
words.length == indices.length
助记字符串 s 以 ‘#’ 字符结尾
对于每个下标 indices[i] ,s 的一个从 indices[i] 开始、到下一个 ‘#’ 字符结束(但不包括 ‘#’)的 子字符串 恰好与 words[i] 相等
给定一个单词数组 words ,返回成功对 words 进行编码的最小助记字符串 s 的长度 。
示例 1:
输入:words = ["time", "me", "bell"]
输出:10
解释:一组有效编码为 s = "time#bell#" 和 indices = [0, 2, 5] 。
words[0] = "time" ,s 开始于 indices[0] = 0 到下一个 '#' 结束的子字符串,如加粗部分所示 "time#bell#"
words[1] = "me" ,s 开始于 indices[1] = 2 到下一个 '#' 结束的子字符串,如加粗部分所示 "time#bell#"
words[2] = "bell" ,s 开始于 indices[2] = 5 到下一个 '#' 结束的子字符串,如加粗部分所示 "time#bell#"
示例 2:
输入:words = ["t"]
输出:2
解释:一组有效编码为 s = "t#" 和 indices = [0] 。
提示:
1 <= words.length <= 2000
1 <= words[i].length <= 7
words[i] 仅由小写字母组成
思路1 - 字典树
如果某个单词是其它单词的后缀,则不计算该单词的长度。
注意:前缀需要计算。因为从一个位置pos到结束符’#'标识一个单词,例如any和anytime不能写成anytime#
和[0,3]
,这标识的是anytime
和time
两个词,因此只能忽略后缀。
使用字典树的方法就是:
- 将所有单词翻转,并按长度由大到小排序
- 构造字典树,结果为字典树中每一条独立路径的长度+独立路径数(
#
的数量)
例如:
time、me、bell、anytime、anything、any
将这些单词翻转并按长度排序后:
gnihtyna、emityna、emit、lleb、yna、em
如果某些单词是其它单词的后缀,则翻转后的单词是其它单词的前缀,这样构造字典树,字典树中的每一条独立路径就能组成一条有效编码。
构造字典树如下:
有效编码:助记字符串anything#anytime#bell#any#
,下标数组[0, 9, 12, 14, 17, 22]
AC代码
const int TRIE_NODE_SIZE = 26;
// 字典树节点
struct TrieNode {
TrieNode* next[TRIE_NODE_SIZE];
bool isEnd;
int len; // 单词长度
TrieNode() {
for (int i = 0; i < TRIE_NODE_SIZE; ++i) {
next[i] = nullptr;
}
isEnd = false;
len = 0;
}
};
// 字典树
class Trie {
public:
// 构造根节点
Trie() {
root = new TrieNode();
sum = 0;
num = 0;
}
// 字典树中插入插入word
void insert(string word) {
TrieNode* node = root;
bool flag = false;
for (const auto& ch : word) {
if (node->next[ch - 'a'] == nullptr) {
// 创建节点
node->next[ch - 'a'] = new TrieNode();
flag = true;
}
node->next[ch - 'a']->len = node->len + 1;
node = node->next[ch - 'a'];
}
// 一次节点都没有创建,则该单词为之前某单词的前缀
if (!flag) {
return;
}
node->isEnd = true;
sum += node->len;
num++;
}
public:
TrieNode* root; // 根节点
int sum; // 叶子节点所代表单词的长度之和
int num; // 叶子节点数
};
static bool cmp(const string& a, const string& b) {
return a.length() > b.length();
}
class Solution {
public:
int minimumLengthEncoding(vector<string>& words) {
int result = 0;
vector<string> tmpWords(words);
// 将所有单词由大到小排序
sort(tmpWords.begin(), tmpWords.end(), cmp);
// 将单词翻转构建字典树
Trie* trie = new Trie();
for (auto& word : tmpWords) {
reverse(word.begin(), word.end());
trie->insert(word);
}
return trie->sum + trie->num;
}
};
思路2 - 常规法
判断后缀的常规法
超时,通过样例27/36
bool cmp(const string& a, const string& b)
{
return a.length() < b.length();
}
class Solution {
public:
// 判断str是否是s的前缀
bool isPrefix(string s, string str)
{
if (str.length() > s.length()) {
return false;
}
for (int i = 0; i < str.size(); ++i) {
if (str[i] != s[i]) {
return false;
}
}
return true;
}
// 判断str是否是s的后缀
bool isSuffix(string s, string str)
{
reverse(s.begin(), s.end());
reverse(str.begin(), str.end());
return isPrefix(s, str);
}
int minimumLengthEncoding(vector<string>& words) {
int result = 0;
sort(words.begin(), words.end(), cmp);
for (int i = 0; i < words.size(); ++i) {
bool isSuffixFlag = false;
for (int j = i + 1; j < words.size(); ++j) {
// 判断i是否是j的后缀
if (isSuffix(words[j], words[i])) {
isSuffixFlag = true;
break;
}
}
if (!isSuffixFlag) {
result += (words[i].length() + 1);
}
}
return result;
}
};