概要
通过geeksforgeeks上总结的常见的动态规划面试题,来了解动态规划的基本思路。在之后的博文,将通过对LeetCode上相关题目的题解来强化学习。
lis(最大上升序列长)
题目
https://leetcode.com/problems/longest-increasing-subsequence/description/
分析
若暴力求解最大上升序列,则需要考虑所有序列情况,设序列长为N,那么将耗费时间复杂度 O(2N) O ( 2 N ) 。
考虑通过动态规划,划分子问题,通过记忆法或制表法避免重叠子问题求解。
可将此题看做搜索所有情况,找出最大的上升序列长,即 lis(N)=max0<i<N(_lis(i)) l i s ( N ) = max 0 < i < N ( _ l i s ( i ) )
_lis(i) : 以i结尾的上升序列的最大长度。
接下来,就是求取以i结尾的序列的最大上升长度。
考虑最优子结构:将问题划分为子问题。
即把结尾为i的上升序列分解为它的子上升序列,问题转化为求解上升子序列的最大长度。
若下标为i的元素比下标为j的元素大,那么以i结尾的上升序列的最大长度是它最大的子上升序列加一。
_lis(i)=max0<j<i(_lis(j))+1 _ l i s ( i ) = max 0 < j < i ( _ l i s ( j ) ) + 1
如果下标为i的元素的上升子序列不存在,那么它的上升序列即为它本身,长度为1。
现在,可以写下状态转移方程。
lis(N)=max0<i<N(_lis(i)) l i s ( N ) = max 0 < i < N ( _ l i s ( i ) )
_lis(i)={max0<j<i(_lis(j))+11if seq[i]>seq[j]others(1) (1) _ l i s ( i ) = { max 0 < j < i ( _ l i s ( j ) ) + 1 if s e q [ i ] > s e q [ j ] 1 o t h e r s
编程
现在,让我们采用自顶向下的记忆法来解决重叠的子问题求解。
#include<vector>
#include <stdio.h>
#include <iostream>
using namespace std;
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.empty())
return 0;
int N = nums.size();
int dp[nums.size()];
memset(dp, 0, sizeof(dp));
int max = 1; // {0}时,直接等于此处值
for(int i = 0; i < N; i++)
_lis(nums, i, &max, dp);
return max;
}
int _lis(vector<int>& arr, int i, int * globalMax, int dp[]){
int max = 1, tmp = 1;
if(dp[i] != 0)
return dp[i];
if(i == 0)
return dp[i] = 1;
//int t;
for(int j = 0; j < i; j++){
tmp = _lis(arr, j, globalMax, dp);
if(arr[i] > arr[j] && tmp+1 > max){ //条件 s
max = tmp+1;
} // e
/*if(tmp >= max){ max = tmp; t = j; }*/
}
//if(arr[i] > arr[t]) max++;
if(*globalMax < max)
*globalMax = max;
return dp[i] = max;
}
};
int main(int argc, char *argv[]){
int a[] = {10, 9, 2, 5, 3, 7, 101, 18};
vector<int> arr(a, a + sizeof(a) / sizeof(int));
Solution * s = new Solution();
cout << s->lengthOfLIS(arr);
return 0;
}
条件处可用注释处代替。
再使用自底向上的制表法。
#include<vector>
#include <stdio.h>
#include <iostream>
using namespace std;
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.empty())
return 0;
int N = nums.size();
int dp[nums.size()];
for(int i = 0; i < N; i++){
dp[i] = 1;
for(int j = 0; j < i; j++){
if(nums[i] > nums[j] && dp[i] < dp[j] + 1)
dp[i] = dp[j] + 1;
}
}
int max = 1;
for(int i = 0; i < N; i++){
max = (dp[i] > max) ? dp[i] : max;
}
return max;
}
};
int main(int argc, char *argv[]){
int a[] = {4,10,4,3,8,9};
vector<int> arr(a, a + sizeof(a) / sizeof(int));
Solution * s = new Solution();
cout << s->lengthOfLIS(arr);
return 0;
}
算法的时间复杂度均为 O(N2) O ( N 2 )
空间复杂度均为 O(N) O ( N )
lcs(最大公共子序列长)
题目
http://www.lintcode.com/zh-cn/problem/longest-common-subsequence/
分析
求解两串最大公共子序列长,设lcs(i, j)为串1的[0, i]子串和串2的[0, j]子串的最大公共子序列长。
将问题划分为更小的子问题,lcs的取值可能与哪些子问题有关?
当str1[i]等于str2[j]时,lcs(i, j)的取值为lcs(i-1, j-1)加一。
当str1[i]与str2[j]不相等时,lcs(i, j)的取值可能与lcs(i-1, j)或lcs(i, j-1)相等,因为是最大公共子序列长,所以会选择两者中最大的数值作为lcs(i, j)的取值。
因此,可以写出状态转移式:
lcs(i,j)={lcs(i−1,j−1)+1max(lcs(i−1,j),lcs(i,j−1))if str1[i]=str2[j]others(2) (2) l c s ( i , j ) = { l c s ( i − 1 , j − 1 ) + 1 if s t r 1 [ i ] = s t r 2 [ j ] max ( l c s ( i − 1 , j ) , l c s ( i , j − 1 ) ) o t h e r s
由上式可以看出,当i等于0或者j等于0时,上式不成立。这也是递归的出口,算法的边界条件。
When i=0 or j = 0: lcs(i,j)={10if str1[i]=str2[j]if str1[i]≠str2[j](3) (3) When i=0 or j = 0: l c s ( i , j ) = { 1 if s t r 1 [ i ] = s t r 2 [ j ] 0 if s t r 1 [ i ] ≠ s t r 2 [ j ]
编程
现在,我们先用自顶向下的记忆法来解决重叠子问题的重复计算。
#include<string>
#include <iostream>
using namespace std;
class Solution { public: /** * @param A: A string * @param B: A string * @return: The length of longest common subsequence of A and B */ int longestCommonSubsequence(string &A, string &B) { // write your code here if(A.empty() || B.empty()) return 0; int dp[A.length()][B.length()]; memset(dp, -1, sizeof(int)*A.length()*B.length()); for(int i = 0; i < A.length(); i++) dp[i][0] = (A.at(i) == B.at(0)) ? 1 : 0; for(int i = 0; i < B.length(); i++) dp[0][i] = (A.at(0) == B.at(i)) ? 1 : 0; return lcs(A, B, &dp[0][0], A.length() - 1, B.length() -1); } int lcs(string &A, string &B, int * dp, int i, int j){ if((i == 0 || j == 0)) return *(dp+i*B.length()+j); if(*(dp+i*B.length()+j) >= 0) return *(dp+i*B.length()+j); if(A.at(i) == B.at(j)) return *(dp+i*B.length()+j) = lcs(A, B, dp, i-1, j-1) + 1; return *(dp+i*B.length()+j) = (lcs(A, B, dp, i-1, j) > lcs(A, B, dp, i, j-1)) ? *(dp+(i - 1) * B.length()+j) : *(dp+i*B.length() + j - 1); } }; int main(){ string A = "orfufddaunsxhkkrpvxiitjehoggahgeyuzfbsdwxqcmprdxap"; string B = "coioukswtaxujdhbpoekwkglartrlxxxngjodqfzikyunhtext"; Solution * s = new Solution(); cout << s->longestCommonSubsequence(A, B); return 0; }
再用自底向上的制表法。
#include<string>
#include <iostream>
using namespace std;
class Solution {
public:
/** * @param A: A string * @param B: A string * @return: The length of longest common subsequence of A and B */
int longestCommonSubsequence(string &A, string &B) {
// write your code here
int dp[A.length()][B.length()];
memset(dp, -1, sizeof(int) * A.length() * B.length());
for(int i = 0; i < A.length(); i++){
for(int j = 0; j < B.length(); j++){
if(i == 0 || j == 0){
dp[i][j] = (A.at(i) == B.at(j)) ? 1 : 0;
continue;
}
if(A.at(i) == B.at(j)){
dp[i][j] = dp[i - 1][j - 1] + 1;
continue;
}
dp[i][j] = (dp[i-1][j] > dp[i][j-1]) ? dp[i-1][j] : dp[i][j-1];
}
}
return dp[A.length() - 1][B.length()-1];
}
};
int main(){
string A = "orfufddaunsxhkkrpvxiitjehoggahgeyuzfbsdwxqcmprdxap";
string B = "coioukswtaxujdhbpoekwkglartrlxxxngjodqfzikyunhtext";
Solution * s = new Solution();
cout << s->longestCommonSubsequence(A, B);
return 0;
}
算法的时间复杂度均为 O(N∗M) O ( N ∗ M )
空间复杂度均为 O(N∗M) O ( N ∗ M )
Edit Distance(最小编辑距离)
题目
https://leetcode.com/problems/edit-distance/description/
分析
同前两题一样,可以分解为两个子序列的最小编辑距离。
ed(i, j):序列1的[0, i)子序列与序列2的[0, j)子序列的最小编辑距离。
序列的最后一位元素相等,那么它的最小编辑距离等于子序列的最小编辑距离。
若不相等,则需要由其子序列进行插入、删除、替换。即由子序列[0, i)与[0, j-1)比较后进行插入;子序列[0, i-1)与[0, j)比较后进行删除;子序列[0, i-1)与[0, j-1)比较后进行替换之后的情况。则最小编辑距离为这三种情况的最小值加上操作数1。
因此,状态转移方程为:
ed(n,m)={ed(n−1,m−1)1+min(ed(n−1,m),ed(n,m−1),ed(n−1,m−1))if seq[n−1]=seq[m−1]others(4) (4) e d ( n , m ) = { e d ( n − 1 , m − 1 ) if s e q [ n − 1 ] = s e q [ m − 1 ] 1 + min ( e d ( n − 1 , m ) , e d ( n , m − 1 ) , e d ( n − 1 , m − 1 ) ) o t h e r s
当序列1或序列2的子序列个数为0时,最小编辑距离为另一个子序列的个数(记为a)。即进行a次插入使两串相同。所以,基本情况为:
ed(n,m)={mnif n=0if m=0(5) (5) e d ( n , m ) = { m if n = 0 n if m = 0
编程
自顶向下记忆法:
#include <iostream>
#include <string>
#include <stdio.h>
using namespace std;
class Solution {
public:
int minDistance(string word1, string word2) {
int dp[word1.length() + 1][word2.length() + 1];
memset(dp, -1, sizeof(int) * (1+word1.length()) * (1+word2.length()));
for(int i = 0; i <= word1.length(); i++)
dp[i][0] = i;
for(int i = 0; i <= word2.length(); i++)
dp[0][i] = i;
return md(word1, word2, word1.length(), word2.length(), &dp[0][0]);
}
int md(string s1, string s2, int n, int m, int * dp) {
if(*(dp + n * (s2.length()+1) + m) >= 0)
return *(dp + n*(s2.length()+1) + m);
if(s1.at(n - 1) == s2.at(m - 1)) {
return *(dp + n*(s2.length()+1) + m) = md(s1, s2, n - 1, m - 1, dp);
}
return *(dp + n*(s2.length()+1) + m) = 1 + min(md(s1, s2, n-1, m, dp),md(s1, s2, n, m-1, dp),md(s1, s2, n-1, m-1, dp));
}
int min(int a, int b, int c){
int min = a;
min = (min > b) ? b : min;
min = (min > c) ? c : min;
return min;
}
};
int main() {
string a ="";
string b = "";
Solution * s = new Solution();
cout << s->minDistance(a, b);
return 0;
}
自底向上制表法:
#include <iostream>
#include <string>
#include <stdio.h>
using namespace std;
class Solution {
public:
int minDistance(string word1, string word2) {
int dp[word1.length() + 1][word2.length() + 1];
for(int i = 0; i <= word1.length(); i++) {
for(int j = 0; j <= word2.length(); j++) {
if(i == 0) {
dp[i][j] = j;
continue;
}
if(j == 0) {
dp[i][j] = i;
continue;
}
if(word1.at(i-1) == word2.at(j-1)){
dp[i][j] = dp[i-1][j-1];
continue;
}
dp[i][j] = 1 + min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1]);
}
}
return dp[word1.length()][word2.length()];
}
int min(int a, int b, int c){
int min = a;
min = (min > b) ? b : min;
min = (min > c) ? c : min;
return min;
}
};
int main() {
string a ="";
string b = "";
Solution * s = new Solution();
cout << s->minDistance(a, b);
return 0;
}
时间复杂度:O(n*m)
空间复杂度:O(n*m)