Chapter 16 Dynamic Programming (4)
基本题目
300. 最长上升子序列
动态规划:根据之前所有更小的元素调整 dp 数值
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n, 1);
int maxLen = 1;
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
maxLen = max(dp[i], maxLen);
}
return maxLen;
}
};
贪心思想:为了让序列更长,我们需要让最后添加的数尽可能的小。每次读取一个元素时,我们可以考虑以它为结尾最长的符合条件的子序列,用它去替换之前得到的等长子序列。这样新的子序列的末尾元素会更小,可能在未来容纳更多的元素
正确性证明:如果末尾元素更大,那么将它添加到原来的等长子序列之后,可以得到更长的子序列
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size(), len = 1;
vector<int> lastVal(n + 1, INT_MIN);
lastVal[1] = nums[0];
for (int i = 0; i < n; i++) {
if (nums[i] > lastVal[len]) {
lastVal[len + 1] = nums[i];
len++;
}
else {
auto it = lower_bound(lastVal.begin(), lastVal.begin() + len, nums[i]);
*it = nums[i];
}
}
return len;
}
};
1143. 最长公共子序列
动态规划:前 i 个字符和前 j 个字符的比较
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int n = text1.size(), m = text2.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (text1[i - 1] == text2[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[n][m];
}
};
1035. 不相交的线
与最长公共子序列完全等价
class Solution {
public:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
int n = nums1.size(), m = nums2.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (nums1[i - 1] == nums2[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[n][m];
}
};
674. 最长连续递增序列
动态规划:如果更大则等于前一数值加 1,否则等于 1。可以滚动数组优化
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
int run = 0, prev = INT_MIN, maxRun = 0;
for (const int &num : nums) {
if (num > prev) run++;
else {
maxRun = max(run, maxRun);
run = 1;
}
prev = num;
}
return max(run, maxRun);
}
};
718. 最长重复子数组
动态规划:如果元素匹配,则 dp[i][j] = dp[i-1][j-1] + 1,否则为 0
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
int n = nums1.size(), m = nums2.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
int maxLen = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
if (nums1[i] == nums2[j]) {
dp[i + 1][j + 1] = dp[i][j] + 1;
maxLen = max(maxLen, dp[i + 1][j + 1]);
}
return maxLen;
}
};
每一个重复子数组实际上对应了一种数组对齐的方式,因此可以采用滑动数组的方式求解
这种方法本质上是利用了状态转移函数的特点:只依赖于同一对角线上的元素。滑动数组的方式实际上就是遍历所有的对角线,省略的 dp 数组的空间分配
class Solution {
public:
int maxLength(vector<int> &nums1, vector<int> &nums2, int offset) {
int i = offset > 0 ? offset : 0;
int j = offset > 0 ? 0 : -offset;
int maxLen = 0, run = 0;
while (i < nums1.size() && j < nums2.size()) {
if (nums1[i++] == nums2[j++]) run++;
else {
maxLen = max(maxLen, run);
run = 0;
}
}
return max(maxLen, run);
}
int findLength(vector<int>& nums1, vector<int>& nums2) {
int maxLen = 0, n = nums1.size(), m = nums2.size();
for (int offset = -m + 1; offset < n; offset++) {
if (n - offset < maxLen) break;
maxLen = max(maxLength(nums1, nums2, offset), maxLen);
}
return maxLen;
}
};
53. 最大子序和
动态规划:以此为末尾元素的最大子序和
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int curr = 0, max = INT_MIN, n = nums.size();
for (const int n : nums) {
curr = curr > 0 ? curr + n : n;
if (curr > max) max = curr;
}
return max;
}
};
贪心算法的解法参考[[10. 贪心算法(1)#53. 最大子序和]]
392. 判断子序列
双指针/动态规划:匹配时考虑下一个
class Solution {
public:
bool isSubsequence(string s, string t) {
int m = s.size(), n = t.size();
int i = 0, j = 0;
while (i < m && j < n) {
if (t[j] == s[i]) i++;
j++;
}
return i == m;
}
};
如果匹配串较多:可以统计每个位置之后各个字母首次出现的位置
115. 不同的子序列
动态规划:考虑前 i 个和前 j 个构成的字串的匹配数目
class Solution {
public:
int numDistinct(string s, string t) {
int n = s.size(), m = t.size();
if (n < m) return 0;
// s[0..i) and t[0..j)
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
for (int i = 0; i <= n; i++) dp[i][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= min(i, m); j++) {
dp[i][j] = dp[i - 1][j];
if (s[i - 1] == t[j - 1]) dp[i][j] += dp[i - 1][j - 1];
}
}
return dp[n][m];
}
};
583. 两个字符串的删除操作
动态规划:与编辑距离类似,去掉替换操作
class Solution {
public:
int minDistance(string word1, string word2) {
int n = word1.size(), m = word2.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
for (int i = 0; i <= n; i++) dp[i][0] = i;
for (int j = 1; j <= m; j++) dp[0][j] = j;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (word1[i - 1] == word2[j - 1])
dp[i][j] = dp[i - 1][j - 1];
else
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1;
}
}
return dp[n][m];
}
};
72. 编辑距离
动态规划:考虑前 i 个和前 j 个构成的字串的编辑距离
class Solution {
public:
int minDistance(string word1, string word2) {
int n = word1.size(), m = word2.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
for (int i = 0; i <= n; i++) dp[i][0] = i;
for (int j = 1; j <= m; j++) dp[0][j] = j;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (word1[i - 1] == word2[j - 1])
dp[i][j] = dp[i - 1][j - 1];
else
dp[i][j] = min(min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
}
}
return dp[n][m];
}
};
647. 回文子串
动态规划:由于只取决于对角线相邻元素,因此可以修改遍历方式,滚动数组优化
class Solution {
public:
int countSubstrings(string s) {
int count = 1, n = s.size();
int p, q;
for (int i = 1; i < n; i++) {
p = i, q = i;
do {
if (s[p] == s[q]) count++;
else break;
} while (--p >= 0 && ++q < n);
p = i - 1, q = i;
do {
if (s[p] == s[q]) count++;
else break;
} while (--p >= 0 && ++q < n);
}
return count;
}
};
回文子串还可以使用 Manacher 方法求解,利用已经探测到的中心对称信息来加速探测过程
516. 最长回文子序列
动态规划:考察 s[i..j] 子串的最长回文子序列,根据首尾字符是否相同进行转移
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n = s.size();
vector<vector<int>> dp(n, vector<int>(n));
for (int i = 0; i < n; i++) dp[i][i] = 1;
for (int i = n - 2; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
if (s[i] == s[j]) dp[i][j] = dp[i + 1][j - 1] + 2;
else dp[i][j] = max(dp[i][j - 1], dp[i + 1][j]);
}
}
return dp[0][n - 1];
}
};