Given a string s and a dictionary of words dict, determine if s can be segmented into a space-separated sequence of one or more dictionary words.
For example, given
s = "leetcode"
,
dict = ["leet", "code"]
.
Return true because "leetcode"
can be segmented as "leet code"
.
这题是经典的dp问题,我之前一直都没搞清楚。今天终于一次写出来accept了。最早使用dfs做的,可想而知,直接超时。
这就像斐波那契数列那样,如果你用递归,大部分时间都在做重复的工作。换成这个题,思路也是一样的,那如何减少工作量?就是把之前算出来的结果cache起来。
dp的逻辑普遍不好想,所以要经常联系如何用dp的角度考虑问题。这种字符类的算法题,一半和dp有关。
思路:将大问题分解成小问题:
比如输入是“abcd” ,字典我们不知道:
我们倒着想,如果abcd是可以break的,根据题意,要么abcd在字典里,要么abcd的两个或者多个部分就在字典里。([a,bcd] or [a,b,cd]之类的情况)
也就是说 abc如果是可以break的,那么如果d在字典里,必然abcd就是break的。如果这一点想通了,下面的推导和分析就是把思路转化为普遍情况的程序的过程。
从最开始遍历到最后,建立一个boolean数组来存结果dp[n+1], n是字符串s的大小。
我们定义,dp[]的意义在于,dp[i]代表着到第i个substring的时候,是否能分解。
比如说: dp[2] 那就代表 s.substring(0,2)的时候的情况(也就是 "ab"是否可以break)。 dp[0]算是一个base case,因为我们每次都在拿前面的结果来推断现在的情况,那么最开始的时候(“a”)可以使用dp[0]的结果,所以多加一个会方便很多。
为啥要用dp[n+1],这其实也是方便计算。看别人代码的时候,我就在想,莫非都要多申明一个空间?其实不然,你也可以用dp[n],但是就是得加点判断,麻烦点。在熟练dp的思路以后,这些细节都可以自己做处理。
最关键的一点就是搞清dp的递推式:dp[i]=?
这个题不难想:根据上面的分析 如果要dp[i]成立
(1)那么要么s.substring(0,i)是在字典里的,比如说abcd, [a,abcd]
(2)要么存在一个节点 j,使得s.substring(0,j) 是可以break, 而且s.substring(j,i)在字典里. 比如说 abcd [a,b,cd]
那么ab 是可以break的,cd在字典里, dp[i]=true;
你可能会想到,要是多个字符的时候,那怎么判断。我在想dp问题的时候,总会看到各种各样的分支情况,干扰了思维。
其实不用担心,我们从左到右遍历的时候,已经把这个情况调查完了,结果已经存在了dp中。只要任意的dp[k]==true,那就说明s.substring(0,k)必然是可以break的。所以只要存在一个(2)这样的情况,dp[i]=true; 所以dp的好处是不用做重复的工作.
所以我觉得dp的解法就是 (1)定义数组含义dp[], dp[][]代表什么?(2)分析子问题 (3)推导递推公式 (4)完成for循环 以及具体操作, 计算出dp[i]
下面的代码综合了(1)和(2)的情况:因为dp[0]是true的,那么我们肯定会看一下substring(0,i)是不是在dict里。所以两层for循环, 第二层是为了找出是否存(2)这样的情况使dp[i]=true;
public boolean wordBreak(String s, Set<String> dict) {
int n= s.length();
boolean[] dp=new boolean[n+1];
dp[0]=true;
for(int i=1;i<=n;i++){
for(int j=0;j<=i;j++){
if(dp[j] && dict.contains(s.substring(j,i))){
dp[i]=true;
}
}
}
return dp[n];
}