动态规划 (Dynamic Programming)

本文回顾常见的动态规划问题,并解决以下 2 个问题:

  • 求最优值
  • 输出最优值的一个可行解

背包问题

参考:https://www.cnblogs.com/sinkinben/p/knapsack.html

最长公共子串

最长公共子串,也称为 "Longest Common Substring, LCS" 。

dp[i, j] 表示 s1[0, ..., i]s2[0, ..., j] 的 LCS 子串的长度 。

dp[i, j] = dp[i - 1, j - 1] + 1 if s1[i] == s2[j]
dp[i, j] = 0, otherwise
return max(dp)

最优值

int lcstring(string &s1, string &s2)
{
    int len1 = s1.length(), len2 = s2.length(), res = 0;
    vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
    for (int i = 1; i <= len1; ++i)
    {
        for (int j = 1; j <= len2; ++j)
        {
            dp[i][j] = (s1[i - 1] == s2[j - 1]) ? dp[i - 1][j - 1] + 1 : 0;
            res = max(dp[i][j], res);
        }
    }
    return res;
}

可行解

由于子串是连续的,并且已知 LCS 长度,所以我们只需要「记住」LCS 子串在 s1 的结束位置。

int lcstring(string &s1, string &s2)
{
    int len1 = s1.length(), len2 = s2.length(), res = 0;
    int end = 0;
    vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
    for (int i = 1; i <= len1; ++i)
    {
        for (int j = 1; j <= len2; ++j)
        {
            dp[i][j] = (s1[i - 1] == s2[j - 1]) ? dp[i - 1][j - 1] + 1 : 0;
            res = max(dp[i][j], res);
            if (dp[i][j] == dp[i - 1][j - 1] + 1)
                end = i;
        }
    }
    printf("lcstring = %s \n", s1.substr(end + 1 - res).c_str());
    return res;
}

最长公共子序列

最长公共子序列,即 "Longest Common Sub-sequence, LCS" 。

dp[i, j] 表示 s1[0, ..., i]s2[0, ..., j] 的 LCS 序列的长度 。

dp[i, j] = max(dp[i-1][j-1] + s1[i] == s2[j], dp[i-1, j], dp[i, j-1])
return dp[m, n]

最优值

Leetcode: 1143. Longest Common Subsequence

class Solution
{
public:
    int longestCommonSubsequence(string s1, string s2)
    {
        int n1 = s1.length(), n2 = s2.length();
        vector<vector<int>> dp(n1 + 1, vector<int>(n2 + 1, 0));
        for (int i = 1; i <= n1; ++i)
            for (int j = 1; j <= n2; ++j)
                dp[i][j] = max(dp[i - 1][j - 1] + (s1[i - 1] == s2[j - 1]),
                               max(dp[i][j - 1], dp[i - 1][j]));
        return dp[n1][n2];
    }
};

可行解

int lcsequence(string s1, string s2)
{
    int n1 = s1.length(), n2 = s2.length();
    vector<vector<int>> dp(n1 + 1, vector<int>(n2 + 1, 0));
    vector<vector<int>> path(n1 + 1, vector<int>(n2 + 1, 0));
    for (int i = 1; i <= n1; ++i)
        for (int j = 1; j <= n2; ++j)
            dp[i][j] = max(dp[i - 1][j - 1] + (s1[i - 1] == s2[j - 1]), max(dp[i][j - 1], dp[i - 1][j]));
    
    int i = n1, j = n2;
    string lcs = "";
    while (i > 0 && j > 0)
    {
        if (s1[i - 1] == s2[j - 1]) lcs.push_back(s1[i - 1]), --i, --j;
        else if (dp[i][j - 1] > dp[i - 1][j]) --j;
        else --i;
    }
    reverse(lcs.begin(), lcs.end());
    cout << lcs << "\n";
    return dp[n1][n2];
}

输出所有 LCS 可行解:https://www.cnblogs.com/sinkinben/p/14938702.html

最长递增子序列

最长递增子序列,即 "Longest Increasing Sub-sequence, LIS" 。或者叫「最长不下降子序列」,取决于是否要求子序列是严格递增的。

最优值

Leetcode:


经典 \(O(n^2)\) 解法

dp[i] 表示以 a[i] 结尾的 LIS 的长度。

