目录
1--动态规划理论基础
动态规划经典问题:① 背包问题;② 打家劫舍;③ 股票问题; ④ 子序列问题;
动态规划五部曲:
① 确定 dp 数组及其下标的含义;
② 确定递推公式;
③ 确定 dp 数组的初始化;
④ 确定遍历顺序,一般为从左到右;
⑤ 打印 dp 数组,一般用于检查;
2--斐波那契数
主要思路:
经典动态规划,dp[i] 表示第 i 个数的斐波那契数;递推公式为:dp[i] = dp[i-1] + dp[i - 2];初始化dp[0] = 1,dp[1] = 1;遍历顺序从左到右;
#include <iostream>
#include <vector>
class Solution {
public:
int fib(int n) {
if(n <= 1) return n;
std::vector<int> dp(n+1, 0);
dp[0] = 0; dp[1] = 1;
for(int i = 2; i <= n; i++){
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
int main(int argc, char argv[]){
int test = 2;
Solution S1;
int res = S1.fib(test);
std::cout << res << std::endl;
return 0;
}
3--爬楼梯
主要思路:
经典动态规划,dp[i] 表示到达第 i 阶楼梯的方法数;递推公式为 dp[i] = dp[i - 1] + dp[i - 2];初始化dp[1] = 1, dp[2] = 2;遍历顺序从左到右;
#include <iostream>
#include <vector>
class Solution {
public:
int climbStairs(int n) {
if(n <= 2) return n;
std::vector<int> dp(n+1, 0);
dp[1] = 1, dp[2] = 2;
for(int i = 3; i <= n; i++){
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
int main(int argc, char argv[]){
int n = 2;
Solution S1;
int res = S1.climbStairs(n);
std::cout << res << std::endl;
return 0;
}
4--使用最小花费爬楼梯
主要思路:
dp[i] 表示到达第 i 阶楼梯需要的最小花费;初始化 dp[0] = 0, dp[1] = 0,因为可以从 0 和 1 出发,因此不需要花费;递推公式为 dp[i] = std::min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]);默认从左到右遍历;
#include <iostream>
#include <vector>
class Solution {
public:
int minCostClimbingStairs(std::vector<int>& cost) {
std::vector<int> dp(cost.size()+1, 0); // 到达第i阶的最小花费
dp[0] = 0;
dp[1] = 0;
for(int i = 2; i <= cost.size(); i++){
dp[i] = std::min(cost[i-2]+dp[i-2], cost[i-1]+dp[i-1]);
}
return dp[cost.size()];
}
};
int main(int argc, char argv[]){
// cost = [1,100,1,1,1,100,1,1,100,1]
std::vector<int> cost = {1, 100, 1, 1, 1, 100, 1, 1, 100, 1};
Solution S1;
int res = S1.minCostClimbingStairs(cost);
std::cout << res << std::endl;
return 0;
}
5--不同路径
主要思路:
dp[i][j] 表示到达 (i, j) 位置的路径数,初始化两个边界 dp[i][0] = 1,dp[0][j] = 1;状态转移方程为:dp[i][j] = dp[i-1][j] + dp[i][j-1];遍历顺序为从上到下,从左到右;
#include <iostream>
#include <vector>
class Solution {
public:
int uniquePaths(int m, int n) {
std::vector<std::vector<int>> dp(m, std::vector<int>(n, 0));
// 初始化
for(int i = 0; i < m; i++) dp[i][0] = 1;
for(int j = 0; j < n; j++) dp[0][j] = 1;
// 遍历
for(int r = 1; r < m; r++){
for(int c = 1; c < n; c++){
dp[r][c] = dp[r-1][c] + dp[r][c-1];
}
}
return dp[m-1][n-1];
}
};
int main(int argc, char argv[]){
// m = 3, n = 7
int m = 3, n = 7;
Solution S1;
int res = S1.uniquePaths(m, n);
std::cout << res << std::endl;
return 0;
}
6--不同路径II
主要思路:
与上题类似,dp[i][j] 表示到达 (i, j) 位置的路径数,初始化两个边界 dp[i][0] = 1,dp[0][j] = 1(确保边界没有障碍物,有障碍物初始化为0);状态转移方程为:(i, j)不是障碍物则 dp[i][j] = dp[i-1][j] + dp[i][j-1],(i, j)为障碍物则dp[i][j] = 0;遍历顺序为从上到下,从左到右;
#include <iostream>
#include <vector>
class Solution {
public:
int uniquePathsWithObstacles(std::vector<std::vector<int>>& obstacleGrid) {
int m = obstacleGrid.size(), n = obstacleGrid[0].size();
if(obstacleGrid[m-1][n-1] == 1) return 0;
std::vector<std::vector<int>> dp(m, std::vector<int>(n, 0));
// 初始化
for(int i = 0; i < m && obstacleGrid[i][0] != 1; i++) dp[i][0] = 1;
for(int j = 0; j < n && obstacleGrid[0][j] != 1; j++) dp[0][j] = 1;
// 遍历
for(int r = 1; r < m; r++){
for(int c = 1; c < n; c++){
if(obstacleGrid[r][c] == 1) dp[r][c] = 0; // 障碍
else dp[r][c] = dp[r-1][c] + dp[r][c-1];
}
}
return dp[m-1][n-1];
}
};
int main(int argc, char argv[]){
// obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
std::vector<std::vector<int>> test = {{0, 0, 0}, {0, 1, 0}, {0, 0, 0}};
Solution S1;
int res = S1.uniquePathsWithObstacles(test);
std::cout << res << std::endl;
return 0;
}
7--整数拆分
主要思路:
dp[i] 表示整数 i 拆分后的最大乘积;初始化dp[0] = 0, dp[1] = 0, dp[2] = 1; 状态转移方程为:dp[i] = max(dp[i], max(j*(i-j), j * dp[i-j]));
#include <iostream>
#include <vector>
class Solution {
public:
int integerBreak(int n) {
std::vector<int> dp(n+1, 0);
// 初始化
dp[0] = 0, dp[1] = 0, dp[2] = 1;
// 遍历
for(int i = 3; i <= n; i++){
for(int j = 0; j <= i/2; j++){
dp[i] = std::max(dp[i], std::max(j*(i-j), j * dp[i-j]));
}
}
return dp[n];
}
};
int main(int argc, char argv[]){
// n = 10
int test = 10;
Solution S1;
int res = S1.integerBreak(test);
std::cout << res << std::endl;
return 0;
}
8--不同的二叉搜索树
主要思路:
dp[i] 表示由 i 个数字构成的不同二叉搜索树的数目;
初始化:dp[0] = 1;
状态转移方程:dp[i] += dp[j-1] * dp[i-j],0 <= j <= i;其中 dp[j-1] 来构成 j 的左子树,dp[i-j] 来构成 j 的右子树;
遍历顺序:两个 for 循环,第 1 个 for 循环遍历 1 - n,第二个 for 循环遍历 1 - i;
#include <iostream>
#include <vector>
class Solution {
public:
int numTrees(int n) {
std::vector<int> dp(n+1, 0);
// 初始化
dp[0] = 1;
// 遍历
for(int i = 1; i <= n; i++){
for(int j = 1; j <= i; j++){
dp[i] += dp[j-1] * dp[i-j];
}
}
return dp[n];
}
};
int main(int argc, char *argv[]) {
// n = 3
int test = 3;
Solution S1;
int res = S1.numTrees(test);
std::cout << res << std::endl;
return 0;
}
9--背包问题
10--打家劫舍
主要思路:
基于动态规划,dp[i]表示截止到坐标 i 时,能够偷到的最高金额;
初始化 dp[0] = nums[0], dp[1] = max(nums[0], nums[1]);
状态转移方程:dp[i] = max(nums[i] + dp[i-2], dp[i-1]);dp[i-2] + nums[i] 表示偷索引为 i 的房屋,dp[i-1] 表示不偷索引为 i 的房屋;
#include <iostream>
#include <vector>
class Solution {
public:
int rob(std::vector<int>& nums) {
if(nums.size() == 1) return nums[0];
std::vector<int> dp(nums.size(), 0);
dp[0] = nums[0], dp[1] = std::max(nums[0], nums[1]);
for(int i = 2; i < nums.size(); i++){
dp[i] = std::max(dp[i-2] + nums[i], dp[i - 1]);
}
return dp[nums.size() - 1];
}
};
int main(int argc, char argv[]){
std::vector<int> test = {1, 2, 3, 1};
Solution S1;
int res = S1.rob(test);
std::cout << res << std::endl;
return 0;
}
11--打家劫舍II
主要思路:
本题房屋围成一圈,因此首尾房屋不能同时打劫,将本题划分为三种情况:① 不打劫首尾房屋;② 考虑打劫首房屋,则不能打劫尾房屋;③ 考虑打劫尾房屋,则不能打劫首房屋;
#include <iostream>
#include <vector>
class Solution {
public:
int rob(std::vector<int>& nums) {
if(nums.size() == 1) return nums[0];
// 不考虑首尾房屋
std::vector<int> nums1(nums.begin() + 1, nums.end() - 1);
// 考虑首房屋
std::vector<int> nums2(nums.begin(), nums.end() - 1);
// 考虑尾房屋
std::vector<int> nums3(nums.begin() + 1, nums.end());
return std::max(std::max(dp_solution(nums1), dp_solution(nums2)), dp_solution(nums3));
}
int dp_solution(std::vector<int>& nums){ // 打家劫舍I的代码
if(nums.size() == 0) return 0;
if(nums.size() == 1) return nums[0];
std::vector<int> dp(nums.size(), 0);
dp[0] = nums[0], dp[1] = std::max(nums[0], nums[1]);
for(int i = 2; i < nums.size(); i++){
dp[i] = std::max(dp[i-2] + nums[i], dp[i - 1]);
}
return dp[nums.size() - 1];
}
};
int main(int argc, char argv[]){
std::vector<int> test = {2, 3, 2};
Solution S1;
int res = S1.rob(test);
std::cout << res << std::endl;
return 0;
}
12--打家劫舍III
主要思路:
基于二叉树递归(后序遍历)和动态规划,每个节点拥有两个状态,dp[0] 表示不打劫当前节点的最高金额,dp[1] 表示打劫当前节点的最高金额;
对于每个节点 cur,dp[1] = dp_left[0] + dp_right[0] + cur->val,即打劫当前节点时,不能打劫左孩子和右孩子;
dp[0] = std::max(dp_left[0], dp_left[1]) + std::max(dp_right[0], dp_right[1]);不打劫当前节点时,可以打劫也可以不打劫左右孩子,取两种情况的最大值;
#include <iostream>
#include <vector>
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
int rob(TreeNode* root) {
std::vector<int> res = dfs(root);
return std::max(res[0], res[1]);
}
std::vector<int> dfs(TreeNode* cur){
if(cur == nullptr) return {0, 0};
std::vector<int> dp_left = dfs(cur->left);
std::vector<int> dp_right = dfs(cur->right);
int dp_cur0 = std::max(dp_left[0], dp_left[1]) + std::max(dp_right[0], dp_right[1]);
int dp_cur1 = dp_left[0] + dp_right[0] + cur->val;
return {dp_cur0, dp_cur1};
}
};
int main(int argc, char argv[]){
// root = [3, 2, 3, null, 3, null, 1]
TreeNode *Node1 = new TreeNode(3);
TreeNode *Node2 = new TreeNode(2);
TreeNode *Node3 = new TreeNode(3);
TreeNode *Node4 = new TreeNode(3);
TreeNode *Node5 = new TreeNode(1);
Node1->left = Node2;
Node1->right = Node3;
Node2->right = Node4;
Node3->right = Node5;
Solution S1;
int res = S1.rob(Node1);
std::cout << res << std::endl;
return 0;
}
13--买卖股票的最佳时机
主要思路:
每一天都有两个状态:即持有股票和不持有股票;
dp[i][0] 表示第 i 天持有股票时的最大金额,dp[i][1] 表示第 i 天不持有股票时的最大金额;
状态转移方程:dp[i][0] = max(dp[i-1][0], 0-prices[i]);dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[1]);
初始化 dp[0][0] = -prices[0], dp[i][1] = 0;
#include <iostream>
#include <vector>
class Solution {
public:
int maxProfit(std::vector<int>& prices) {
std::vector<std::vector<int>> dp(prices.size(), std::vector<int>(2, 0));
dp[0][0] = -prices[0], dp[0][1] = 0;
for(int i = 1; i < prices.size(); i++){
dp[i][0] = std::max(dp[i-1][0], -prices[i]);
dp[i][1] = std::max(dp[i-1][1], dp[i-1][0] + prices[i]);
}
return dp[prices.size() - 1][1];
}
};
int main(int argc, char argv[]){
// [7, 1, 5, 3, 6, 4]
std::vector<int> test = {7, 1, 5, 3, 6, 4};
Solution S1;
int res = S1.maxProfit(test);
std::cout << res << std::endl;
return 0;
}
14--买卖股票的最佳时机II
主要思路:
每一天都有两个状态:即持有股票和不持有股票;
dp[i][0] 表示第 i 天持有股票时的最大金额,dp[i][1] 表示第 i 天不持有股票时的最大金额;
状态转移方程:dp[i][0] = max(dp[i-1][0], dp[i-1][1]-prices[i]);dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[1]);
由于可以多次买卖,则第 i 天持有股票的状态可能由第 i-1 天不持有股票,在第 i 天买入股票推导而得;
初始化dp[0][0] = -prices[0],dp[0][1] = 0;
#include <iostream>
#include <vector>
class Solution {
public:
int maxProfit(std::vector<int>& prices) {
std::vector<std::vector<int>> dp(prices.size(), std::vector<int>(2, 0));
dp[0][0] = -prices[0], dp[0][1] = 0;
for(int i = 1; i < prices.size(); i++){
dp[i][0] = std::max(dp[i-1][0], dp[i-1][1] - prices[i]);
dp[i][1] = std::max(dp[i-1][1], dp[i-1][0] + prices[i]);
}
return dp[prices.size() - 1][1];
}
};
int main(int argc, char argv[]){
// [7, 1, 5, 3, 6, 4]
std::vector<int> test = {7, 1, 5, 3, 6, 4};
Solution S1;
int res = S1.maxProfit(test);
std::cout << res << std::endl;
return 0;
}
15--买卖股票的最佳时机III
主要思路:
本题最多只能买卖两次,则对于每一天,会有以下五种状态:dp[i][0] 表示第 i 天不进行任何操作,dp[i][1] 表示第 i 天第一次买入,dp[i][2] 表示第 i 天第一次卖出,dp[i][3] 表示第 i 天第二次买入,dp[i][4] 表示第 i 天第二次卖出;
状态转移方程:dp[i][0] = dp[i-1][0],dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i]);
dp[i][2] = max(dp[i-1][2], dp[i-1][1] + prices[i]);
dp[i][3] = max(dp[i-1][3], dp[i-1][2] - prices[i]);
dp[i][4] = max(dp[i-1][4], dp[i-1][3] + prices[i]);
初始化:dp[0][0] = 0,dp[0][1] = -prices[0],dp[0][2] = 0,dp[0][3] = -prices[0],dp[0][4] = 0;
#include <iostream>
#include <vector>
class Solution {
public:
int maxProfit(std::vector<int>& prices) {
std::vector<std::vector<int>> dp(prices.size(), std::vector<int>(5, 0));
dp[0][0] = 0, dp[0][1] = -prices[0], dp[0][2] = 0, dp[0][3] = -prices[0], dp[0][4] = 0;
for(int i = 1; i < prices.size(); i++){
dp[i][0] = dp[i-1][0];
dp[i][1] = std::max(dp[i-1][1], dp[i-1][0] - prices[i]);
dp[i][2] = std::max(dp[i-1][2], dp[i-1][1] + prices[i]);
dp[i][3] = std::max(dp[i-1][3], dp[i-1][2] - prices[i]);
dp[i][4] = std::max(dp[i-1][4], dp[i-1][3] + prices[i]);
}
return dp[prices.size() - 1][4];
}
};
int main(int argc, char argv[]){
// [3, 3, 5, 0, 0, 3, 1, 4]
std::vector<int> test = {3, 3, 5, 0, 0, 3, 1, 4};
Solution S1;
int res = S1.maxProfit(test);
std::cout << res << std::endl;
return 0;
}
16--买卖股票的最佳时机IV
主要思路:
类似于上题,上题中最多可以买卖两次,对应五种状态;本题最多可以买卖k次,对应2K+1种状态。
状态转移方程与初始化类似于上题。
#include <iostream>
#include <vector>
class Solution {
public:
int maxProfit(int k, std::vector<int>& prices) {
std::vector<std::vector<int>> dp(prices.size(), std::vector<int>(2*k+1, 0));
// 初始化
for(int j = 1; j < 2*k+1; j += 2){
dp[0][j] = -prices[0]; // 持有
dp[0][j+1] = 0; // 不持有
}
// 递推
for(int i = 1; i < prices.size(); i++){
for(int j = 1; j < 2*k+1; j += 2){
dp[i][j] = std::max(dp[i-1][j], dp[i][j - 1] - prices[i]); // 持有
dp[i][j+1] = std::max(dp[i-1][j+1], dp[i][j] + prices[i]); // 不持有
}
}
return dp[prices.size() - 1][2*k];
}
};
int main(int argc, char *argv[]) {
// k = 2, prices = [2,4,1]
int k = 2;
std::vector<int> prices = {2, 4, 1};
Solution S1;
int res = S1.maxProfit(k, prices);
std::cout << res << std::endl;
}
17--买卖股票的最佳时机含冷冻期
主要思路:
将第 i 天拆分为四种状态:dp[i][0] 表示持有股票,dp[i][1] 表示保持卖出股票状态(冷冻期的下一天),dp[i][2] 表示卖出股票,dp[i][3] 表示冷冻期(卖出股票的下一天)。
状态转移方程:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i], dp[i-1][3] - prices[i]);
dp[i][1] = max(dp[i-1][1], dp[i-1][3]);
dp[i][2] = dp[i-1][0] + prices[i];
dp[i][3] = dp[i-1][2]
初始化:
dp[0][0] = -prices[0], dp[0][1] = 0, dp[0][2] = 0, dp[0][3] = 0;对于不能直接推导出初始化值的状态,可以根据状态转移方程的合理性来推导,即把数据代入到状态转移方程,得出初始化结果。
#include <iostream>
#include <vector>
class Solution {
public:
int maxProfit(std::vector<int>& prices) {
std::vector<std::vector<int>>dp(prices.size(), std::vector<int>(4, 0));
// 初始化
dp[0][0] = -prices[0], dp[0][1] = 0, dp[0][2] = 0, dp[0][3] = 0;
for(int i = 1; i < prices.size(); i++){
// 状态转移
dp[i][0] = std::max(std::max(dp[i-1][0], dp[i-1][1] - prices[i]), dp[i-1][3] - prices[i]); // 持有股票可以是前一天就持有股票;也可以是前一天是保持卖出状态,然后当天买入股票;还可以前一天是冷冻期,然后当天买入股票;
dp[i][1] = std::max(dp[i-1][1], dp[i-1][3]); // 保持卖出股票的状态,前一天可以是保持卖出股票的状态,前一天也可以是冷冻期
dp[i][2] = dp[i-1][0] + prices[i]; // 卖出股票,前一天只能是持有股票
dp[i][3] = dp[i-1][2]; // 冷冻期,前一天只能是卖出股票
}
// 不持有股票
return std::max(std::max(dp[prices.size() - 1][1], dp[prices.size() - 1][2]), dp[prices.size() - 1][3]);
}
};
int main(int argc, char argv[]){
// prices = [1,2,3,0,2]
std::vector<int> test = {1, 2, 3, 0, 2};
Solution S1;
int res = S1.maxProfit(test);
std::cout << res << std::endl;
return 0;
}
18--买卖股票的最佳时机含手续费
主要思路:
与买卖股票的最佳时机II类似,只是在卖出股票的时候需要减去对应的手续费。
#include <iostream>
#include <vector>
class Solution {
public:
int maxProfit(std::vector<int>& prices, int fee) {
// 持有股票和不持有股票两种状态
std::vector<std::vector<int>> dp(prices.size(), std::vector<int>(2, 0));
dp[0][0] = -prices[0]; // 持有股票
dp[0][1] = 0; // 不持有股票 // 这里可能会误初始化为 -fee,但考虑实际价值如果当天买入就卖出,白亏一笔手续费,所以干脆不买,初始化为0
for(int i = 1; i < prices.size(); i++){
dp[i][0] = std::max(dp[i-1][0], dp[i-1][1] - prices[i]);
dp[i][1] = std::max(dp[i-1][1], dp[i-1][0] + prices[i] - fee);
}
return dp[prices.size() - 1][1];
}
};
int main(int argc, char argv[]){
// prices = [1, 3, 2, 8, 4, 9], fee = 2
std::vector<int> test = {1, 3, 2, 8, 4, 9};
int fee = 2;
Solution S1;
int res = S1.maxProfit(test, fee);
std::cout << res << std::endl;
return 0;
}
19--最长递增子序列
主要思路:
dp[i] 表示以字母 nums[i] 结尾的最长子序列长度;
状态转移方程:当 j < i 且 nums[j] < nums[i] 时,nums[i] = std::max(dp[i], dp[j] + 1);
#include <iostream>
#include <vector>
class Solution {
public:
int lengthOfLIS(std::vector<int>& nums) {
std::vector<int> dp(nums.size(), 1); // 初始化为1,至少是以自己作为结尾,其最小长度是1
int res = 1;
for(int i = 1; i < nums.size(); i++){
for(int j = 0; j < i; j++){
if(nums[j] < nums[i]){
dp[i] = std::max(dp[i], dp[j] + 1);
}
}
if (dp[i] > res) res = dp[i]; // 记录最长的长度
}
return res;
}
};
int main(int argc, char argv[]){
// nums = [10, 9, 2, 5, 3, 7, 101, 18]
std::vector<int> test = {10, 9, 2, 5, 3, 7, 101, 18};
Solution S1;
int res = S1.lengthOfLIS(test);
std::cout << res << std::endl;
return 0;
}
20--最长连续递增序列
主要思路:
与上题类似,但本题要求连续递增的子序列,因此不需要遍历索引为[0, i)的数值,只需要判断前面一个数值;
#include <iostream>
#include <vector>
class Solution {
public:
int findLengthOfLCIS(std::vector<int>& nums) {
std::vector<int> dp(nums.size(), 1);
int res = 1;
for(int i = 1; i < nums.size(); i++){
if(nums[i-1] < nums[i]) dp[i] = std::max(dp[i], dp[i-1] + 1);
if(dp[i] > res) res = dp[i];
}
return res;
}
};
int main(int argc, char argv[]){
// nums = [1, 3, 5, 4, 7]
std::vector<int> test = {1, 3, 5, 4, 7};
Solution S1;
int res = S1.findLengthOfLCIS(test);
std::cout << res << std::endl;
return 0;
}
21--最长重复子数组
主要思路:
dp[i][j] 表示以 nums1[i] 和 nums2[j] 结尾的, 公共的长度最长的子数组的长度;
#include <iostream>
#include <vector>
class Solution {
public:
int findLength(std::vector<int>& nums1, std::vector<int>& nums2) {
// dp[i][j] 表示以 nums1[i] 和 nums2[j]结尾的, 公共的长度最长的子数组的长度
std::vector<std::vector<int>> dp(nums1.size(), std::vector<int>(nums2.size(), 0));
int res = 0;
// 初始化
for(int i = 0; i < nums1.size(); i++){
if(nums1[i] == nums2[0]){
res = 1;
dp[i][0] = 1;
}
}
for(int j = 0; j < nums2.size(); j++){
if(nums1[0] == nums2[j]){
res = 1;
dp[0][j] = 1;
}
}
// 状态转移
for(int i = 1; i < nums1.size(); i++){
for(int j = 1; j < nums2.size(); j++){
if(nums1[i] == nums2[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
if (dp[i][j] > res) res = dp[i][j];
}
}
return res;
}
};
int main(int argc, char *argv[]) {
// nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
std::vector<int> nums1 = {1, 2, 3, 2, 1};
std::vector<int> nums2 = {3, 2, 1, 4, 7};
Solution S1;
int res = S1.findLength(nums1, nums2);
std::cout << res << std::endl;
return 0;
}
22--最长公共子序列
主要思路:
dp[i][j]表示在 text1 [0, i] 和 text2 [0, j] 范围内最长的公共子序列长度;
状态转移方程:
text1[i] == text2[j] 时,dp[i][j] = dp[i-1][j-1] + 1;
text1[i] != text2[j] 时, dp[i][j] = std::max(dp[i-1][j], dp[i][j-1]);即取[0, i] [0, j-1] 和 [0, i-1][0, j]的最大值;
#include <iostream>
#include <vector>
#include <string>
class Solution {
public:
int longestCommonSubsequence(std::string text1, std::string text2){
// dp[i][j]表示在 text1 [0, i] 和 text2 [0, j] 范围内最长的公共子序列长度
std::vector<std::vector<int>> dp(text1.length(), std::vector<int>(text2.length(), 0));
// 初始化
for(int i = 0; i < text1.length(); i++){
if(text1[i] == text2[0]){
dp[i][0] = 1;
}
else if(i > 0){
dp[i][0] = dp[i-1][0];
}
}
for(int j = 0; j < text2.length(); j++){
if(text1[0] == text2[j]){
dp[0][j] = 1;
}
else if(j > 0){
dp[0][j] = dp[0][j-1];
}
}
// 状态转移
for(int i = 1; i < text1.length(); i++){
for(int j = 1; j < text2.length(); j++){
if(text1[i] == text2[j]){
dp[i][j] = dp[i-1][j-1] + 1;
}
else{
dp[i][j] = std::max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[text1.length() - 1][text2.length() - 1];
}
};
int main(int argc, char *argv[]) {
// text1 = "abcde", text2 = "ace"
std::string str1 = "abcde";
std::string str2 = "ace";
Solution S1;
int res = S1.longestCommonSubsequence(str1, str2);
std::cout << res << std::endl;
return 0;
}
23--不相交的线
主要思路:
本题等价于求两个序列的最长公共子序列;
#include <iostream>
#include <vector>
class Solution {
public:
int maxUncrossedLines(std::vector<int>& nums1, std::vector<int>& nums2) {
std::vector<std::vector<int>> dp(nums1.size(), std::vector<int>(nums2.size(), 0));
for(int i = 0; i < nums1.size(); i++){
if(nums1[i] == nums2[0]) dp[i][0] = 1;
else if(i > 0) dp[i][0] = dp[i-1][0];
}
for(int j = 0; j < nums2.size(); j++){
if(nums1[0] == nums2[j]) dp[0][j] = 1;
else if(j > 0) dp[0][j] = dp[0][j-1];
}
for(int i = 1; i < nums1.size(); i++){
for(int j = 1; j < nums2.size(); j++){
if(nums1[i] == nums2[j]) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = std::max(dp[i-1][j], dp[i][j-1]);
}
}
return dp[nums1.size() - 1][nums2.size() - 1];
}
};
int main(int argc, char argv[]){
// nums1 = [1,4,2], nums2 = [1,2,4]
std::vector<int> nums1 = {1, 4, 2}, nums2 = {1, 2, 4};
Solution S1;
int res = S1.maxUncrossedLines(nums1, nums2);
std::cout << res << std::endl;
return 0;
}
24--最大子序和
主要思路:
dp[i] 表示以 nums[i] 结尾的最大连续子数组的和;
dp[i] = std::max(dp[i-1] + nums[i], nums[i]);
#include <iostream>
#include <vector>
class Solution {
public:
int maxSubArray(std::vector<int>& nums) {
std::vector<int> dp(nums.size(), 0);
int max = nums[0];
dp[0] = nums[0];
for(int i = 1; i < nums.size(); i++){
dp[i] = std::max(dp[i-1] + nums[i], nums[i]);
if(dp[i] > max) max = dp[i];
}
return max;
}
};
int main(int argc, char argv[]){
// nums = [-2,1,-3,4,-1,2,1,-5,4]
std::vector<int> nums = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
Solution S1;
int res = S1.maxSubArray(nums);
std::cout << res << std::endl;
return 0;
}
25--判断子序列
主要思路:
与最长公共子序列类似,只需要判断最长公共子序列是否等于 s 字符串的长度即可;
即 dp[s.length() - 1][t.length() - 1] 是否等于 s.length();
#include <iostream>
#include <vector>
class Solution {
public:
bool isSubsequence(std::string s, std::string t) {
if(s.length() == 0) return true;
if(t.length() == 0 && s.length() != 0) return false;
std::vector<std::vector<int>> dp(s.length(), std::vector<int>(t.length(), 0));
for(int j = 0; j < t.length(); j++){
if(s[0] == t[j]) dp[0][j] = 1;
else if(j > 0) dp[0][j] = dp[0][j-1];
}
for(int i = 1; i < s.length(); i++){
for(int j = 1; j < t.length(); j++){
if(s[i] == t[j]) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = dp[i][j-1];
}
}
if(dp[s.length() - 1][t.length() - 1] == s.length()) return true;
else return false;
}
};
int main(int argc, char argv[]){
// s = "abc", t = "ahbgdc"
std::string str1 = "abc", str2 = "ahbgdc";
Solution S1;
bool res = S1.isSubsequence(str1, str2);
if(res) std::cout << "true" << std::endl;
else std::cout << "false" << std::endl;
return 0;
}
26--不同的子序列
主要思路:
本题可以转化为有多少种删除的方式,可以将字符串s转化为字符串t;
定义 dp[i][j] 表示以字符串s中第i个字符结尾,通过删除s中字符,构成t中前j个字符(以第j个字符结尾)的方式的个数;
当s[i] = t[j] 时,dp[i][j] = dp[i-1][j-1] + dp[i-1][j];加上dp[i-1][j]的原因是,可以不使用s[i]字符,由前i-1个字符来构成t的前j个字符;
#include <iostream>
#include <vector>
class Solution {
public:
int numDistinct(std::string s, std::string t) {
std::vector<std::vector<int>> dp(s.length(), std::vector<int>(t.length(), 0));
if(s[0] == t[0]) dp[0][0] = 1;
for(int i = 1; i < s.length(); i++){
if(s[i] == t[0]) dp[i][0] = dp[i-1][0] + 1;
else dp[i][0] = dp[i-1][0];
}
for(int i = 1; i < s.length(); i++){
for(int j = 1; j < t.length(); j++){
if(s[i] == t[j]) dp[i][j] = (dp[i-1][j-1] + dp[i-1][j]) % 1000000007;
else dp[i][j] = dp[i-1][j]; // 用s中前i-1个字符来构成t中的前j个字符
}
}
return dp[s.length() - 1][t.length() - 1] % 1000000007;
}
};
int main(int argc, char argv[]){
// s = "rabbbit", t = "rabbit"
std::string str1 = "rabbbit", str2 = "rabbit";
Solution S1;
int res = S1.numDistinct(str1, str2);
std::cout << res << std::endl;
return 0;
}
27--两个字符串的删除操作
主要思路:
基于动态规划,dp[i][j]表示以单词word1中第[i-1]字符结尾,word2中第[j-1]字符结尾,使得word1和word2相同所需的最小步数。
当word[i-1]==word2[j-1]时,dp[i][j] = dp[i-1][j-1];
当word[i-1] != word2[j-1]时,dp[i][j] = std::min(dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + 2);
#include <iostream>
#include <string>
#include <vector>
class Solution {
public:
int minDistance(std::string word1, std::string word2) {
std::vector<std::vector<int>>dp(word1.size() + 1, std::vector<int>(word2.size() + 1, 0));
// 初始化
for(int i = 1; i <= word1.size(); i++){
dp[i][0] = i;
}
for(int j = 1; j <= word2.size(); j++){
dp[0][j] = j;
}
for(int i = 1; i <= word1.size(); i++){
for(int j = 1; j <= word2.size(); j++){
if(word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1];
else{
dp[i][j] = std::min(dp[i-1][j] + 1, std::min(dp[i][j-1] + 1, dp[i-1][j-1] + 2));
}
}
}
return dp[word1.size()][word2.size()];
}
};
int main(int argc, char *argv[]){
// word1 = "sea", word2 = "eat"
std::string word1 = "sea";
std::string word2 = "eat";
Solution S1;
int res = S1.minDistance(word1, word2);
std::cout << res << std::endl;
return 0;
}
28--编辑距离
主要思路:
dp[i][j] 表示使得word1[0, i-1]和word2[0, j-1]相等的最少操作次数;
当 word1[i-1] 不等于 word2[j-1] 时,dp[i][j] = std::min(dp[i-1][j-1] + 1, std::min(dp[i-1][j] + 1, dp[i][j-1] + 1));
#include <iostream>
#include <vector>
#include <string>
class Solution {
public:
int minDistance(std::string word1, std::string word2) {
// dp[i][j] 表示使得word1[0, i-1]和word2[0, j-1]相等的最少操作次数
std::vector<std::vector<int>> dp(word1.length() + 1, std::vector<int>(word2.length()+1, 0));
// 初始化
for(int i = 1; i <= word1.length(); i++){
dp[i][0] = i; // word2为空串
}
for(int j = 1; j <= word2.length(); j++){
dp[0][j] = j; // word1为空串
}
for(int i = 1; i <= word1.length(); i++){
for(int j = 1; j <= word2.length(); j++){
if(word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1];
else{
// dp[i-1][j-1] + 1 表示替换操作
// dp[i-1][j] + 1 表示删除word1[i-1]
// dp[i][j-1] + 1 表示增加word1[i-1]
dp[i][j] = std::min(dp[i-1][j-1] + 1, std::min(dp[i-1][j] + 1, dp[i][j-1] + 1));
}
}
}
return dp[word1.length()][word2.length()];
}
};
int main(int argc, char argv[]){
// word1 = "horse", word2 = "ros"
std::string word1 = "horse";
std::string word2 = "ros";
Solution S1;
int res = S1.minDistance(word1, word2);
std::cout << res << std::endl;
return 0;
}
29--回文子串
主要思路:
起始本题更适合用双指针来做,动态规划可以参考代码随想录的思路:回文子串
布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串;
30--最长回文子序列
主要思路:
dp[i][j]表示在s[i, j]范围内的最长回文子串;
状态转移
s[i] == s[j]时,dp[i][j] = dp[i+1][j-1] + 2
s[i] != s[j]时,dp[i][j] = max(dp[i+1][j], dp[i][j-1])
dp[i+1][j]表示考虑s[j],不考虑s[i]; dp[i][j-1]表示考虑s[i],不考虑s[j]
根据递归方程中的i+1和j-1,可知遍历顺序为从下到上,从左到右
#include <iostream>
#include <string>
#include <vector>
class Solution {
public:
int longestPalindromeSubseq(std::string s) {
// dp[i][j]表示在s[i, j]范围内的最长回文子串
std::vector<std::vector<int>> dp(s.length(), std::vector<int>(s.length(), 0));
// 初始化
for(int i = 0; i < s.length(); i++) dp[i][i] = 1;
// 状态转移
// s[i] == s[j]时,dp[i][j] = dp[i+1][j-1] + 2
// s[i] != s[j]时,dp[i][j] = max(dp[i+1][j], dp[i][j-1])
// dp[i+1][j]表示考虑s[j],不考虑s[i]; dp[i][j-1]表示考虑s[i],不考虑s[j]
// 根据递归方程中的i+1和j-1,可知遍历顺序为从下到上,从左到右
for(int i = s.length() - 2; i >= 0; i--){
for(int j = i+1; j < s.length(); j++){
if(s[i] == s[j]) dp[i][j] = dp[i+1][j-1] + 2;
else{
dp[i][j] = std::max(dp[i][j-1], dp[i+1][j]);
}
}
}
return dp[0][s.length() - 1];
}
};
int main(int argc, char* argv[]){
// s = "bbbab"
std::string test = "bbbab";
Solution S1;
int res = S1.longestPalindromeSubseq(test);
std::cout << res << std::endl;
}