专题训练之 DP
参考:
1.Leetcode1186[最大子数组变形]
class Solution { public: int maximumSum(vector<int>& arr) { int n = arr.size(); vector<vector<int>> dp(n, vector<int>(2, 0)); dp[0][0] = arr[0]; int res = arr[0]; for (int i = 1; i < n; i++) { dp[i][0] = max(arr[i], dp[i-1][0]+arr[i]); dp[i][1] = max(dp[i-1][1]+arr[i], dp[i-1][0]); res = max(res, max(dp[i][0], dp[i][1])); } return res; } };
子数组中最多可以去掉一个数,求最大子数组。需要维护两个不同的状态,表示到当前为止,是否去掉了一个数了。
class Solution { public: int rob(vector<int>& nums) { int n = nums.size(); if (n == 0) return 0; else if (n == 1) return nums[0]; else if (n == 2) return max(nums[0], nums[1]); vector<int> dp(n, 0); dp[0] = nums[0]; dp[1] = max(nums[0], nums[1]); for (int i = 2; i < n; i++) { dp[i] = max(dp[i-2]+nums[i], dp[i-1]); } return dp[n-1]; } };
* 选不选第 i 个位置的值
class Solution { public: int rob(vector<int>& nums) { int n = nums.size(); if (n == 0) return 0; else if (n == 1) return nums[0]; else if (n == 2) return max(nums[0], nums[1]); vector<int> dp(n, 0); dp[0] = nums[0]; dp[1] = max(nums[1], nums[0]); int res = max(dp[0], dp[1]); for (int i = 2; i < n-1; i++) { dp[i] = max(dp[i-1], dp[i-2]+nums[i]); res = max(res, dp[i]); } dp[1] = nums[1]; dp[2] = max(nums[1], nums[2]); for (int i = 3; i < n; i++) { dp[i] = max(dp[i-1], dp[i-2]+nums[i]); res = max(res, dp[i]); } return res; } };
上一题的变形,增加了一个条件:不能同时选取头/ 尾的数。计算两次,一次不选头,一次不选尾部。
4.Best Time to Buy and Sell Stock
A.Leetcode121:买一次,卖一次。贪心即可,维持当前访问到的数中的最小值
B.Leetcode122:可以多次交易,求利润最大。贪心即可,当访问的位置比前一个位置的值大时,便可加上其差值。
C.Leetcode123:可以参与两次交易,求利润最大。暴力一点的做法是枚举的分割给定的数组,将其分为两部分,然后求各自的最大,将结果相加,不断更新这个结果。维护两个数组,dp1[i] 表示从 [0, i] 这个范围内进行一次交易最大的利润,问题简化成 leetcode121。dp2[i] 表示从 [i, n-1] 这个范围内的最大的利润。然后从头到尾扫一遍,枚举分割点。
class Solution { public: int maxProfit(vector<int>& prices) { int n = prices.size(); if (n < 2) return 0; vector<int> dp1(n, 0), dp2(n, 0); int minx = prices[0]; int ans = 0; for (int i = 1; i < n; i++) { int val = prices[i] - minx; dp1[i] = max(dp1[i-1], val); minx = min(minx, prices[i]); ans = max(ans, dp1[i]); } int maxx = prices[n-1]; for (int i = n-2; i > 0; i--) { int val = maxx - prices[i]; dp2[i] = max(dp2[i+1], val); maxx = max(maxx, prices[i]); ans = max(ans, dp2[i] + dp1[i-1]); } return ans; } };
D.Leetcode188:最多进行 K 次交易,求利润最大。设置两个数组,buy[i] 表示第 i 次买时,当前最大的利润是多少。sell[i] 表示第 i 次卖时,当前最大的利润是多少。buy[i] = max(buy[i], sell[i-1]-nums[j]),sell[i] = max(sell[i], buy[i]+nums[j])。
class Solution { public: int maxProfit(int k, vector<int>& nums) { int n = nums.size(), ans = 0; if (k >= n/2) { for (int i = 1; i < n; i++) { if (nums[i] > nums[i-1]) ans += nums[i]-nums[i-1]; } return ans; } vector<int> buy(k+1, INT_MIN), sell(k+1, 0); for (int i = 0; i < n; i++) { for (int j = 1; j <= k; j++) { buy[j] = max(buy[j], sell[j-1]-nums[i]); sell[j] = max(sell[j], buy[j]+nums[i]); } } return sell[k]; } };
E.Leetcode309:可以进行任意次数的交易,但是存在冷冻期,当你在第 i 天卖出手头的股票,不能立刻在第 i+1 天买入新的股票。设置两个数组 buy[i] 表示在 [0, i] 的范围内,手上含有股票的最大利润是多少。 sell[i] 表示在 [0, i] 的范围内,手上没有股票的最大的利润是多少。sell[i] = max(sell[i-1], max(buy[j] + prices[i]))(其中 j < i)。buy[i] = max(buy[i-1], sell[i-1]+prices[i])。
1 class Solution { 2 public: 3 int maxProfit(vector<int>& prices) { 4 int n = prices.size(); 5 if (n == 0) return 0; 6 vector<int> buy(n+1, INT_MIN), sell(n, 0); 7 for (int i = 0; i < n; i++) { 8 if (i != 0) sell[i] = sell[i-1]; 9 for (int j = i-1; j >= 0; j--) { 10 sell[i] = max(sell[i], buy[j] + prices[i]); 11 } 12 if (i == 0) buy[i] = -prices[i]; 13 else if (i == 1) buy[i] = max(-prices[i], -prices[i-1]); 14 else buy[i] = max(buy[i-1], sell[i-2] - prices[i]); 15 } 16 return sell[n-1]; 17 } 18 };
class Solution { public: vector<int> maxSumOfThreeSubarrays(vector<int>& nums, int k) { int n = nums.size(); vector<int> sum(n+1, 0), dp(3, 0), pos[3]; int sm = 0; for (int i = 0; i < n; i++) { sm += nums[i]; if (i >= k-1) { sum[i] = sm; sm -= nums[i-k+1]; if (i >= 3*k-1) { if (sum[i-2*k] > dp[0]) { dp[0] = sum[i-2*k]; pos[0] = {i-2*k}; } if (sum[i-k] + dp[0] > dp[1]) { dp[1] = sum[i-k] + dp[0]; pos[1] = {pos[0][0], i-k}; } if (sum[i] + dp[1] > dp[2]) { dp[2] = sum[i] + dp[1]; pos[2] = {pos[1][0], pos[1][1], i}; } } } } return {pos[2][0]-k+1,pos[2][1]-k+1,pos[2][2]-k+1}; } };
给定一个数组,从数组中取出三个不相交,且各自元素数目为 k 的子数组,求三个子数组的最大和。
设置 sum[i],表示在 [i-k+1, i] 这个范围内子数组的和。dp[0] 表示一个满足条件的子数组的最大和,对应 pos[0] 表示 dp[0] 所对应子数组的位置.
class Solution { public: int lengthOfLIS(vector<int>& nums) { vector<int> dp; int n = nums.size(); for (int i = 0; i < n; i++) { int num = nums[i]; auto iter = lower_bound(dp.begin(), dp.end(), num); if (iter == dp.end()) dp.push_back(num); else if (num < *iter) *iter = num; } return dp.size(); } };
求最长上升子串
解法1(time: N^2):dp[i] 表示以 i 结尾满足条件的最长上升子串。dp[i] = max(dp[i], dp[j]+1) (0<=j<i && nums[j] < nums[i])
解法2(time: Nlog(n)):用 dp 数组保存满足条件的最长上升子串。i 从 0 开始访问,若 dp 数组中没有比 nums[i] 大的元素,则将 nums[i] 加入到 dp 数组的尾部。若有,则找到第一个比 nums[i] 大的元素,用 nums[i] 进行替换。最后 dp.size() 便是所求的答案
class Solution { public: vector<int> largestDivisibleSubset(vector<int>& nums) { vector<int> res; int n = nums.size(); if (n == 0) return res; sort(nums.begin(), nums.end()); vector<int> dp(n, 1), pre(n); for (int i = 0; i < n; i++) { pre[i] = i; for (int j = i-1; j >= 0; j--) { if (nums[i] % nums[j] == 0 && dp[j]+1>dp[i]) { dp[i] = dp[j]+1; pre[i] = j; } } } int pos, ans = 0; for (int i = 0; i < n; i++) { if (dp[i] > ans) { ans = dp[i]; pos = i; } } while (pre[pos] != pos) { res.push_back(nums[pos]); pos = pre[pos]; } res.push_back(nums[pos]); reverse(res.begin(), res.end()); return res; } };
求满足条件的最长子串
思路同上一题的解法1
class Solution { public: int findNumberOfLIS(vector<int>& nums) { int n = nums.size(); if (n == 0) return 0; vector<int> dp(n, 1), cnt(n, 1); for (int i = 0; i < n; i++) { for (int j = i-1; j >= 0; j--) { if (nums[i] > nums[j]) { if (dp[i] == dp[j]+1) cnt[i] += cnt[j]; else if (dp[i] < dp[j]+1) { dp[i] = dp[j] + 1; cnt[i] = cnt[j]; } } } } int ans = 0, res = 0; for (int i = 0; i < n; i++) { if (dp[i] > ans) { ans = dp[i]; res = cnt[i]; } else if (dp[i] == ans) { res += cnt[i]; } } return res; } };
第 6 题得到变形,求最长上升子串的个数。在第 6 题的基础上需要额外记录数量。
class Solution { public: int minHeightShelves(vector<vector<int>>& books, int shelf_width) { int n = books.size(); vector<int> dp(n+1, INT_MAX); dp[0] = 0; dp[1] = books[0][1]; for (int i = 1; i < n; i++) { int maxx = books[i][1]; int width = books[i][0]; dp[i+1] = dp[i] + maxx; for (int j = i-1; j >= 0; j--) { if (width+books[j][0] > shelf_width) break; maxx = max(maxx, books[j][1]); width += books[j][0]; dp[i+1] = min(dp[i+1], dp[j] + maxx); } } return dp[n]; } };
按顺序将书放入书架,给定书架的宽度,求书架的最小高度。
dp[i] 表示考虑了前 i 本书,满足条件时书架的最小高度。dp[i] = min(dp[i], dp[j] + maxx) (表示从 [j+1, i] 位置的书放到新的一层,maxx 表示这些书中最高的那个值)
10.Leetcode1143
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 = 0; i < n; i++) { for (int j = 0; j < m; j++) { if (text1[i] == text2[j]) { dp[i+1][j+1] = max(dp[i+1][j+1], dp[i][j]+1); } else { dp[i+1][j+1] = max(dp[i+1][j+1], max(dp[i+1][j], dp[i][j+1])); } } } return dp[n][m]; } };
给定两个字符串,求最长公共子串。
11.Leetcode1092
class Solution { public: string shortestCommonSupersequence(string& A, string& B) { int i = 0, j = 0; string res = ""; for (char c : lcs(A, B)) { while (A[i] != c) res += A[i++]; while (B[j] != c) res += B[j++]; res += c, i++, j++; } return res + A.substr(i) + B.substr(j); } string lcs(string& A, string& B) { int n = A.size(), m = B.size(); vector<vector<string>> dp(n + 1, vector<string>(m + 1, "")); for (int i = 0; i < n; ++i) for (int j = 0; j < m; ++j) if (A[i] == B[j]) dp[i + 1][j + 1] = dp[i][j] + A[i]; else dp[i + 1][j + 1] = dp[i + 1][j].size() > dp[i][j + 1].size() ? dp[i + 1][j] : dp[i][j + 1]; return dp[n][m]; } };
给定两个字符串,求一个长度最短的字符串,使得给定的两个字符串为该字符串的子串。
类似于上面的方法,先求出最长的公共子串,然后补足剩下没有被公共子串所涵盖的部分。
12.Leetcode72
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 = 1; i <= m; i++) dp[0][i] = i; for (int i = 1; i <= n; i++) dp[i][0] = i; for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { if (word1[i] == word2[j]) dp[i+1][j+1] = dp[i][j]; else { dp[i+1][j+1] = min(min(dp[i+1][j]+1, dp[i][j+1]+1), dp[i][j]+1); } } } return dp[n][m]; } };
只能进行单个字符的增加、删除和替换,求把字符串 word1 变成字符串 word2 最少需要几次操作
设 dp[i][j] 表示 word1[0, i) -> word2[0, j) 最少需要的变换次数
13.Leetcode115
class Solution { public: int numDistinct(string s, string t) { int n = s.size(), m = t.size(); vector<vector<long long>> dp(n+1, vector<long long>(m+1, 0)); for (int i = 0; i <= n; i++) dp[i][0] = 1; for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { if (s[i] == t[j]) { dp[i+1][j+1] = dp[i][j] + dp[i][j+1]; } else { dp[i+1][j+1] = dp[i][j+1]; } } } return (int)dp[n][m]; } };
class Solution { public: int numDistinct(string s, string t) { int m = t.size(); vector<long long> dp(m, 0); for (auto c : s) { for (int i = m-1; i >= 0; i--) { if (c == t[i]) { dp[i] = (i > 0? dp[i-1]: 1) + dp[i]; } } } return (int)dp[m-1]; } };
题意:给定字符串 s 和 t,求 s 中有多少个子串和 t 相等
解法1:设置 dp[i][j] 表示 s[0, i) 部分的子串和 t[0, j) 相等的个数。当 s[i] == t[j] 时,意味着 s[i] 这个位置的字符可留可不留,此时 dp[i][j] = dp[i-1][j] + dp[i-1][j-1]。
当 s[i] != t[j] 时,意味着 s[i] 这个位置一定不留,此时 dp[i][j] = dp[i-1][j]。初始化时 dp[i][0] = 1,即空串是所有串的子串
14.Leetcode1035
class Solution { public: int maxUncrossedLines(vector<int>& A, vector<int>& B) { int n = A.size(), m = B.size(); vector<vector<int>> dp(n+1, vector<int>(m+1, 0)); for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { if (A[i] == B[j]) { dp[i+1][j+1] = dp[i][j] + 1; } else { dp[i+1][j+1] = max(dp[i+1][j], dp[i][j+1]); } } } return dp[n][m]; } };
最长公共子串
15.Leetcode1312
class Solution { public: int minInsertions(string s) { int n = s.size(); vector<vector<int>> dp(n, vector<int>(n, 0)); for (int len = 2; len <= n; len++) { for (int i = 0, j = i+len-1; j < n; i++, j++) { if (s[i] == s[j]) { dp[i][j] = dp[i+1][j-1]; } else { dp[i][j] = min(dp[i+1][j]+1, dp[i][j-1]+1); } } } return dp[0][n-1]; } };
设置 dp[i][j] 表示使得 s[i, j] 满足回文串条件最少需要插入的数量。if (s[i] == s[j]) dp[i][j] = dp[i+1][j-1]; else dp[i][j] = min(dp[i+1][j]+1, dp[i][j-1]+1)
16.Leetcode712
class Solution { public: int minimumDeleteSum(string s1, string s2) { int n = s1.size(), m = s2.size(); vector<vector<int>> dp(n+1, vector<int>(m+1)); for (int i = 1; i <= n; i++) { dp[i][0] = dp[i-1][0] + s1[i-1]; } for (int i = 1; i <= m; i++) { dp[0][i] = dp[0][i-1] + s2[i-1]; } for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { if (s1[i] == s2[j]) dp[i+1][j+1] = dp[i][j]; else { int val1 = s1[i], val2 = s2[j]; dp[i+1][j+1] = min(dp[i+1][j]+val2, dp[i][j+1]+val1); } } } return dp[n][m]; } };
删除两个字符串中的一些字符,使得两个字符串一致。求最小删除字符的 ASCII 之和。最长公共子串变形
17.Leetcode1278
class Solution { public: int palindromePartition(string s, int K) { int n = s.size(); vector<vector<int>> pd(n, vector<int>(n)), dp(n, vector<int>(K+1)); for (int len = 2; len <= n; len++) { for (int i = 0; i < n; i++) { int j = i+len-1; if (j >= n) break; pd[i][j] = pd[i+1][j-1]; if (s[i] != s[j]) pd[i][j]++; } } for (int i = 0; i < n; i++) dp[i][1] = pd[0][i]; for (int k = 2; k <= K; k++) { for (int i = k-1; i < n; i++) { dp[i][k] = i+1; for (int j = i-1; j >= k-2; j--) { dp[i][k] = min(dp[i][k], dp[j][k-1]+pd[j+1][i]); } } } return dp[n-1][K]; } };
pd[i][j] 表示将字符串 s[i: j] 变成回文串最少需要改变的字符数。dp[i][k] 表示将字符串前 i 位分成 k 份满足条件的子串最少需要改变的字符。
if (s[i] == s[j]) pd[i][j] = pd[i+1][j-1]; else pd[i][j] = pd[i+1][j-1]+1;
dp[i][k] = min(dp[i][k], dp[j][k-1]+pd[j+1][i])
18.Leetcode813
class Solution { public: double largestSumOfAverages(vector<int>& A, int K) { int n = A.size(); vector<int> sum(n); sum[0] = A[0]; for (int i = 1; i < n; i++) sum[i] = sum[i-1]+A[i]; vector<vector<double>> avg(n, vector<double>(n)); for (int i = 0; i < n; i++) avg[i][i] = sum[i]; for (int i = 1; i < n; i++) avg[0][i] = 1.0*sum[i]/(i+1); for (int i = 1; i < n; i++) { for (int j = i-1; j >= 0; j--) { avg[j+1][i] = 1.0*(sum[i]-sum[j])/(i-j); } } vector<vector<double>> dp(n, vector<double>(K+1)); for (int i = 0; i < n; i++) dp[i][1] = avg[0][i]; for (int k = 2; k <= K; k++) { for (int i = k-1; i < n; i++) { for (int j = 0; j < i; j++) { dp[i][k] = max(dp[i][k], dp[j][k-1]+avg[j+1][i]); } } } return dp[n-1][K]; } };
类似于上一题。先求出 avg[i][j] 表示数组 [i, j] 范围内的平均值。接着利用和上题一样的思路即可求出
19.Leetcode1335
class Solution { public: int minDifficulty(vector<int>& A, int d) { int n = A.size(); if (n < d) return -1; vector<vector<int>> pd(n, vector<int>(n)); for (int i = 0; i < n; i++) pd[i][i] = A[i]; for (int len = 2; len <= n; len++) { for (int i = 0; i < n; i++) { int j = i+len-1; if (j >= n) break; pd[i][j] = max(pd[i+1][j], pd[i][j-1]); } } vector<vector<int>> dp(n, vector<int>(d+1)); for (int i = 0; i < n; i++) dp[i][1] = pd[0][i]; for (int k = 2; k <= d; k++) { for (int i = k-1; i < n; i++) { dp[i][k] = INT_MAX; for (int j = k-2; j < i; j++) { dp[i][k] = min(dp[i][k], dp[j][k-1]+pd[j+1][i]); } } } return dp[n-1][d]; } };
类似于上一题。先求出 pd[i][j] 表示数组 [i, j] 范围内的最大值。接着利用和上题一样的思路即可求出
20.Leetcode516
class Solution { public: int longestPalindromeSubseq(string s) { int n = s.size(); vector<vector<int>> dp(n, vector<int>(n, 0)); for (int i = 0; i < n; i++) dp[i][i] = 1; for (int len = 2; len <= n; len++) { for (int i = 0; i < n; i++) { int j = i+len-1; if (j >= n) break; if (s[i] == s[j]) dp[i][j] = dp[i+1][j-1]+2; else dp[i][j] = max(dp[i+1][j], dp[i][j-1]); } } return dp[0][n-1]; } };
区间 DP. if (s[i] == s[j]) dp[i][j] = dp[i+1][j-1]+2; else dp[i][j] = max(dp[i+1][j], dp[i][j-1]);
21.Leetcode312
class Solution { public: int maxCoins(vector<int>& nums) { int n = nums.size(); vector<int> newNums(n+2, 1); for (int i = 1; i <= n; i++) { newNums[i] = nums[i-1]; } vector<vector<int>> dp(n+2, vector<int>(n+2)); for (int i = 1; i <= n; i++) { dp[i][i] = newNums[i-1] * newNums[i] * newNums[i+1]; } for (int len = 2; len <= n; len++) { for (int i = 1; i <= n; i++) { int j = i+len-1; if (j > n) break; dp[i][j] = max(newNums[i-1]*newNums[i]*newNums[j+1]+dp[i+1][j], dp[i][j-1]+newNums[i-1]*newNums[j]*newNums[j+1]); for (int k = i+1; k < j; k++) { dp[i][j] = max(dp[i][j], dp[i][k-1]+newNums[i-1]*newNums[k]*newNums[j+1]+dp[k+1][j]); } } } return dp[1][n]; } };
题意:给定一组整数,按任意的顺序删掉一个数直到删掉了所有的数。删掉一个数所得到的分数是其当前左右两边的数和自己的成绩。求最后最大的分数是多少
设置 dp[i][j] 表示在数组 [i, j] 的范围内,满足条件最大的分数是多少。当确定了 i 和 j 的值后,需要枚举 k (i <= k <= j) 表示在区间 [i, j] 范围内最后一个删掉的数是什么
22.Leetcode375
class Solution { public: int getMoneyAmount(int n) { vector<vector<int>> dp(n+1, vector<int>(n+1)); for (int len = 2; len <= n; len++) { for (int i = 1; i+len-1 <= n; i++) { int j = i+len-1; dp[i][j] = min(i+dp[i+1][j], j+dp[i][j-1]); for (int k = i+1; k < j; k++) { dp[i][j] = min(dp[i][j], k+max(dp[i][k-1], dp[k+1][j])); } } } return dp[1][n]; } };
和上一题类似。关键点在于 k 代表第一次猜哪个位置的值,dp[i][j] = min(dp[i][j], k + max(dp[i][k-1], dp[k+1][j]));
23.Leetcode1000
class Solution { public: int mergeStones(vector<int>& stones, int K) { int n = stones.size(); if ((n - 1) % (K - 1) != 0) return -1; vector<int> sums(n + 1); vector<vector<int>> dp(n, vector<int>(n)); for (int i = 1; i < n + 1; ++i) { sums[i] = sums[i - 1] + stones[i - 1]; } for (int len = K; len <= n; ++len) { for (int i = 0; i + len <= n; ++i) { int j = i + len - 1; dp[i][j] = INT_MAX; for (int t = i; t < j; t += K - 1) { dp[i][j] = min(dp[i][j], dp[i][t] + dp[t + 1][j]); } if ((j - i) % (K - 1) == 0) { dp[i][j] += sums[j + 1] - sums[i]; } } } return dp[0][n - 1]; } };
详细解释见:https://leetcode.com/problems/minimum-cost-to-merge-stones/discuss/247567/JavaC%2B%2BPython-DP