2024.1.9
- 题目来源
- 我的题解
- 方法一 集合+动态规划
- 方法二 字典树+动态规划
题目来源
力扣每日一题;题序:2707
我的题解
方法一 集合+动态规划
题目的要求是讲字符串s划分为多个不重叠的子串,然后尽可能使得更多的子串匹配到dictionary中的字符串,然后将这些匹配的子串删除之后,使得剩下的额外字符最小。
可以假设s的长度为n,分割的方式:
- 把s的最后一个字符s[n-1]当做是额外字符,这问题转化为求长度为n-1的子问题;
- 找到一个s的后缀s[j…n-1]构成的子串在dictionary中,这问题转换为j-1的子问题。
因此,可以将得出动态规划的转移方程:
d p [ i ] = d p [ i − 1 ] + 1 , s [ i − 1 ] 当做额外字符 dp[i]=dp[i-1]+1,s[i-1]当做额外字符 dp[i]=dp[i−1]+1,s[i−1]当做额外字符
d p [ i ] = m i n ( d p [ i ] , d p [ j ] ) , s 的后缀 s [ j . . . n − 1 ] 构成的子串在 d i c t i o n a r y 中 dp[i]=min(dp[i],dp[j]),s的后缀s[j...n-1]构成的子串在dictionary中 dp[i]=min(dp[i],dp[j]),s的后缀s[j...n−1]构成的子串在dictionary中
为了判断子串是不是在dictionary中,将dictionary中的字符串使用HashSet集合存储。
时间复杂度:O( n 3 + m n^3+m n3+m)。n为s的长度,m为dictionary的长度。m是初始化HashSet的时间; n 3 n^3 n3分别是递推需要O(n),每次子问题需要O(n),并且求子串需要O(n)。
空间复杂度:O(n+m)。dp数组的空间和HashSet的空间。
public int minExtraChar(String s, String[] dictionary) {int n=s.length();int m=dictionary.length;int[] dp=new int[n+1];Arrays.fill(dp,Integer.MAX_VALUE);Set<String> set=new HashSet<>();for(int i=0;i<m;i++){set.add(dictionary[i]);}dp[0]=0;for(int i=1;i<=n;i++){// s[i-1]当做额外字符dp[i]=dp[i-1]+1;for(int j=i-1;j>=0;j--){//s的后缀s[j...n-1]构成的子串在dictionary中if(set.contains(s.substring(j,i))){dp[i]=Math.min(dp[i],dp[j]);}}}return dp[n];
}
方法二 字典树+动态规划
在方法一中查找某个子串是否在dictionary中效率较低,因为查找了s[i+1,j],再次查找s[i,j],这时后者仅仅第一个字符不同,但是仍需要从开始开始对比,要花费O(n)的时间重复查找。
可以使用字典树存储dictionary中字符串的逆序,这样在字典树上找到表示后缀s[i+1,j]的节点后,只需要再花O(1)的时间来判断表示后缀s[i,j]的节点是否存在。
时间复杂度:O( n 2 + ∣ S ∣ n^2+|S| n2+∣S∣)。|S|表示dictionary中最长字符串的长度
空间复杂度:O(n+∣T∣⋅Σ)。|T|表示dictionary中所有字符串的长度之和,Σ为字符集的大小,这里是26。
public int minExtraChar(String s, String[] dictionary) {
int n=s.length();int m=dictionary.length;int[] dp=new int[n+1];Arrays.fill(dp,Integer.MAX_VALUE);Trie t=new Trie();// 将dictionary中的字符串逆序插入字典树for(int i=0;i<m;i++){StringBuilder sb=new StringBuilder(dictionary[i]);t.insert(sb.reverse().toString());}dp[0]=0;for(int i=1;i<=n;i++){dp[i]=dp[i-1]+1;Trie node=t;for(int j=i-1;j>=0;j--){// 若当前节点为空表示在dictionary中找不到s[j,i]的字符串if(node!=null){// 看下一个字典树节点是不是存在node=node.searchSuffix(s.charAt(j));if(node!=null&&node.isEnd()){dp[i]=Math.min(dp[i],dp[j]);}}}}return dp[n];
}
class Trie {Trie children[];//子节点boolean isEndOf;//是否是结尾public Trie() {children=new Trie[26];isEndOf=false;}//插入新的字符串public void insert(String word) {Trie cur=this;for(int i=0;i<word.length();i++){int ch=word.charAt(i)-'a';if(cur.children[ch]==null){//若没有该分支,则新建分支cur.children[ch]=new Trie();}cur=cur.children[ch];}cur.isEndOf=true;//记录结尾}//判断当前字典树节点是不是一个结尾public boolean isEnd(){return isEndOf;}private Trie searchSuffix(char ch){//查找ch为结尾的字典树节点Trie cur=this;if(cur==null||cur.children[ch-'a']==null){return null;}cur=cur.children[ch-'a'];return cur;}
}
有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈😄~