动态规划 (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;
}