fill dp with 1
dp[i] = max(dp[i], dp[j] + 1) for 0 <= j < i and a[j] < a[i]
return max(dp)

代码实现:

class Solution
{
  public:
    int lengthOfLIS(vector<int> &nums)
    {
        int n = nums.size(), res = 0;
        vector<int> dp(n, 1);
        for (int i = 0; i < n; ++i)
        {
            for (int j = 0; j < i; ++j)
                if (nums[j] < nums[i])
                    dp[i] = max(dp[i], dp[j] + 1);
            res = max(res, dp[i]);
        }
        return res;
    }
};


\(O(n\log{n})\) 解法

想要 LIS 尽可能「长」,那么 LIS 的增长速率要尽可能慢,即:LIS 末端的元素尽可能小。

  • 假设 LIS 序列的下标范围是 [0, idx],令 vec[idx] 表示 LIS 序列末端的最小值。
    • 换个角度来看,vec[idx] 表示所有长度为 idx + 1 的 LIS 中,它们的末端的最小值。
  • idx = 0 时,表示 LIS 只有一个元素 nums[0] ,依次类推。
class Solution {
public:
    const int INF = 0x3f3f3f3f;
    int lengthOfLIS(vector<int>& nums) {
        if (nums.empty()) return 0;

        int n = nums.size(), idx = 0;
        vector<int> vec(n, INF);
        vec[0] = nums[0];
        
        for (int i = 1; i < n; ++i)
        {
            if (nums[i] > vec[idx])
                vec[++idx] = nums[i];
            else
            {
                /* We use lower_bound here, thus nums[i] <= *itor. */
                auto itor = std::lower_bound(begin(vec), begin(vec) + idx + 1, nums[i]);
                *itor = nums[i];
            }
        }
        /* there are totally idx + 1 valid elements in vec */
        return idx + 1;
    }
};

更加简洁的代码:

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        if (nums.empty()) return 0;

        int n = nums.size();
        vector<int> vec{nums[0]};
        
        for (int i = 1; i < n; ++i)
        {
            auto itor = std::lower_bound(begin(vec), end(vec), nums[i]);
            if (itor == end(vec))
                vec.push_back(nums[i]);
            else
                *itor = nums[i];
        }
        return vec.size();
    }
};

可行解

int LIS(vector<int> &nums)
{
    int n = nums.size(), maxlen = 0;
    vector<int> dp(n, 1), prev(n, -1);
    for (int i = 0; i < n; ++i)
    {
        prev[i] = i;
        for (int j = 0; j < i; ++j)
        {
            if (nums[j] < nums[i])
            {
                dp[i] = max(dp[i], dp[j] + 1);
                if (dp[i] == dp[j] + 1)
                    prev[i] = j;
            }
        }
        maxlen = max(maxlen, dp[i]);
    }

    int idx = n - 1;
    vector<int> seq;
    /* Find (one of) the end position of LIS. */
    while (idx >= 0 && dp[idx] != maxlen) --idx;

    /* Backtrack the DP path. */
    while (idx != prev[idx])
    {
        seq.push_back(nums[idx]);
        idx = prev[idx];
    }
    seq.push_back(nums[idx]);
    reverse(seq.begin(), seq.end());

    /* Print one of the LIS. */
    for (int x : seq) cout << x << ' ';

    return maxlen;
}


可行解的数目

Leetcode: 673. Number of Longest Increasing Subsequence

cnt[i] 表示以 nums[i] 结尾的 LIS 的数目。

class Solution
{
  public:
    int findNumberOfLIS(vector<int> &nums)
    {
        int n = nums.size(), maxlen = 1;
        vector<int> dp(n, 1), cnt(n, 1);
        for (int i = 0; i < n; ++i)
        {
            for (int j = 0; j < i; ++j)
            {
                if (nums[j] < nums[i])
                {
                    if (dp[i] < dp[j] + 1)
                    {
                        dp[i] = dp[j] + 1;
                        cnt[i] = cnt[j];
                    }
                    else if (dp[i] == dp[j] + 1)
                        cnt[i] += cnt[j];
                }
            }
            maxlen = max(maxlen, dp[i]);
        }

        int res = 0;
        for (int i = 0; i < n; ++i)
            if (dp[i] == maxlen)
                res += cnt[i];
        return res;
    }
};

