动态规划入门1

概要

通过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(i1,j1)+1max(lcs(i1,j),lcs(i,j1))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(NM) O ( N ∗ M )

空间复杂度均为 O(NM) 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(n1,m1)1+min(ed(n1,m),ed(n,m1),ed(n1,m1))if seq[n1]=seq[m1]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)

系列文:
动态规划入门1
动态规划入门2
动态规划入门3

点赞