【算法篇】POJ-3267 The Cow Lexicon

134 阅读4分钟

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;
}