这里是题目描述:LeetCode-820.单词的压缩编码
根据题干我们可以看出,如果一个单词时另一个单词的后缀子串,则将它并入另一个单词中,最后编码时不计入它的长度,最短的编码长度是所有不是其他单词后缀的单词的长度和个数的总和
那么,如何较高效的判断哪些单词是后缀,哪些不是呢?
方法一:字典树+DFS
本题中我们需要判断输入单词的后缀情况,我们可以将所有单词反转,问题就变成了判断单词的前缀情况:"me"
是"time"
的后缀,将它们反转后得到"em"
和"emit"
,前者是后者的前缀
接下来,将反转后的单词存入字典树(Trie树),它具有特性:所有具有相同前缀的单词会被存入从root节点出发的相同的路径中。例如,有单词"time"
、"me"
、"el"
、"bell"
,将它们的反转结果存入Trie树,如下图:
通过Trie的存储,我们可以看到所有是其他单词后缀的单词都并入了其他单词的路径中,每条从root出发到达叶子节点的路径就是一个不是其他单词前缀的单词。接下来,通过DFS来遍历Trie树,计算所有叶子节点的深度以及叶子节点个数的总和,就是我们要求的最短压缩编码长度
字典树+DFS 题解代码:
import java.util.HashMap;
import java.util.Map.Entry;
public class Solution {
public static void main(String[] args) {
String[] words={"time", "me", "bell"};
Solution obj=new Solution();
System.out.println(obj.minimumLengthEncoding(words));
}
public int minimumLengthEncoding(String[] words) {
if(words.length==0)
{
return 0;
}
if(words.length==1)
{
return words[0].length()+1;
}
Trie root=new Trie('.'); //Trie树的root节点
for(int i=0;i<words.length;i++)
{
root.insert(words[i],words[i].length()-1); //将单词插入Trie树中
}
return dfSearchTrie(root,0,0);
}
int dfSearchTrie(Trie node,int deep,int encodeLen) //深度优先搜索,统计所有叶子节点的深度以及单词间分隔符的和,即压缩编码长度
{
if(node.child.isEmpty()) //孩子为空,是叶子节点
{
encodeLen+=(deep+1);
}
else
{
for(Entry<Character,Trie> e:node.child.entrySet())
{
encodeLen=dfSearchTrie(e.getValue(),deep+1,encodeLen);
}
}
return encodeLen;
}
}
class Trie //字典树
{
char ch;
HashMap<Character,Trie> child;
Trie(char ch)
{
this.ch=ch;
this.child=new HashMap<>();
}
void insert(String word,int index) //向Trie树中插入单词
{
char curCh=word.charAt(index);
if(!this.child.containsKey(curCh))
{
this.child.put(curCh,new Trie(curCh));
}
Trie nextNode=this.child.get(curCh);
if(index>0)
{
nextNode.insert(word,index-1);
}
}
}
设单词个数是n,单词平均长度是m
时间复杂度:O(nm)
空间复杂度:O(nm)
方法二:反转+排序
继续利用方法一中的“反转单词,后缀问题变前缀问题“的思想,但是这一次不需要使用Trie树
在用java语言解题时,我们可以使用数组的排序库函数:Arrays.sort(String [])
。排序结果如下图:
我们可以发现规律:若单词t是单词s的后缀,那么排序后单词t一定在s的前一位。因此,我们可以计算最短压缩编码长度:遍历排序后的数组,若当前遍历的单词是后一个单词的后缀,则不将它计入编码长度;否则,当前单词不是任何一个单词的后缀,将它的长度+1计入编码长度
反转+排序题解代码:
import java.util.Arrays;
public class Solution {
public static void main(String[] args) {
String[] words={"time", "tme", "bell"};
Solution obj=new Solution();
System.out.println(obj.minimumLengthEncoding(words));
}
public int minimumLengthEncoding(String[] words) {
if(words.length==0)
{
return 0;
}
if(words.length==1)
{
return words[0].length()+1;
}
for(int i=0;i<words.length;i++)
{
words[i]=reverseString(words[i]);
}
Arrays.sort(words);
int encodeLen=0; //编码长度
for(int i=0;i<words.length-1;i++)
{
if(!isPrefix(words[i],words[i+1])) //当前单词不是紧挨着的下一个单词的前缀
{
encodeLen+=(words[i].length()+1);
}
}
encodeLen+=(words[words.length-1].length()+1);
return encodeLen;
}
String reverseString(String s) //反转字符串
{
StringBuffer sb=new StringBuffer();
for(int i=s.length()-1;i>=0;i--)
{
sb.append(s.charAt(i));
}
return sb.toString();
}
boolean isPrefix(String s1,String s2) //判断s1是否是s2的前缀
{
if(s1.length()>s2.length())
{
return false;
}
for(int i=0;i<s1.length();i++)
{
if(s1.charAt(i)!=s2.charAt(i))
{
return false;
}
}
return true;
}
}
经验总结
- 对于后缀问题,我们可以进行某些操作(例如本题中的反转每个单词),让它变成前缀问题
- 对于前缀问题,我们可以尝试使用Trie树(前缀树、字典树)来解决
- 对于无序的数组,我们可以尝试排序来转化问题,然后从排序后的数组来寻找规律