在第三节中,我们将讨论序列化问题中的动态规划解法。这部分多半分为单序列和双序列等问题
例一:最长上升子序列。
最长上升子序列问题,有一个正整数数列,长度n在1000之内,元素a[i]在10^5之内,求最长递增子序列的长度。
分析一:发现问题的可分性质
如果我们采用穷举法,将有2^n的时间复杂度;这里面有很多是重复的4、3、***类型的子序列,以4开头的递增子序列的长度都是1.
很明显,我可以写出递归函数
dp(int i)={
a[i]=1;
for (int j = i+1; j < n; ++j){
if(a[i]<a[j])
temp=max(a[i], dp(j));
}
return temp;
}:以a[i]开始的子序列的最长递增长度,然后从dp(i)中选择最大的即可。
这里,时间复杂度是O(n^2);在递归与分治法过后,是不是发现了问题的端倪呢?到这里,我们就可以很方便地转化成动态规划的问题了。形式有两种,读者可以自己去实现:1)dp[i]:以a[i]开始的最长递增子序列的长度;2)dp[i] 以a[i]结尾的最长递增子序列的长度
分析二:针对查找的优化
假设dp[i]:以a[i]为结尾的最长LIS的长度。很显然,我们按照i递增的顺序递增dp[i],这样,如果dp[j](j<i)长度相同,我们需要选择a[j]最小的那个来计算就可以了;而不是进行遍历。所以,我们定义新的子问题dp[i]长度为i+1的子序列中,结尾元素的最小值(不存在就是INF)。
对于每个a[j], 如果i=0或者dp[i-1]<a[j], dp[i] = min(dp[i], a[j])
for (int i = 0; i < n; ++i){
for (j = 0; j < n; ++j){
if(i==0 || dp[i-1]<a[j]) dp[i]=min(dp[i], a[j]);
}
}
这样下去,有两层循环:长度l和数组下标j;时间复杂度仍然是O(n^2);但是,有改进的地方:因为数组dp[i]是单调递增的;所以对于每次a[j],仅仅需要最多1次更新(不可能长度为i和i+1的LIS结尾的元素都是a[j]),所以我们可以采用二分查找来确定更新的dp[i];
for (i = 0; i < n; ++i){
*lower_bound(dp, dp+n, a[i]) = a[i];
}
return lower_bound(dp, dp+n, INF) - dp;
Eg2:Word Break
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".
分析:
很显然,我们可以先将问题二分:找到以s[0]开头,在dict中的字符串s1,然后剩下的是s2;这样判断solution(s2)是否满足即可。
仔细分析不难发现,加入dict=["ab", "abab"], s="ababcd";此时我们需要判断solution("abcd"), solution("cd"); 而solution("cd")又包含在solution("abcd")之中。所以问题满足重叠子问题和最优子结构。这个时间复杂度是O(n^2)。
class Solution { public: bool wordBreak(string s, unordered_set<string> &dict) { int n=s.size(); if(n==0) return true; bool dp[n+1]; fill(dp, dp+n+1, false); dp[0]=true; int i, j; for (i = 0; i < n; ++i){ for(j=i;j>=0;j--){ if(dp[j]){ if( dict.find(s.substr(j, i+1 - j)) != dict.end() ){ dp[i+1]=true; break; } } } } return dp[n]; } };
进一步的分析:如果有这样的一个例子,s="aaaaaaaaaaaaaaa...a"(100个a);同样dict里面的元素就是s,这样,这个时间复杂度依然是O(n^2),而实际上,dp[0]~dp[n-1]的结果都为0.在计算dp[i]的时候,我们的时间复杂度也是O(n), 这个时间复杂度可以减小为O(dict.size()). 另一方面,我们采用了线性查找来确定d[j]为true,实际上,我们可以采用堆栈来存储dp[j]为true对应的j,这也能在一定程度上减小时间复杂度。