theme: juejin
---
题目:给定一个长度为L的字符串,以及有W个单词的字典。问最少需要从字符串中删除几个字母,使其最后仅由字典的单词组成。
例如:给定字符串s为jesslookedjustliketimherbrother,字典:["looked","just","like","her","brother"],则需要删除字符'j','e','s','s','t','i','m',即删除jesslookedjustliketimherbrother中删除线标记出来的部分,最终返回的是需要删除的字符数量,即7。
思考:逆向思维,若记最终保留的字符串长度为t,如果能求出t的长度的最大值,则本题答案即为s的长度减去t的长度。怎么求t的最大值呢?如果上来直接采取暴力手段,先将字典中的元素按照长度进行排序,先保留字典中长度较长的元素,然后再考虑较短的元素。依然拿上面的例子举例,先将字典按照元素的长度进行倒排,得到["brother","looked","just","like","her"],最后不难发现需要保留的字符即是字典中这5个元素,并且保留的个数都为1,因此最终返回的长度为s的长度减去这五个字符的长度,即len(s)-len("borther")-len("looked")-len("just")-len("like")-len("her"),最后结果也为7。
但是!!这种暴力解法其实有问题的,例如给定字符串为"abcde",字典为["bcd","ab","de"]。如果按照上面的解法则最终保留的字符串为"bcd",因此得出的答案为2。但显然还有更优的选择,那就是保留"ab"和"de",这种情况需要删除的字符数量为1。因此上面的暴力解法不可行!
显然还有一种最最暴力的方法,那就是求出字符串的所有字串,然后判断保留下来的字串是否都由字典中的元素组成,显然这种方法时间复杂度极高,至少为指数级别,其中n为字符串的长度。
下面直接给结果。那就是采用动态规划的方法,首先定义dp数组,dp[i]为使得子串t都由字典中元素组成所需删除的最少字符数,其中子串t是从输入的字符串s从i位置到末尾截取而来的。
那么如果知道了dp[i+1],dp[i+2],....,dp[len(s)-1],应该怎么推导得出dp[i]呢?有两种情况,第一种情况即使加入i位置的字符,但是该字符毫无用处,并不能跟字典中任何一个元素的首字符匹配,这种情况显然加入的字符也会被删除掉,则dp[i]=dp[i+1]+1;第二种情况,加入的字符跟字典中的元素可能有连锁反应,即字符串s以i位置为起点可能跟字典某些元素匹配。此时就要考虑是否要把匹配的元素保留下来,如果保留,记当前与之匹配的元素为e,e的长度为len(e),此时dp[i]=dp[i+len(e)],即此时从i位置到i+len(e)-1之间的字串即为匹配的元素e,需要将他们保留下来。按照这个逻辑i从后往前移,最终算得dp[0]即为本题的解。
直接上代码,getMinimumDeleteCharacter为主方法,str是输入的字符串,dict为字典。
public int getMinimumDeleteCharacter(String str,String [] dict){
//将字典放入前缀树中,方便后续查找
for (String s : dict) {
put(s);
}
//dp数组多预留一个长度,存放边界情况,对于dp[str.length()],此时其是一个空串,显然值为0
int [] dp = new int[str.length()+1];
for(int i=str.length()-1;i>=0;i--){
dp[i]=dp[i+1]+1;
List<Integer> list = getMatchedLength(str,i);
/**
* 拿str.subString(i)与字典进行比较,list的size()等于字典中有多少个元素与str.subString(i)的前缀相等
* list.get(i)对应字典中匹配成功的元素的长度
*/
for(int length:list){
dp[i]=Math.min(dp[i],dp[i+length]);
}
}
return dp[0];
}
为了在i位置与字典进行匹配,建立前缀树,以下是将字典存放进前缀树的put()方法:
public void put(String key){
root = put(root,key,0);
}
public Node put(Node node,String key,int index){
if(node==null){
node = new Node();
}
if(index==key.length()){
node.isEnd=true;
return node;
}
char c = key.charAt(index);
node.child[c]=put(node.child[c],key,index+1);
return node;
}
获取所有匹配的字符串所对应的长度的getMatchedLength()方法:
public LinkedList<Integer> getMatchedLength(String key, int index){
LinkedList<Integer> list = new LinkedList<>();
Node p = root;
int count=0;
for(int i=index;i<key.length();i++){
if(p==null) return list;
char c = key.charAt(i);
p = p.child[c];
count++;
if(p!=null&&p.isEnd){
list.add(count);
}
}
return list;
}
最后贴出最终代码:
/**
* 为了方便理解,这里直接取的一个字节的长度,实际上题目指定了字典和字符串中都为小写字符,因此为了减少内存消耗,读者可将R设为26,
* 但是需要记住同步改put()和getMatchedLength方法,所有字符需要都减'a'
*/
private final int R=256;
/**
* 前缀树的根
*/
private Node root;
private class Node{
boolean isEnd;//是否为数据结点,默认不是
Node [] child = new Node[R];
}
public int getMinimumDeleteCharacter(String str,String [] dict){
//将字典放入前缀树中,方便后续查找
for (String s : dict) {
put(s);
}
int [] dp = new int[str.length()+1];
for(int i=str.length()-1;i>=0;i--){
dp[i]=dp[i+1]+1;
List<Integer> list = getMatchedLength(str,i);
/**
* 拿str.subString(i)与字典进行比较,list的size()等于字典中有多少个元素与str.subString(i)的前缀相等
* list.get(i)对应字典中匹配成功的元素的长度
*/
for(int length:list){
dp[i]=Math.min(dp[i],dp[i+length]);
}
}
return dp[0];
}
public LinkedList<Integer> getMatchedLength(String key, int index){
LinkedList<Integer> list = new LinkedList<>();
Node p = root;
int count=0;
for(int i=index;i<key.length();i++){
if(p==null) return list;
char c = key.charAt(i);
p = p.child[c];
count++;
if(p!=null&&p.isEnd){
list.add(count);
}
}
return list;
}
public void put(String key){
root = put(root,key,0);
}
public Node put(Node node,String key,int index){
if(node==null){
node = new Node();
}
if(index==key.length()){
node.isEnd=true;
return node;
}
char c = key.charAt(index);
node.child[c]=put(node.child[c],key,index+1);
return node;
}