思考
动态规划5步骤
- 1. dp数组的定义及其下标的含义
- 2. 递推公式
- 3. dp数组如何初始化
- 4. 遍历顺序
- 5. 打印dp数组
基础题目
斐波拉契数
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
/*动态规划五部曲
1. 确定dp[i]的含义:表示第i个斐波拉契数的值为dp[i]
2. 地推公式:dp[i] = dp[i-1] + dp[i-2]
3. dp数组如何初始化 dp[0]=1,dp[1]=2
4. 遍历顺序 从前往后
5. 打印dp数组
*/
int fib(int n)
{
if (n == 0)
return 0;
if (n == 1 || n == 2)
return 1;
vector<int> dp(n + 1);
dp[0] = dp[1] = 1;
for (int i = 2; i <= n; i++)
{
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n - 1];
}
};
爬楼梯
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
/*动态规划五部曲
1. 确定dp[i]的含义:表示到达第i阶台阶有dp[i]种方法
2. 地推公式:dp[i] = dp[i-1] + dp[i-2]
3. dp数组如何初始化 dp[0]=0,dp[1]=1,dp[2] = 2
4. 遍历顺序 从前往后
5. 打印dp数组
*/
int climbStairs(int n)
{
if (n == 1)
return 1;
if (n == 2)
return 2;
vector<int> dp(n + 1);
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++)
{
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
使用最小花费爬楼梯
#include <iostream>
#include <vector>
#include <algorithm>
using std::vector;
class Solution
{
public:
/*
输入:cost = [10,15,20]
终点是:下标为3的位置
*/
/*
动态规划5步骤
- 1. dp数组的定义及其下标的含义
dp[i] 到达i位置的花费
- 2. 递推公式
dp[i] = dp[i-1] + cost[i-1]
dp[i] = dp[i-2] + cost[i-2]
dp[i] = min(dp[i-1] + cost[i-1],dp[i-2] + cost[i-2])
- 3. dp数组如何初始化
dp[0] = 0
dp[1] = 0
- 4. 遍历顺序
从前往后遍历
- 5. 打印dp数组
*/
int minCostClimbingStairs(vector<int> &cost)
{
vector<int> dp(cost.size() + 1);
dp[0] = 0;
dp[1] = 0;
int i = 2;
while (i <= cost.size()) // 终点是:下标为3的位置
{
dp[i] = std::min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
i++;
}
return dp[i - 1];
}
};
未初始化 dp 数组的大小:dp 是 vector 类型,但未指定大小,直接访问 dp[0] 和 dp[1] 会导致未定义行为(如崩溃)。
不同路径I
- dp[i]的含义理解很重要。
#include <iostream>
#include <vector>
#include <algorithm>
using std::vector;
class Solution
{
public:
/*递归五部曲
1. dp[i][j]的含义
从(0,0)到达(i,j)有几种路径
2. 递归公式
dp[i][j] = dp[i-1][j]+dp[i][j-1] ; (i>1,j>1)
3. dp的初始化
dp[i][j] = 1;(i=0,j=0)
4. 遍历顺序
从左往右,从上往下
5. 打印dp数组
*/
int uniquePaths(int m, int n)
{
// vector定义二维数组!!!!
vector<vector<int>> dp(m, vector<int>(n, 0)); // 定义m个大小为n的vector(初始化为0)
for (int i = 0; i < m; i++) // 注意这里需要从0开始,因为需要考虑m=1时
{
dp[i][0] = 1;
}
for (int i = 0; i < n; i++) // 注意这里需要从0开始,因为需要考虑n=1时
{
dp[0][i] = 1;
}
for (int i = 1; i < m; i++)
{
for (int j = 1; j < n; j++)
{
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
};
不同路径II
注意:无论条件如何改变,主要关注于动态五部曲是否发送改变,一条一条进行梳理。
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int row = obstacleGrid.size();
int col = obstacleGrid[0].size();
if(row == 0 || obstacleGrid[0][0] == 1)
return 0;
if(row == 1 && col == 1)
{
return 1;
}
vector<vector<long>> dp(row, vector<long>(col, 0)); //申请动态规划的数组,初始化为0
for(int i = 1; i < row; i++)
{
if(obstacleGrid[i][0] != 1)
dp[i][0] = 1;
else
break;
}
for(int i = 1; i < col; i++)
{
if(obstacleGrid[0][i] != 1)
dp[0][i] = 1;
else
break;
}
for(int i = 1; i < row; i++)
{
for(int j = 1; j < col; j++)
{
if(obstacleGrid[i][j] != 1)
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[row-1][col-1];
}
};
整数拆分
#include <iostream>
#include <vector>
#include <algorithm>
using std::vector;
class Solution
{
public:
/*递归五部曲
1. dp[i]的含义
dp[i]:拆分数i的最大乘积为dp[i]
2. 递归公式(障碍物处无路径)
dp[i] = max((i-j)*j,dp[i-j]*j) (直接拆封成两个数,拆分成多个数)
3. dp的初始化
dp[0] = dp[1] = 0;
dp[2] =1;
4. 遍历顺序
从前往后
5. 打印dp数组
*/
// 给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。
int integerBreak(int n)
{
vector<int> dp(n + 1, 0);
dp[0] = 0;
dp[1] = 0; // 注意k>=2
dp[2] = 1;
for (int i = 3; i <= n; i++)
{
for (int j = 1; j < i; j++)
{
// 使用std::max比较2个以上的数
dp[i] = std::max(dp[i], std::max((i - j) * j, dp[i - j] * j));
}
}
return dp[n];
}
};
注意不要忘记max(dp[i])
不同的二叉搜索树
递归解法
class Solution {
public:
// 计算从节点值为left到节点值为right的范围内,可以组成的二叉搜索树的数量
int GetExactNodeNum(int left, int right) {
// 如果left等于right,说明只有一个节点,只有一种树的结构(单个节点本身)
if (left == right) return 1;
// 初始化当前范围内的树的数量为0
int num = 0;
// 遍历当前范围内的所有节点值,尝试将每个节点值作为根节点
for (int i = left; i <= right; i++) {
// 如果当前节点值i是范围内的最小值(即left),则没有左子树
// 只需要计算右子树的数量(从i+1到right)
if (i == left) num += GetExactNodeNum(i + 1, right);
// 如果当前节点值i是范围内的最大值(即right),则没有右子树
// 只需要计算左子树的数量(从left到i-1)
else if (i == right) num += GetExactNodeNum(left, i - 1);
// 如果当前节点值i既不是最小值也不是最大值,则既有左子树也有右子树
// 左子树的数量为从left到i-1的树的数量
// 右子树的数量为从i+1到right的树的数量
// 以i为根节点的树的数量为左子树数量乘以右子树数量
else {
num += GetExactNodeNum(left, i - 1) * GetExactNodeNum(i + 1, right);
}
}
// 返回当前范围内可以组成的二叉搜索树的数量
return num;
}
// 计算由n个节点组成的二叉搜索树的数量
int numTrees(int n) {
// 调用GetExactNodeNum函数,从值为1的节点到值为n的节点
return GetExactNodeNum(1, n);
}
};
动态规划(连贯的思路)
#include <vector>
#include <string>
using namespace std;
class Solution
{
public:
/*
1. dp[i] i个数字有dp[i]种的二叉搜索树
2. 递归公式
dp[i] += dp[j-1] *dp[i-j] (j--->j)
头节点是j
左子树:j-1(比j小的有j-1个)
右子树: i-j(比j大的有i-j个)
3. 初始化
dp[0] = 1;
dp[1] = 1;
dp[2] = 2;
*/
// 动态规划函数,用于计算ans[n]的值
// 主函数,计算由n个节点组成的BST的数量
int numTrees(int n)
{
if (n == 0 || n == 1)
return 1;
else if (n == 2)
return 2;
vector<int> dp(n + 1, 0);
dp[0] = 1;
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++)
{
for (int j = 1; j <= i; j++) // 注意 j也可以等于i,即i也可以自己当头节点
{
dp[i] += (dp[j - 1] * dp[i - j]);
}
}
return dp[n];
}
};
0-1背包问题
0-1背包
二维数组解决0-1背包
- 0-1背包
n种物品,每种物品只有一个 - 递归五部曲
-
dp数组的含义:dp[i][j]:[0~i]物品,任取放入容量为j的背包中。
-
递推公式
-
dp数组的初始化
-
遍历顺序
- 二维的dp数组
- 先遍历背包再物品----可以
- 先遍历物品再背包----可以
- 二维的dp数组
-
打印
-
#include <bits/stdc++.h>
using namespace std;
int main()
{
int n, bagweight; // bagweight代表行李箱空间
cin >> n >> bagweight;
vector<int> weight(n, 0); // 存储每件物品所占空间
vector<int> value(n, 0); // 存储每件物品价值
for (int i = 0; i < n; ++i)
{
cin >> weight[i];
}
for (int j = 0; j < n; ++j)
{
cin >> value[j];
}
// dp数组, dp[i][j]代表行李箱空间为j的情况下,从下标为[0, i]的物品里面任意取,能达到的最大价值
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
// 初始化, 因为需要用到dp[i - 1]的值
// j < weight[0]已在上方被初始化为0
// j >= weight[0]的值就初始化为value[0]
for (int j = weight[0]; j <= bagweight; j++)
{ // 装不下物品0的背包就为0,可以装下的就为value[0]
dp[0][j] = value[0];
}
for (int i = 1; i < weight.size(); i++)
{ // 遍历科研物品
for (int j = 0; j <= bagweight; j++)
{ // 遍历行李箱容量
if (j < weight[i])
dp[i][j] = dp[i - 1][j]; // 如果装不下这个物品,那么就继承dp[i - 1][j]的值
else
{
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
}
cout << dp[n - 1][bagweight] << endl;
return 0;
}
一维数组解决0-1背包
- 递归五部曲
-
dp数组的含义:dp[j]:容量为j的背包的最大价值
-
递推公式
- dp[j] = max(dp[j],dp[j-weight[i]]+value[i]) (加入物品i,不加入物品i)
-
dp数组的初始化
-
遍历顺序
-
打印
-
/ 一维dp数组实现
#include <iostream>
#include <vector>
using namespace std;
int main()
{
// 读取 M 和 N
int M, N;
cin >> M >> N;
vector<int> costs(M);
vector<int> values(M);
for (int i = 0; i < M; i++)
{
cin >> costs[i];
}
for (int j = 0; j < M; j++)
{
cin >> values[j];
}
// 创建一个动态规划数组dp,初始值为0
vector<int> dp(N + 1, 0);
// 外层循环遍历每个类型的研究材料
for (int i = 0; i < M; ++i)
{
// 内层循环从 N 空间逐渐减少到当前研究材料所占空间
for (int j = N; j >= costs[i]; --j)
{
// 考虑当前研究材料选择和不选择的情况,选择最大值
dp[j] = max(dp[j], dp[j - costs[i]] + values[i]);
}
}
// 输出dp[N],即在给定 N 行李空间可以携带的研究材料最大价值
cout << dp[N] << endl;
return 0;
}
分割等和子集
回溯方法(暴搜)
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
class Solution
{
public:
bool backtracking(vector<int> &nums, int startIndex, int target, int curSum)
{
// 终止条件
if (curSum == target)
return true;
if (curSum > target)
return false;
// 单层递归逻辑
for (int i = startIndex; i < nums.size(); i++)
{
curSum += nums[i];
if (backtracking(nums, i + 1, target, curSum))
return true;
curSum -= nums[i]; // 回溯
}
return false;
}
bool canPartition(vector<int> &nums)
{
int total = accumulate(nums.begin(), nums.end(), 0);
if (total % 2 != 0)
return false;
int target = total / 2;
// 排序优化:从大到小排列,加快剪枝效率
sort(nums.begin(), nums.end(), greater<int>());
return backtracking(nums, 0, target, 0);
}
};
动态规划算法
翻译,就是这个集合中有没有,个数少于集合中元素的个数的和等于整个集合和的一半。
就是集合中的元素为物品,看是否里面的物品可以将背包(容量为和的一半)能否装满。重量就是价值。即dp[sum/2] ?= sum/2
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
class Solution
{
public:
bool canPartition(vector<int> &nums)
{
// dp[i]中的i表示背包内总和
// 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200
// 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了
vector<int> dp(10001, 0);
// 也可以使用库函数一步求和
int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum % 2 == 1)
return false;
int target = sum / 2;
// 开始 01背包
for (int i = 0; i < nums.size(); i++)
{
for (int j = target; j >= nums[i]; j--)
{ // 每一个元素一定是不可重复放入,所以从大到小遍历
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
// 集合中的元素正好可以凑成总和target
if (dp[target] == target)
return true;
return false;
}
};
最后一块石头的重量
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
class Solution
{
public:
// 总体思路:让重量相近的石头先碰撞。
// 石头分成两堆:总和一半/2,总和-总和一半的一堆 而且 前一堆>后一堆 向下取整
// 最后的结果:
/*动规五部曲
1. dp[j]的含义
重量为j的背包的最大价值为dp[j]
2. 递归表达式
dp[j] = max(dp[j],dp[j-weight[i]]+weight[i])
3. 初始化
dp[0] = 0;
所有初始化为0
4. 遍历顺序
5. 打印
*/
int lastStoneWeightII(vector<int> &stones)
{
vector<int> dp(1501, 0);
int target = accumulate(stones.begin(), stones.end(), 0) / 2;
for (int i = 0; i < stones.size(); ++i) // 先遍历物品
{
for (int j = target; j >= stones[i]; --j) // 遍历背包,从大往小遍历
{ // j >= stones[i] 背包容量不能小于石头的容量
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return (accumulate(stones.begin(), stones.end(), 0) - dp[target]) - dp[target];
}
};
目标和(不是很懂)
回溯
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
class Solution
{
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int> &candidates, int target, int sum, int startIndex)
{
if (sum == target)
{
result.push_back(path);
}
// 如果 sum + candidates[i] > target 就终止遍历
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++)
{
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i + 1);
sum -= candidates[i];
path.pop_back();
}
}
public:
int findTargetSumWays(vector<int> &nums, int S)
{
int sum = 0;
for (int i = 0; i < nums.size(); i++)
sum += nums[i];
if (S > sum)
return 0; // 此时没有方案
if ((S + sum) % 2)
return 0; // 此时没有方案,两个int相加的时候要格外小心数值溢出的问题
int bagSize = (S + sum) / 2; // 转变为组合总和问题,bagsize就是要求的和
// 以下为回溯法代码
result.clear();
path.clear();
sort(nums.begin(), nums.end()); // 需要排序
backtracking(nums, bagSize, 0, 0);
return result.size();
}
};
动态规划
#include <iostream>
#include <vector>
using namespace std;
class Solution
{
public:
int findTargetSumWays(vector<int> &nums, int target)
{
int sum = 0;
for (int i = 0; i < nums.size(); i++)
sum += nums[i];
if (abs(target) > sum)
return 0; // 此时没有方案
if ((target + sum) % 2 == 1)
return 0; // 此时没有方案
int bagSize = (target + sum) / 2;
vector<int> dp(bagSize + 1, 0);
dp[0] = 1;
/*
if (nums[i] > j) dp[i][j] = dp[i - 1][j];
else dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];
*/
for (int i = 0; i < nums.size(); i++)
{
for (int j = bagSize; j >= nums[i]; j--)
{
dp[j] += dp[j - nums[i]];
}
}
return dp[bagSize];
}
};
一和零
回溯(超时)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 使用回溯
class Solution
{
public:
/*
1. dp数组的含义
2. 递推公式
3. 初始化
4. 遍历顺序
5. 打印
*/
vector<string> sub;
int max;
int coutZero()
{
int coutZero = 0;
for (string s : sub)
{
coutZero += count(s.begin(), s.end(), '0');
}
return coutZero;
}
int coutOne()
{
int coutOne = 0;
for (string s : sub)
{
coutOne += count(s.begin(), s.end(), '1');
}
return coutOne;
}
void backtracking(vector<string> &strs, int m, int n, int startIndex)
{
if (coutZero() > m || coutOne() > n) // 剪枝
{
return;
}
else
{
if (sub.size() > max)
{
max = sub.size();
}
}
for (int i = startIndex; i < strs.size(); i++)
{
sub.push_back(strs[i]);
sub.shrink_to_fit();
backtracking(strs, m, n, i + 1);
sub.pop_back();
sub.shrink_to_fit();
}
}
int findMaxForm(vector<string> &strs, int m, int n)
{
max = 0;
backtracking(strs, m, n, 0);
return max;
}
};
动态规划
#include <numeric>
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 使用回溯
class Solution
{
public:
/*
注意:每个物品的重量:x个0,y个1
背包的容量:m个0,n个1
注意:这里使用的二维数组,但是实际上是一维数组的解法,因为这里的重量有两个维度
1. dp数组的含义
dp[i][j]:i个0,j个1,最大背dp[i][j]个物品 最后求dp[i][j]
2. 递推公式
放入物品:dp[i-x][j-y]+1
不放入物品:dp[i][j]
dp[i][j] = max(dp[i-x][j-y]+1,dp[i][j])
3. 初始化
4. 遍历顺序
5. 打印
*/
int findMaxForm(vector<string> &strs, int m, int n)
{
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (string str : strs) // 先遍历物品
{
// 计算0的个数,1的个数
int x = 0, y = 0;
for (char c : str)
{
if (c == '0')
x++;
else
y++;
}
// 再遍历背包
for (int i = m; i >= x; i--) // 遍历0(也可以先遍历1)
{
for (int j = n; j >= y; j--) // 遍历1
{
dp[i][j] = max(dp[i][j], dp[i - x][j - y] + 1);
}
}
}
return dp[m][n];
}
};
0-1背包问题总结
1. 纯0-1背包:装满这么大容量的最大价值
2. 分割等和子集
能否装满背包
3. 最后一块石头的重量
这么大容量的背包最多装多少
4. 目标和问题
装满这么多,有多少种方法
5. 一和零问题
装满背包,有多少个物品
统一使用一维数组进行求解:
for (int i = 0; i < nums.size(); i++) // 先遍历背包(0->bgsize)
{
for (int j = target; j >= nums[i]; j--) // 再遍历物品(target->???) j >= nums[i]:表示背包容量必须大于物品重量
{
递推公式
}
}
1. 纯0-1背包:装满这么大容量的最大价值
dp[j] = max(dp[j],dp[j-weight[i]]+value[i]) (不放物品i,放物品i)
2. 分割等和子集
能否装满背包
背包的容量就是集合的和的一半,物品就是集合中的元素
dp[j]数组的含义:容量为j的背包的最大价值
递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
for (int i = 0; i < nums.size(); i++)
{
for (int j = target; j >= nums[i]; j--) // 每一个元素一定是不可重复放入,所以从大到小遍历
{
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
3. 最后一块石头的重量
这么大容量的背包最多装多少
本质:让质量相近的石头碰撞。石头分成两堆,1. 一半的一堆,2. 总和-一半的 而且 1《 2 (1堆的和除以2会向下取整)
结果就是:2堆的和-1堆的和
dp[j]数组的含义:重量为j的背包的最大价值为dp[j]
递推公式:dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
for (int i = 0; i < stones.size(); ++i) // 先遍历物品
{
for (int j = target; j >= stones[i]; --j) // 遍历背包,从大往小遍历
{ // j >= stones[i] 背包容量不能小于石头的容量
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
4. 目标和问题
装满这么多,有多少种方法
非负数组nums和一个整数target,添加+和-。
加法的总和:x 减法的总和sum-x 目标:target=2x-sum
-》转为背包问题:背包重量target,物品就是集合中的元素
dp[j]数组的含义:背包重量为j时有dp[j]中方法
递推公式:dp[j] += dp[j - nums[i]]; ???????????????????????????
for (int i = 0; i < nums.size(); i++)
{
for (int j = bagSize; j >= nums[i]; j--)
{
dp[j] += dp[j - nums[i]];
}
}
5. 一和零问题
装满背包,有多少个物品
二进制数组strs,m,n,返回strs的最大子集,含最多m个0,n个1
dp[i][j]数组的含义:i个0,j个1,最大背dp[i][j]个物品 最后求dp[i][j]
递推公式:dp[i][j] = max(dp[i][j], dp[i - x][j - y] + 1);
for (string str : strs) // 先遍历物品
{
// 计算0的个数,1的个数
int x = 0, y = 0;
for (char c : str)
{
if (c == '0')
x++;
else
y++;
}
// 再遍历背包
for (int i = m; i >= x; i--) // 遍历0(也可以先遍历1)
{
for (int j = n; j >= y; j--) // 遍历1
{
dp[i][j] = max(dp[i][j], dp[i - x][j - y] + 1);
}
}
}
完全背包问题
完全背包理论
- 纯完全背包问题,两个for循环是可以颠倒的
零钱兑换II
回溯
超时
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
int result;
vector<vector<int>> elems;
vector<int> elem;
bool backing(int curSum, int amount, vector<int> &coins)
{
if (curSum == amount)
return true;
else if (curSum > amount)
{
return false;
}
for (int i = 0; i < coins.size(); i++)
{
curSum += coins[i];
elem.push_back(coins[i]);
bool ret = backing(curSum, amount, coins);
if (ret)
{
vector<int> tmp(elem);
sort(tmp.begin(), tmp.end());
vector<vector<int>>::iterator it = find(elems.begin(), elems.end(), tmp);
if (it == elems.end()) // elems不存在elem元素
{
elems.push_back(elem);
for_each(elem.begin(), elem.end(), [](int x)
{ cout << x << " "; });
cout << endl;
++result;
}
// elem.clear();// ❌ 会影响回溯中其它分支
}
curSum -= coins[i];
elem.pop_back();
}
return false;
}
int change(int amount, vector<int> &coins)
{
if (amount == 0)
return 1;
result = 0;
backing(0, amount, coins);
return result;
}
};
动态规划
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
int change(int amount, vector<int> &coins)
{
/*回溯5部曲
1. dp[j]含义
dp[j]: amount为j是,求和的方法数
2. 递推公式(有多少种方法,都是这个递推公式:一般dp[0]=1)
dp[j] += dp[j - coins[i]];
3. 初始化
dp[0] = 1; // 背包为0,默认有1种装法
4. 遍历顺序
5. 打印
*/
// vector<int> dp(amount + 1, 0);
vector<uint64_t> dp(amount + 1, 0); // 防止相加数据超int 题目数据保证结果符合 32 位带符号整数。
dp[0] = 1;
for (int i = 0; i < coins.size(); i++) // 先遍历物品
{
for (int j = coins[i]; j <= amount; j++) // 再遍历背包
{
// dp[j] = max(dp[j], dp[j - coins[i]] + coins[i]);
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
};
组合总和
回溯算法
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
int result;
vector<int> elem;
bool backing(int curSum, int amount, vector<int> &coins)
{
if (curSum == amount)
return true;
else if (curSum > amount)
{
return false;
}
for (int i = 0; i < coins.size(); i++)
{
curSum += coins[i];
elem.push_back(coins[i]);
bool ret = backing(curSum, amount, coins);
if (ret)
{
++result;
// elem.clear();// ❌ 会影响回溯中其它分支
}
curSum -= coins[i];
elem.pop_back();
}
return false;
}
int combinationSum4(vector<int> &nums, int target)
{
if (target == 0)
return 1;
result = 0;
backing(0, target, nums);
return result;
}
};
;
动态规划IV
- 先遍历物品再遍历背包,得到的是组合数
- 先遍历背包再遍历物品,得到的是排列数
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
class Solution
{
public:
int combinationSum4(vector<int> &nums, int target)
{
vector<unsigned int> dp(target + 1, 0);
dp[0] = 1;
for (int i = 0; i <= target; i++)
{ // 遍历背包
for (int j = 0; j < nums.size(); j++)
/*
j < nums.size() && i >= nums[j] 的条件会导致 j 的循环在 i >= nums[j] 不满足时提前终止。
这意味着如果 nums 中有多个元素,其中一些满足 i >= nums[j] 而另一些不满足,
循环会在第一个不满足条件的 j 处终止,从而跳过后续可能满足条件的 j
*/
{ // 遍历物品
if (i >= nums[j])
dp[i] += dp[i - nums[j]];
}
}
return dp[target];
}
};
零钱兑换问题
回溯
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
class Solution
{
public:
int result;
vector<vector<int>> elems;
vector<int> elem;
int min;
bool backing(unsigned int curSum, int amount, vector<int> &coins)
{
if (curSum == amount)
return true;
else if (curSum > amount)
{
return false;
}
for (int i = 0; i < coins.size(); i++)
{
curSum += coins[i];
elem.push_back(coins[i]);
bool ret = backing(curSum, amount, coins);
if (ret)
{
++result;
if (min > elem.size())
{
min = elem.size();
}
}
curSum -= coins[i];
elem.pop_back();
}
return false;
}
int coinChange(vector<int> &coins, int amount)
{
min = INT_MAX;
if (amount == 0)
return 0;
backing(0, amount, coins);
if (min == INT_MAX)
return -1;
return min;
}
};
动态规划
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
class Solution
{
public:
/*动规五部曲
1. dp数组的含义:dp[j] 装满量为j的背包,最少的数目为dp[j]
2. dp递推公式
dp[j] = min(dp[j-conis[i]],dp[j])
3. 初始化
4. 遍历顺序
5. 打印
*/
int coinChange(vector<int> &coins, int amount)
{
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 0; i < coins.size(); i++)
{ // 遍历物品
for (int j = coins[i]; j <= amount; j++)
{ // 遍历背包
if (dp[j - coins[i]] != INT_MAX)
{ // 如果dp[j - coins[i]]是初始值则跳过
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
}
}
}
if (dp[amount] == INT_MAX)
return -1;
return dp[amount];
}
};
完全平方数
动态规划
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
class Solution
{
public:
/*动规五部曲
1. dp数组的含义:dp[j] 装满量为j的背包,最少的数目为dp[j]
2. dp递推公式
dp[j] = min(dp[j-conis[i]],dp[j])
3. 初始化
4. 遍历顺序
5. 打印
*/
// int numSquares(int n);
int numSquares(int n)
{
vector<int> coins;
for (int i = 1; i <= n && i * i <= n; ++i)
{
coins.push_back(i * i);
}
coins.shrink_to_fit();
int amount = n;
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 0; i < coins.size(); i++)
{ // 遍历物品
for (int j = coins[i]; j <= amount; j++)
{ // 遍历背包
if (dp[j - coins[i]] != INT_MAX)
{ // 如果dp[j - coins[i]]是初始值则跳过
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
}
}
}
if (dp[amount] == INT_MAX)
return -1;
return dp[amount];
}
};
单词拆分
纯回溯
#include <iostream>
#include <vector>
using namespace std;
class Solution
{
public:
// 纯回溯判断从start位置开始,能否用wordDict拼接成功
bool backing(const string &s, vector<string> &wordDict, int start)
{
// 到达字符串末尾说明匹配成功
if (start == s.size())
return true;
// 尝试字典中每个单词
for (int i = 0; i < wordDict.size(); ++i)
{
int len = wordDict[i].size();
// 判断当前单词是否能匹配s的[start, start+len)子串
if (start + len <= s.size() && s.substr(start, len) == wordDict[i])
{
// 匹配成功则递归判断剩余部分
if (backing(s, wordDict, start + len))
return true;
}
}
// 所有单词均不匹配则返回false
return false;
}
bool wordBreak(string s, vector<string> &wordDict)
{
return backing(s, wordDict, 0);
}
};
回溯+记忆
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
class Solution
{
public:
// 记忆化数组,memo[i]表示从字符串s的第i个位置开始是否能成功分割
// 0 表示未访问,1 表示能分割成功,-1 表示不能分割成功
vector<int> memo;
// 递归函数,从s的start位置开始判断能否用wordDict拼接成功
bool backing(const string &s, vector<string> &wordDict, int start)
{
// 如果start已经到达字符串末尾,说明成功匹配完整个字符串
if (start == s.size())
return true;
// 如果memo[start]不为0,说明之前已经计算过,直接返回结果,避免重复计算
if (memo[start] != 0)
return memo[start] == 1;
// 遍历字典中所有单词,尝试匹配当前字符串位置的子串
for (int i = 0; i < wordDict.size(); ++i)
{
int len = wordDict[i].size();
// 判断剩余字符串长度是否足够匹配wordDict[i]
// 并且s从start开始的子串是否等于wordDict[i]
if (start + len <= s.size() && s.substr(start, len) == wordDict[i])
{
// 如果匹配成功,则递归从start+len位置继续判断
if (backing(s, wordDict, start + len))
{
// 记忆化记录当前位置能成功分割,返回true
memo[start] = 1;
return true;
}
}
}
// 所有单词尝试完均无法匹配成功,记录失败状态并返回false
memo[start] = -1;
return false;
}
// 主函数,初始化记忆化数组,调用递归函数判断
bool wordBreak(string s, vector<string> &wordDict)
{
// 初始化memo,长度为s.size(),初始值均为0(未访问)
memo = vector<int>(s.size(), 0);
// 从字符串起始位置开始递归判断
return backing(s, wordDict, 0);
}
};
打家劫舍
打家劫舍I
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
class Solution
{
public:
/*本房间偷与不偷与前两个房间有关,类似与爬楼梯
*/
/*动态规划五部曲
1. dp数组的含义
dp[i]:房价数为i时偷取的最大金币数量dp[i],return dp[num.size()-1]
2. 递推公式
偷i: dp[i-2]+nums[i] (偷i的话那么上一个房间就不能偷)
不偷i:dp[i-1]
dp[i] = max(dp[i-2]+nums[i],dp[i-1])
3. 初始化
dp[0] = num[0]; 必须偷 (只有一个房间时必偷)
dp[1] = max(num[0],num[1])
4. 遍历顺序
从小到大
5. 打印
*/
int rob(vector<int> &nums)
{
vector<int> dp(nums.size(), 0);
if (nums.size() == 0)
return 0;
else if (nums.size() == 1)
return nums[0];
else if (nums.size() == 2)
return max(nums[0], nums[1]);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for (int i = 2; i < nums.size(); i++)
{
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[nums.size() - 1];
}
};
打家劫舍II
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
class Solution
{
public:
/* 环形解耦为线性
// 注意:这里的要或不要是指考虑或者不考虑(那么其实1,2包含3的)
1. 要首不要尾部
2. 要尾不要首部
3. 首尾都不需要
*/
int rob1(vector<int> &nums)
{
vector<int> dp(nums.size(), 0);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for (int i = 2; i < nums.size(); i++)
{
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[nums.size() - 1];
}
int rob(vector<int> &nums)
{
if (nums.size() == 0)
return 0;
else if (nums.size() == 1)
return nums[0];
else if (nums.size() == 2)
return max(nums[0], nums[1]);
vector<int> begin(nums.begin(), nums.end() - 1);
vector<int> end(nums.begin() + 1, nums.end());
return max(rob1(begin), rob1(end));
}
};
打家劫舍III(树形DP)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
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:
/*每个节点两种状态:(偷与不偷)
dp[0]:不偷该节点的最大金钱数量 dp[1]:偷该节点的最大金钱数量
后序遍历,最终的结果:max(root的dp[0],root的dp[1])
为什么后序遍历:
*/
vector<int> robTree(TreeNode *cur)
{
if (cur == nullptr)
return vector<int>(2, 0);
vector<int> leftDp = robTree(cur->left);
vector<int> rightDp = robTree(cur->right);
int val1 = cur->val + leftDp[0] + rightDp[0]; // 偷当前节点,那么其左右孩子就不能被偷
int val2 = max(leftDp[0], leftDp[1]) + max(rightDp[0], rightDp[1]); // 不偷当前节点
return vector<int>{val2, val1}; //(不偷当前节点,偷当前节点)
}
int rob(TreeNode *root)
{
vector<int> rootDp = robTree(root);
return max(rootDp[0], rootDp[1]);
}
};
股票问题
买卖股票的最佳时机I(只卖一次)
暴力搜索
超出时机限制
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
class Solution
{
public:
// 1。 暴力法
int maxProfit(vector<int> &prices)
{
int max = 0;
for (int i = 0; i < prices.size(); i++)
{
for (int j = i + 1; j < prices.size(); j++)
{
if (max < (prices[j] - prices[i]))
max = prices[j] - prices[i];
}
}
return max;
}
};
贪心算法
#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
using namespace std;
class Solution
{
public:
/*连续贪
每次先贪最小值,然后根据最小值来贪最大差值
*/
int maxProfit(vector<int> &prices)
{
if (prices.empty())
return 0;
int minPrice = INT_MAX; // 初始化最低价格为最大值
int maxProfit = 0; // 初始化最大利润为0
for (int price : prices)
{
// 更新最低价格
minPrice = min(minPrice, price);
// 更新最大利润
maxProfit = max(maxProfit, price - minPrice);
}
return maxProfit;
}
};
动态规划
#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
using namespace std;
class Solution
{
public:
/*动态规划
1. dp[i]的含义
dp[i][0]:持有这支股票的最大利润
dp[i][1]:不持有这支股票的最大利润
return max(dp[prices.size-1][0],dp[prices.size-1][1])
2. 递推公式
dp[i][0] = max(dp[i-1][0],-price[i])两种状态
继续持有这张股票:dp[i-1][0]
买入这张股票: -price[i]
dp[i][0] = max(dp[i-1][1],dp[i-1][0]+price[i])两种状态
继续不持有这张股票: dp[i-1][1]
卖了这张股票: dp[i-1][0]+price[i]
3. 初始化
dp[0][0] = -price[0]
dp[0][1] = 0
4. 遍历顺序
5. 打印
*/
int maxProfit(vector<int> &prices)
{
vector<vector<int>> dp(prices.size(), vector<int>(2, 0));
dp[0][0] = -prices[0];
dp[0][1] = 0;
for (int i = 1; i < prices.size(); i++)
{
dp[i][0] = max(dp[i - 1][0], -prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
}
return max(dp[prices.size() - 1][0], dp[prices.size() - 1][1]);
}
};
买卖股票的最佳时机II(可买卖多次)
贪心
int maxProfit(vector<int>& prices) {
int profit = 0;
for (int i = 1; i < prices.size(); ++i) {
if (prices[i] > prices[i - 1]) {
profit += prices[i] - prices[i - 1]; // 贪心地加上每一次上涨的差值
}
}
return profit;
}
动态规划
#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
using namespace std;
class Solution
{
public:
/*动态规划
1. dp[i]的含义
dp[i][0]:持有这支股票的最大利润
dp[i][1]:不持有这支股票的最大利润
return max(dp[prices.size-1][0],dp[prices.size-1][1])
2. 递推公式
dp[i][0] = max(dp[i-1][0], dp[i-1][1]-price[i])两种状态
继续持有这张股票:dp[i-1][0]
买入这张股票: dp[i-1][1]-price[i] //注意此时的利润就不是从0开始了,
dp[i][1] = max(dp[i-1][1],dp[i-1][0]+price[i])两种状态
继续不持有这张股票: dp[i-1][1]
卖了这张股票: dp[i-1][0]+price[i]
3. 初始化
dp[0][0] = -price[0]
dp[0][1] = 0
4. 遍历顺序
5. 打印
*/
int maxProfit(vector<int> &prices)
{
vector<vector<int>> dp(prices.size(), vector<int>(2, 0));
dp[0][0] = -prices[0];
dp[0][1] = 0;
for (int i = 1; i < prices.size(); 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[i]);
}
return max(dp[prices.size() - 1][0], dp[prices.size() - 1][1]);
}
};
买卖股票的最佳时机III(只卖2次)
#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
using namespace std;
class Solution
{
public:
/*动态规划
1. dp[i]的含义
dp[i][0] 不操作
dp[i][1] 第一次持有
dp[i][2] 第一次不持有
dp[i][3] 第二次持有
dp[i][4] 第二次不持有
2. 递推公式
dp[i][0] = dp[i-1][0]
dp[i][1] = max(dp[i-1][1],dp[i-1][0]-price[i]) (延续前一日状态,第i天买入)
dp[i][2] = max(dp[i-1][2],dp[i-1][1]+price[i]) (延续前一日状态,第i天卖出)
dp[i][3] = max(dp[i-1][3],dp[i-1][2]-price[i]) (延续前一日状态,第i天买入)
dp[i][4] = max(dp[i-1][4],dp[i-1][3]+price[i]) (延续前一日状态,第i天卖出)
3. 初始化
4. 遍历顺序
5. 打印
*/
int maxProfit(vector<int> &prices)
{
if (prices.size() == 0)
return 0;
vector<vector<int>> dp(prices.size(), 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] = 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]);
}
return dp[prices.size() - 1][4];
}
};
买卖股票的最佳时机IV
买卖股票的最佳时机含冷冻期
买卖股票的最佳时机含手续费
子序列问题
最长递增子序列
#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
#include <algorithm>
using namespace std;
class Solution
{
public:
/*动规五部曲
1. dp数组的含义
dp[i]: 以nums[i]结尾的最长递增子序列的长度
2. 递推公式
dp[i] = max(dp[i],dp[j]+1) (j<i且dp[j]<dp[i])
3. 初始化
dp[i]=1
4. 遍历顺序
5. 打印
*/
int lengthOfLIS(vector<int> &nums)
{
if (nums.size() <= 1)
return nums.size();
vector<int> dp(nums.size(), 1);
int result = 0;
for (int i = 1; i < nums.size(); i++)
{
for (int j = 0; j < i; j++) // 从小到大和从大到小都可以
{
if (nums[i] > nums[j])
dp[i] = max(dp[i], dp[j] + 1);
}
if (dp[i] > result)
result = dp[i]; // 取长的子序列
}
return result;
}
};
最长连续递增序列
#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
#include <algorithm>
using namespace std;
class Solution
{
public:
/*动规五部曲
1. dp数组的含义
dp[i]: 以nums[i]结尾的最长递增子序列的长度
2. 递推公式
dp[i] = max(dp[i],dp[j]+1) (j<i且dp[j]<dp[i])
3. 初始化
dp[i]=1
4. 遍历顺序
5. 打印
*/
int findLengthOfLCIS(vector<int> &nums)
{
if (nums.size() <= 1)
return nums.size();
vector<int> dp(nums.size(), 1);
int result = 0;
for (int i = 1; i < nums.size(); i++)
{
for (int j = 0; j < i; j++) // 从小到大和从大到小都可以
{
if (nums[i] > nums[j] && i == j + 1)
dp[i] = max(dp[i], dp[j] + 1);
}
if (dp[i] > result)
result = dp[i]; // 取长的子序列
}
return result;
}
};
最长重复子数组
#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
#include <algorithm>
using namespace std;
class Solution
{
public:
/*动规五部曲
1. dp数组的含义
dp[i][j]: 以i-1结尾的nums1数组和以j-2结尾的nums2数组的最大重复子组的长度
2. 递推公式
if(nums1[i-1]==nums[j-1])
dp[i][j] = dp[i-1][j-1]+1;
3. 初始化
dp[i][0]和dp[0][j]没有意义
全都初始化为0
4. 遍历顺序
5. 打印
*/
int findLength(vector<int> &nums1, vector<int> &nums2)
{
vector<vector<int>> dp(nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
int result = -1;
for (int i = 1; i <= nums1.size(); i++)
{
for (int j = 1; j <= nums2.size(); j++)
{
if (nums1[i - 1] == nums2[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
if (result < dp[i][j])
result = dp[i][j];
}
}
return result;
}
};
最长公共子序列
#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
#include <algorithm>
#include <map>
#include <numeric>
using namespace std;
class Solution
{
public:
/*动规五部曲
1. dp数组的含义
dp[i][j]: 以[0,i-1]结尾的nums1数组和以[0,j-1]结尾的nums2数组的最长公共子序列的长度
2. 递推公式
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]);
3. 初始化
dp[i][0]和dp[0][j] 全都初始化为0
4. 遍历顺序
5. 打印
*/
int longestCommonSubsequence(string text1, string text2)
{
vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
int result = -1;
for (int i = 1; i <= text1.size(); i++)
{
for (int j = 1; j <= text2.size(); 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[text1.size()][text2.size()];
}
};
不相交的线
#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
#include <algorithm>
#include <map>
#include <numeric>
using namespace std;
class Solution
{
public:
/*
不相交的线:本质上就是在求相同的公共的子序列问题(可以不相邻)
动规五部曲
1. dp数组的含义
2. 递推公式
3. 初始化
4. 遍历顺序
5. 打印
*/
int maxUncrossedLines(vector<int> &nums1, vector<int> &nums2)
{
vector<vector<int>> dp(nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
int result = -1;
for (int i = 1; i <= nums1.size(); i++)
{
for (int j = 1; j <= nums2.size(); 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[nums1.size()][nums2.size()];
}
};
最大子序和
#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
#include <algorithm>
#include <map>
#include <numeric>
using namespace std;
class Solution
{
public:
/*
不相交的线:本质上就是在求相同的公共的子序列问题(可以不相邻)
动规五部曲
1. dp数组的含义
dp[i]:以nums[i]结尾的最大子序列的和
2. 递推公式
dp[i] = max(dp[i-1]+nums[i],nums[i]) (延续前面的和,前面的和不要了从头开始算)
// 注意是子序列:需要连续啊!!!,dp[i] = max(dp[i-1]+nums[i],dp[i-1])是错误的,dp[i-1]是错误的,断开了不是子序列了
3. 初始化
dp[0] = nums[0]
4. 遍历顺序
5. 打印
*/
int maxSubArray(vector<int> &nums)
{
vector<int> dp(nums.size(), 0);
dp[0] = nums[0];
int maxV = dp[0];
for (int i = 1; i < nums.size(); i++)
{
dp[i] = max(nums[i], dp[i - 1] + nums[i]);
/*注意:这里的dp数组中的每个元素,求的是以nums[i]结尾的最大的值,而不是整个数组最大的值
什么时候需要求出max,什么时候返回dp[size()]???????
求最长公共子序列:直接返回dp[size()]
求最大子序和:
*/
if (maxV < dp[i])
maxV = dp[i];
}
return maxV;
}
};
判断子序列
转化为最长公共子序列问题
#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
#include <algorithm>
#include <map>
#include <numeric>
using namespace std;
class Solution
{
public:
int longestCommonSubsequence(string text1, string text2)
{
vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
int result = -1;
for (int i = 1; i <= text1.size(); i++)
{
for (int j = 1; j <= text2.size(); 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[text1.size()][text2.size()];
}
// 判断子序列:本质就是求公共子序列的长度
//只不过这次子序列的长度为s.size()
bool isSubsequence(string s, string t)
{
return s.size() == longestCommonSubsequence(s, t) ? true : false;
}
};
直接动态规划
#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
#include <algorithm>
#include <map>
#include <numeric>
using namespace std;
class Solution
{
public:
/*
动规五部曲
1. dp数组的含义
dp[i][j]:以i-1结尾字符串s和以j-1结尾的字符串t最长子序列的长度
2. 递推公式
if (s[i - 1] == t[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = dp[i][j - 1]; // 只能删除t中的字符串
3. 初始化
4. 遍历顺序
5. 打印
*/
bool isSubsequence(string s, string t)
{
// 什么时候+1什么时候不加1:看是否描述的j-1结尾
vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 0));
for (int i = 1; i <= s.size(); i++) // 注意这里是有等号的,因为描述的是i-1结尾的
{
for (int j = 1; j <= t.size(); j++)
{
if (s[i - 1] == t[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = dp[i][j - 1]; // 只能删除t中的字符串
}
}
return dp[s.size()][t.size()] == s.size() ? true : false; // 描述的是s.size()-1结尾的s和以t.size()-1结尾的t
}
};
不同的子序列
当发现int溢出的时候,可以使用uint64_t
#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
#include <algorithm>
#include <map>
#include <numeric>
using namespace std;
class Solution
{
/*两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数。
动规五部曲
1. dp数组的含义
dp[i][j]:以i-1结尾的字符串s有以字符串j-1结尾的字符串t的个数
2. 递推公式
if(s[i-1] == t[j-1])
dp[i][j] = dp[i-1][j-1]+dp[i-1][j] //dp[i][j-1]是不考虑s[i-1] 例如:bagg 和bag中的 bag(不考虑第二个g)
else
dp[i][j] = dp[i-1][j]
3. 初始化
4. 遍历顺序
5. 打印
*/
public:
// 当发现int溢出的时候,可以使用uint64_t
int numDistinct(string s, string t)
{
// vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 0));
// ine 41: Char 49: runtime error: signed integer overflow: 1474397256 + 891953512 canno
vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t>(t.size() + 1, 0));
// ***********************************************************************************
for (int i = 0; i <= s.size(); i++)
{
dp[i][0] = 1; // s中有空字符串的个数:将s中的字符全部删除
}
// ***********************************************************************************
for (int i = 1; i <= s.size(); i++)
{
for (int j = 1; j <= t.size(); j++)
{
if (s[i - 1] == t[j - 1])
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
else
dp[i][j] = dp[i - 1][j];
}
}
return dp[s.size()][t.size()];
}
};
两个字符串的删除操作
动态规划(最长公共子序列转换)
#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
#include <algorithm>
#include <map>
#include <numeric>
using namespace std;
class Solution
{
/*本质就是在求两个字符串的公共子序列,转换的步数=两个字符串的总长度-公共子序列的长度*2
*/
public:
int longestCommonSubsequence(string text1, string text2)
{
vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
int result = -1;
for (int i = 1; i <= text1.size(); i++)
{
for (int j = 1; j <= text2.size(); 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[text1.size()][text2.size()];
}
int minDistance(string word1, string word2)
{
return (word1.size() + word2.size() - longestCommonSubsequence(word1, word2) * 2);
}
};
动态规划
#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
#include <algorithm>
#include <map>
#include <numeric>
using namespace std;
class Solution
{
/*两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数。
动规五部曲
1. dp数组的含义
dp[i][j]:以i-1结尾的字符串world1和以字符串j-1结尾的字符串world2的最少转换次数
2. 递推公式
3. 初始化
4. 遍历顺序
5. 打印
*/
public:
int minDistance(string word1, string word2)
{
vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1, 0));
// ***********************************************************************************
for (int i = 0; i <= word1.size(); i++)
{
dp[i][0] = i;
}
for (int j = 0; 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] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 2); // (删除world1,删除workld2,都删)
dp[i][j] = min(min(dp[i - 1][j] + 1, dp[i][j - 1] + 1), dp[i - 1][j - 1] + 2);
}
}
return dp[word1.size()][word2.size()];
}
};
编辑距离
#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
#include <algorithm>
using namespace std;
class Solution
{
public:
/*动规五部曲
1. dp数组的含义
dp[i][j]:以i-1结尾的word1和以j-1结尾的word2的最少操作次数
2. 递推公式(word1->word2)
if(word1[i-1] == word2[j-1])
dp[i][j]=dp[i-1][j-1]
else
删/增(直接删除)
(word1转word2增 相当于word2转word1删)
dp[i][j]=dp[i][j-1]+1(1表示删除操作)
dp[i][j]=dp[i-1][j]+1(1表示删除操作)
替(直接替换)
dp[i-1][j-1]+1
dp[i][j]=min(dp[i][j-1]+1,dp[i-1][j]+1,dp[i-1][j-1]+1)
3. 初始化
dp[i][0] = i word1删除i次变为空串
dp[0][j] = j word2删除j次变为空串
4. 遍历顺序
5. 打印
*/
int minDistance(string word1, string word2)
{
vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1, 0));
for (int i = 0; i <= word1.size(); i++)
dp[i][0] = i;
for (int j = 0; 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] = min(min(dp[i - 1][j - 1], dp[i - 1][j]), dp[i][j - 1] + 1);
}
}
}
return dp[word1.size()][word2.size()];
}
};
回文子串
动态规划
#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
#include <algorithm>
using namespace std;
class Solution
{
public:
/*动规五部曲
1. dp数组的含义
dp[i][j]:[i,j]表示是否是回文字串,bool值
2. 递推公式(s)
if(s[i] == s[j])
{
if(i == j) // 指向同一个字符
{
dp[i][j] = true;
result++;
}
else if((j-i)==1) //只有两个字符,且这两个字符相等
{
dp[i][j] = true;
result++;
}
else if((j-i)>1) // 需要判断[i+1,j-1]区间是否是回文字串
{
if(dp[i+1][j-1] == true)
{
dp[i][j] = true;
result++;
}
}
}
3. 初始化
4. 遍历顺序
有dp[i+1][j-1]--->dp[i][j] 从左往右从下往上
5. 打印
*/
int countSubstrings(string s)
{
vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
int result = 0;
for (int i = s.size() - 1; i >= 0; i--)
{
for (int j = i; j < s.size(); j++)
{
if (s[i] == s[j])
{
if ((j - i) <= 1)
{
dp[i][j] = true;
result++;
}
else if (dp[i + 1][j - 1] == true)
{
dp[i][j] = true;
result++;
}
}
}
}
return result;
}
};
双指针
- 双指针的方式是每次从可能会有的回文串中心,然后向两侧进行延伸,判定是否为回文串。当子串为奇数长度时,回文串中心唯一;当子串为偶数长度时,会由两个数作为回文串中心。
#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
#include <algorithm>
using namespace std;
class Solution
{
public:
int countSubstrings(string s)
{
int n = s.size(), res = 0;
for (int i = 0; i < n; i++)
{
// 分别尝试奇数时回文串中心情况和偶数时回文串中心情况
res += palindromicNums(s, i, i, n);
res += palindromicNums(s, i, i + 1, n);
}
return res;
}
int palindromicNums(const string &s, int i, int j, int n)
{
int res = 0;
// 从中间向两边伸展探寻
while (i >= 0 && j < n && s[i] == s[j])
{
i--;
j++;
res++;
}
return res;
}
};
最长回文子序列
#include <iostream>
#include <cstring>
#include <vector>
#include <climits>
#include <algorithm>
using namespace std;
class Solution
{
public:
/*动规五部曲
1. dp数组的含义
dp[i][j]:表示[i,j]回文字串的长度
2. 递推公式(s)
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]); // 范围左移,范围右移
}
3. 初始化
4. 遍历顺序
有dp[i+1][j-1]--->dp[i][j] 从左往右从下往上
5. 打印
*/
int longestPalindromeSubseq(string s)
{
vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
for (int i = 0; i < s.size(); i++)
dp[i][i] = 1;
int result = 0;
for (int i = s.size() - 1; i >= 0; i--)
{
for (int j = i + 1; j < s.size(); j++) // j>i 所以j从i+1开始
{
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][s.size() - 1];
}
};