最大连续子序列之和

Leetcode: https://leetcode.com/problems/maximum-subarray/

Let dp[i] denote the max sum of contiguous sub-array, ending with nums[i].

Then the state equation is:

dp[i] = max(nums[i], nums[i] + dp[i - 1])

The returned value should be max(dp[0, ..., n - 1]).


最优值

class Solution
{
public:
    int maxSubArray(vector<int>& nums)
    {
        int n = nums.size();
        vector<int> dp(n, 0);
        dp[0] = nums[0];
        for (int i = 1; i < n; ++i)
            dp[i] = max(nums[i], nums[i] + dp[i - 1]);
        return *max_element(dp.begin(), dp.end());
    }
};

/* dp[i] = max(nums[i], nums[i] + dp[i - 1]) */

空间优化:

class Solution
{
public:
    int maxSubArray(vector<int>& nums)
    {
        int dp = 0, res = 0;
        for (int x : nums)
            dp = max(x, x + dp), res = max(res, dp);
        return res;
    }
};

可行解

与「最长公共子串」类似,子数组也是连续的,我们只需要记录「最大连续子数组」的结束位置。

int maxSubArray(vector<int> &nums)
{
    int dp = 0, res = 0, n = nums.size();
    int x, idx = 0;
    for (int i = 0; i < n; ++i)
    {
        x = nums[i];
        dp = max(x, x + dp), res = max(res, dp);
        if (res == dp) idx = i;
    }
    int sum = res;
    while (sum != 0)
    {
        cout << nums[idx] << ' ';
        sum -= nums[idx];
        idx -= 1;
    }
    return res;
}

最长回文子串

dp[i, j] 表示 s[i, ..., j] 是否为回文串。

dp[i, j] = dp[i + 1, j - 1] && (s[i] == s[j])
return max(dp)

最优值与可行解

Leetcode: https://leetcode.com/problems/longest-palindromic-substring/

class Solution
{
  public:
    string longestPalindrome(string s)
    {
        int n = s.length(), maxlen = 1, start = 0;
        vector<vector<bool>> dp(n, vector<bool>(n, 0));
        for (int i = 0; i < n; ++i)
        {
            dp[i][i] = 1;
            if (i + 1 < n) dp[i][i + 1] = (s[i] == s[i + 1]);
        }
        int i, j, d;
        for (d = 2; d < n; ++d)
            for (i = 0; (j = i + d) < n; ++i)
                dp[i][j] = dp[i + 1][j - 1] && (s[i] == s[j]);

        for (int i = 0; i < n; ++i)
            for (int j = i; j < n; ++j)
                if (dp[i][j] && maxlen <= j - i + 1)
                    maxlen = j - i + 1, start = i;
        return s.substr(start, maxlen);
    }
};

最小路径和

Leetcode: https://leetcode.com/problems/minimum-path-sum/

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) 
    {
        if (grid.size() == 0 || grid[0].size() == 0) return 0;
        int m = grid.size(), n = grid[0].size();
        vector<vector<int>> dp(m, vector<int>(n, 0));
        
        dp[0][0] = grid[0][0];
        for (int j = 1; j < n; ++j) dp[0][j] = grid[0][j] + dp[0][j - 1];
        for (int i = 1; i < m; ++i) dp[i][0] = grid[i][0] + dp[i - 1][0];
        
        for (int i = 1; i < m; ++i)
            for (int j = 1; j < n; ++j)
                dp[i][j] = grid[i][j] + min(dp[i - 1][j], dp[i][j - 1]);
        return dp[m - 1][n - 1];
    }
};

/* Let dp[i, j] denote the min-sum to reach position [i, j]
 * dp[i, j] = grid[i, j] + min(dp[i-1, j], dp[i, j-1])
 */

求出可行解的方法与「最长公共子序列」、「背包问题」相同,读者可以自行思考,不再赘述。

矩阵链相乘

给定 \(n\) 个矩阵的矩阵链 \(M_0 \cdot M_1 \dots M_{n-1}\) ,确保矩阵链可进行合法的乘法运算,求最小的乘法操作次数。

如果 \(M_i = (a \times b), M_j = (b \times c)\) ,那么 \(M_i \cdot M_j = (a \times c)\) ,所需要的乘法操作次数为 \(a \cdot b \cdot c\)

输入:vector<int> matrix(n + 1),其中 matrix[i], matrix[i + 1] 表示的是矩阵 \(M_i\) 的大小。

dp[i, j] 表示 \(M_i, \dots, M_j\) 的最小乘法操作次数,那么 dp[i, i] = 0,且有:

dp[i][j] = min(dp[i][j], dp[i][k - 1] + dp[k][j] + matrix[i] * matrix[k] * matrix[j + 1]) for i < k <= j

解释

这是典型的区间 DP 问题。

对于每个 \(k\) ,可以将矩阵链拆分为 2 部分:\(\matrix{A} = (M_i \dots M_{k - 1})\)\(\matrix{B} = (M_k \dots M_j)\) .

  • 得到 \(\matrix{A}\) 需要的操作次数是 dp[i, k - 1] .
  • 得到 \(\matrix{B}\) 需要的操作次数是 dp[k, j] .
  • \(\matrix{A}\) 的大小是 matrix[i] * matrix[k], \(\matrix{B}\) 的大小是 matrix[k] * matrix[j + 1],因此二者相乘所需要的操作次数为 matrix[i] * matrix[k] * matrix[j + 1].

最优值

评测:https://practice.geeksforgeeks.org/problems/matrix-chain-multiplication0303/1

/**
 * n - Given `n` matrice.
 * matrix - matrix[i], matrix[i + 1] denote the size of M[i]
 * len(matrix) == n + 1
 */
int matrixChain(vector<int> &matrix, int n)
{
    const int INF = 0x3f3f3f3f;
    int d, i, j, k;
    vector<vector<int>> dp(n, vector<int>(n, INF));
    for (i = 0; i < n; ++i) dp[i][i] = 0;
    for (d = 1; d < n; ++d)
        for (i = 0; (j = i + d) < n; ++i)
            for (k = i + 1; k <= j; ++k)
                dp[i][j] = min(dp[i][j], dp[i][k - 1] + dp[k][j] + matrix[i] * matrix[k] * matrix[j + 1]);
    return dp[0][n - 1];
}

可行解

输出一个可行解的方法依旧类似,在此处,需要记录的是 dp[i][j] 的转移是根据哪一个 k 来的,因此需要开辟 trace[n][n] 去记录 trace[i][j] 对应的 k 值。

可以参考《算法导论》的 Chapter 15 .

void printSolution(vector<vector<int>> &trace, int n, int i, int j)
{
    if (i < 0 || j < 0 || i >= n || j >= n)
        return;
    if (i == j)
        printf("M%d", i);
    else
    {
        printf("(");
        printSolution(trace, n, i, trace[i][j] - 1);
        printSolution(trace, n, trace[i][j], j);
        printf(")");
    }
}

int matrixChain(vector<int> &matrix, int n)
{
    const int INF = 0x3f3f3f3f;
    int d, i, j, k;
    vector<vector<int>> dp(n, vector<int>(n, INF));
    vector<vector<int>> trace(n, vector<int>(n, 0));
    for (i = 0; i < n; ++i) dp[i][i] = 0;
    for (d = 1; d < n; ++d)
        for (i = 0; (j = i + d) < n; ++i)
            for (k = i + 1; k <= j; ++k)
            {
                int cur = dp[i][k - 1] + dp[k][j] + matrix[i] * matrix[k] * matrix[j + 1];
                dp[i][j] = min(dp[i][j], cur);
                if (cur == dp[i][j])
                    trace[i][j] = k;
            }
    printSolution(trace, n, 0, n - 1);
    return dp[0][n - 1];
}

测试用例

int main()
{
    int N = 5;
    vector<int> nums = {5, 10, 4, 6, 10, 2};

    N = 3;
    nums = {1, 2, 3, 4};

    N = 5;
    nums = {3, 5, 10, 8, 2, 4};

    N = 2;
    nums = {10, 10, 20};

    N = 3;
    nums = {3, 5, 7, 9};

    N = 4;
    nums = {40, 20, 30, 10, 30};

    N = 6;
    nums = {30, 35, 15, 5, 10, 20, 25};

    assert(nums.size() == N + 1);
    cout << matrixChain(nums, N) << endl;
}
posted @ 2019-09-12 16:45  sinkinben  阅读(501)  评论(0编辑  收藏  举报