基础:509. 斐波那契数、70. 爬楼梯、746. 使用最小花费爬楼梯、62.不同路径、63. 不同路径 II、343. 整数拆分、96.不同的二叉搜索树
背包问题
打家劫舍:198.打家劫舍、213.打家劫舍II、337.打家劫舍III
股票问题:121. 买卖股票的最佳时机(只能买卖一次)、122.买卖股票的最佳时机II(可以买卖多次)、123.买卖股票的最佳时机III(最多买卖两次)、188.买卖股票的最佳时机IV(最多买卖k次)、309.最佳买卖股票时机含冷冻期(买卖多次,卖出一天有冷冻期)、714.买卖股票的最佳时机含手续费(买卖多次,每次有手续费)
子序列问题
确定dp数组以及下标的含义:题目给出了,斐波那契数F(n)就是dp数组,因此第i个数的斐波那契数值F(i)就是dp[i]
确定递推公式:题目给出了递推公式,F(n) = F(n - 1) + F(n - 2),因此状态转移方程为dp[i] = dp[i - 1] + dp[i - 2]
dp数组如何初始化:题目给出了初始值,F(0) = 0,F(1) = 1,因此dp[0] = 0,dp[1] = 1
确定遍历顺序:dp[i]是由dp[i - 1]和dp[i - 2]推导而来的,那么遍历顺序是从前往后
举例推导dp数组:假设i=7,那么dp数组应该是{0, 1, 1, 2, 3, 5, 8, 13}
维护整个数组
class Solution {
public:
int fib(int n) {
if(n <= 1) return n;
vector<int> dp(n+1);//创建dp数组
dp[0] = 0;//dp数组初始值
dp[1] = 1;//dp数组初始值
//开始从前往后遍历
for(int i=2; i<=n; i++)
{
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
};
改进,只维护两个数值
class Solution {
public:
//改进,只维护两个数值,不是整个数组
int fib(int n)
{
if(n <= 1) return n;
int dp[2];//数组 仅维护两个数值
dp[0] = 0;
dp[1] = 1;
int sum = 0;
for(int i=2; i<=n; i++)
{
sum = dp[0] + dp[1];//中间状态记录
dp[0] = dp[1];//当前dp[0] = 上一个dp[1]
dp[1] = sum;//当前dp[1] = 上一个的 dp[0]+dp[1]
}
return dp[1];
}
};
确定dp数组以及下标的含义:dp[i],爬到第i层楼梯,有dp[i]种方法
确定递推公式:
题目中说每次可以爬1或2个台阶,那么有两个方向可以得到dp[i]
如果只爬一个台阶,就从i-1层楼梯上一个台阶。即dp[i - 1],上i-1层楼梯,有dp[i - 1]种方法,再上一个台阶就是dp[i]。
如果只爬两个台阶,就从i-2层楼梯上两个台阶。即dp[i - 2],上i-2层楼梯,有dp[i - 2]种方法,再上两个台阶就是dp[i]。
所以dp[i]就是dp[i - 1]与dp[i - 2]之和,即dp[i] = dp[i - 1] + dp[i - 2]
dp数组如何初始化:
不考虑dp[0]如何初始化,只初始化dp[1] = 1,dp[2] = 2,然后从i = 3开始递推
确定遍历顺序:dp[i]是由dp[i - 1]和dp[i - 2]推导而来的,那么遍历顺序是从前往后
举例推导dp数组:假设i=10,那么dp数组应该是{1,2,3,5,8,13,21,44,65,109}
和斐波那契数唯一的区别就是没有dp[0]
class Solution {
public:
int climbStairs(int n) {
//2.维护整个数组
if(n <= 1) return n;
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];
}
};
class Solution {
public:
int climbStairs(int n) {
//1.只维护两个数值
if(n <= 1) return n;
int dp[3];
dp[1] = 1;
dp[2] = 2;
int sum = 0;
for(int i=3; i<=n; i++)
{
sum = dp[1] + dp[2];
dp[1] = dp[2];
dp[2] = sum;
}
return dp[2];
}
};
确定dp数组以及下标的含义:dp[i],爬到第i层楼梯所花费最少的体力
确定递推公式:
dp数组如何初始化:
注意题目中描述的你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯,也就是说,到第0个或者第1个台阶不需要花钱,但是从第0个或者第1个台阶需要花钱,可以看题目给的例子。因此初始化dp[10] = 0,dp[1] = 0
确定遍历顺序:dp[i]是由dp[i - 1]和dp[i - 2]推导而来的,那么遍历顺序是从前往后
举例推导dp数组:假设使用题目的示例2,cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1],则有
cost[i] | 1 | 100 | 1 | 1 | 1 | 100 | 1 | 1 | 100 | 1 |
---|---|---|---|---|---|---|---|---|---|---|
下标i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
dp[i] | 0 | 0 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 5 |
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
//1.维护整个数组
vector<int> dp(cost.size()+1);
dp[0] = 0;
dp[1] = 0;
for(int i=2; i<=cost.size(); i++)
{
dp[i] = min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2]);
}
return dp[cost.size()];
}
};
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int dp[2];
dp[0] = dp[1] = 0;
int finalcost = 0;
for(int i=2; i<=cost.size(); i++)
{
finalcost = min(dp[0]+cost[i-2], dp[1]+cost[i-1]);
dp[0] = dp[1];
dp[1] = finalcost;
}
return dp[1];
}
};
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int dp0 = 0, dp1 = 0;
int dpi;
for(int i=2; i<=cost.size(); i++)
{
dpi = min(dp1+cost[i-1], dp0+cost[i-2]);
dp0 = dp1;
dp1 = dpi;
}
return dp1;
}
};
如果按照第一步花钱,最后一步不花钱。也就是在第0个或者第1个台阶需要花钱,但是到达顶楼的时候不需要花钱
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
//4.如果按照 第一步是花钱的,最后一步不花钱
vector<int> dp(cost.size()+1);
dp[0] = cost[0];
dp[1] = cost[1];
for(int i = 2; i<cost.size(); i++)//顶楼不花钱 不取等号
{
dp[i] = min(dp[i-2], dp[i-1]) + cost[i];
}
//最后一步不花钱的话,取倒数第一步和倒数第二步的最小值
return min(dp[cost.size()-1], dp[cost.size()-2]);
}
};
五步骤
确定dp数组以及下标的含义:表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径
确定递推公式:和爬楼梯的题目类似,每次可以选择向下或者向右走,也就是有两个方向可以推导出dp[i][j],即dp[i][j] = dp[i - 1][j] + dp[i][j-1]
dp数组如何初始化:从(0, 0)的位置到(i, 0)的路径只有一条,所以dp[i][0]一定都是1,那么dp[0][j]也是1
确定遍历顺序:dp[i][j]是由dp[i - 1][j]和dp[i][j-1]推导而来的,也就是从上或者左边推导而来,那么遍历顺序是从左到右
举例推导dp数组:假设m=5,n=6,则有
1 | 1 | 1 | 1 | 1 | 1 |
---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 |
1 | 3 | 6 | 10 | 15 | 21 |
1 | 4 | 10 | 20 | 35 | 56 |
1 | 5 | 15 | 35 | 70 | 126 |
代码
1 | 1 | 1 | 1 | 1 | 1 |
---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 |
1 | 3 | 6 | 10 | 15 | 21 |
1 | 4 | 10 | 20 | 35 | 56 |
1 | 5 | 15 | 35 | 70 | 126 |
class Solution {
public:
int uniquePaths(int m, int n) {
//1.两个数组
vector<vector<int>> dp(m, 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 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];
}
};
i=1 | 1 | 1 | 1 | 1 | 1 | 1 |
---|---|---|---|---|---|---|
i=2 | 1 | 2 | 3 | 4 | 5 | 6 |
i=3 | 1 | 3 | 6 | 10 | 15 | 21 |
i=4 | 1 | 4 | 10 | 20 | 35 | 56 |
i=5 | 1 | 5 | 15 | 35 | 70 | 126 |
class Solution {
public:
int uniquePaths(int m, int n) {
//2.一个数组
vector<int> dp(n, 0);
for(int i=0; i<n; i++) dp[i] = 1;
for(int j=1; j<m; j++)
{
for(int i=1; i<n; i++)
{
dp[i] += dp[i-1];
}
}
return dp[n-1];
}
};
在这m + n - 2 步中,一定有 m - 1 步是要向下走的,不用管什么时候向下走。那么可以转化为组合问题,给m + n - 2个不同的数,随便取m - 1个数,有几种取法:
C m + n − 2 m − 1 C_{m+n-2}^{m-1} Cm+n−2m−1 == C m − 1 + n − 1 m − 1 C_{m-1+n-1}^{m-1} Cm−1+n−1m−1 == C m + n − 2 n − 1 C_{m+n-2}^{n-1} Cm+n−2n−1 == ( m + n − 2 ) ! ( n − 1 ) ! × ( m − 1 ) ! {(m+n-2)!\over (n-1)!×(m-1)!} (n−1)!×(m−1)!(m+n−2)! == ( m − 1 + n − 2 ) × . . . × m × ( m − 1 ) × . . . × 2 × 1 ( n − 1 ) × . . . × 2 × 1 × ( m − 1 ) × . . . × 2 × 1 {(m-1+n-2)×...×m×(m-1)×...×2×1\over (n-1)×...×2×1×(m-1)×...×2×1} (n−1)×...×2×1×(m−1)×...×2×1(m−1+n−2)×...×m×(m−1)×...×2×1 == ( m − 1 + n − 2 ) × . . . × m 1 {(m-1+n-2)×...×m\over 1} 1(m−1+n−2)×...×m
注意最后结果是返回m×…×(m-1+n-2),也就是分母除以分子的结果,还要注意分子溢出的情况,不能直接模拟公式计算。
class Solution {
public:
int uniquePaths(int m, int n) {
//3.数论 组合问题
long long numerator = 1; // 分子
int denominator = m - 1; // 分母
int count = m-1;//相当于分子分母相消m-1项
int t = m+n-2;//分子项
while(count--)
{
numerator *= (t--);
//分母不为0;numerator%denominator==0 表示找到相消项
while(denominator!=0 && numerator%denominator==0)
{
numerator /= denominator;//更新结果
denominator--;//分母项
}
}
return numerator;
}
};
确定dp数组以及下标的含义:表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径
确定递推公式:
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
//1.动态数组 两个数组
int n = obstacleGrid[0].size();
int m = obstacleGrid.size();
//起点或者终点有障碍物 无路可走
if(obstacleGrid[0][0]==1 || obstacleGrid[m-1][n-1]==1) return 0;
//初始化dp数组
vector<vector<int>> dp(m, vector<int>(n, 0));
for(int i=0; i<m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1;
for(int j=0; j<n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1;
//更新dp数组
for(int i=1; i<m; i++)
{
for(int j=1; j<n; j++)
{
if(obstacleGrid[i][j] == 1) continue;
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
};
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
//2.动态数组 一个数组
if(obstacleGrid[0][0] == 1) return 0;
int n = obstacleGrid[0].size();
vector<int> dp(n, 0);
//初始化
for(int j=0; j<n; j++)
{
if(obstacleGrid[0][j] == 1) dp[j] = 0;//遇到障碍物
else if(j==0) dp[j] = 1;
else dp[j] = dp[j-1];
}
//更新dp数组
for(int i=1; i<obstacleGrid.size(); i++)
{
for(int j=0; j<n; j++)//注意这里从0开始
{
if(obstacleGrid[i][j] == 1) dp[j] = 0;
else if(j!=0) dp[j] += dp[j-1];
}
}
return dp[n-1];
}
};
确定dp数组以及下标的含义:分拆数字i,可以得到的最大乘积为dp[i]
确定递推公式:
dp[i] = max({dp[i], (i - j) * j, dp[i - j] * j});
,在递推公式推导的过程中,每次计算dp[i],取最大值dp数组如何初始化:拆分0和拆分1的最大乘积没有意义。从dp[i]的定义来说,拆分数字2,得到的最大乘积是1,初始化dp[2] = 1。
确定遍历顺序:
拆分一个数n使其乘积最大,那么一定是拆分成m个数值相近的因子相乘的乘积最大。10 拆成 3 * 3 * 4和拆成2 * 5,前者的乘积更大。虽然无法确定m,但m一定大于等于2,也就是意味着拆成两个相同的因子的乘积有可能是最大值。那么遍历j时,只需要遍历到 n/2 就可以了,但乘积一定不是最大值。
举例推导dp数组,n为10 时
i | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|
j | 1~2 | 1~3 | 1~4 | 1~5 | 1~6 | 1~7 | 1~8 | 1~9 |
i-j | 2~1 | 3~1 | 4~1 | 5~1 | 6~1 | 7~1 | 8~1 | 9~1 |
dp[i-j] | dp[2] | dp[3] dp[2] | dp[4] dp[3] dp[2] | dp[5] dp[4] dp[3] dp[2] | dp[6] dp[5] dp[4] dp[3] dp[2] | dp[7] dp[6] dp[5] dp[4] dp[3] dp[2] | dp[8] dp[7] dp[6] dp[5] dp[4] dp[3] dp[2] | dp[9] dp[8] dp[7] dp[6] dp[5] dp[4] dp[3] dp[2] |
dp[i] | dp[3] | dp[4] | dp[5] | dp[6] | dp[7] | dp[8] | dp[9] | dp[10] |
class Solution {
public:
int integerBreak(int n) {
//1.动态规划
vector<int> dp(n+1);
dp[2] = 1;
for(int i=3; i<=n; i++)//dp[1] dp[0]没有意义
{
//内层for循环有三种写法
//1.for(int j=1; j
//2.for(int j=1; j
for(int j=1; j<=i/2; j++)
{
dp[i] = max(dp[i], max(j*(i-j), j*dp[i-j]));
}
}
return dp[n];
}
};
1为头结点时,相当于n=2的两棵树加了值为1的头结点,3为头结点时,亦同理。2为头结点时,布局同n=1的树。
dp[3]可以通过dp[1] 和 dp[2] 推导,即元素1为头结点搜索树的数量 + 元 素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量:
dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]
dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量],j相当于是头结点的元素,从1遍历到i为止。
dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量
dp数组如何初始化:空节点也是一棵二叉树,也是一棵二叉搜索树,为了避免乘法为0,初始化dp[0]=1。
确定遍历顺序:dp[i] += dp[j - 1] * dp[i - j]可以看出,节点数为i的状态是依靠 i之前节点数的状态,用j来遍历i中每一个数作为头结点的状态
拆分一个数n使其乘积最大,那么一定是拆分成m个数值相近的因子相乘的乘积最大。10 拆成 3 * 3 * 4和拆成2 * 5,前者的乘积更大。虽然无法确定m,但m一定大于等于2,也就是意味着拆成两个相同的因子的乘积有可能是最大值。那么遍历j时,只需要遍历到 n/2 就可以了,但乘积一定不是最大值。
class Solution {
public:
int numTrees(int n) {
vector<int> dp(n+1);
dp[0] = 1;//初始化空树的状态为1,避免乘法为0
//更新dp数组
for(int i=1; i<=n; i++)//以i为头结点的树
{
//符合条件的有多少棵树 用j从1-i遍历
for(int j=1; j<=i; j++)
{
//对于第i个节点,需要考虑1作为根节点直到i作为根节点的情况,所以需要累加
//一共i个节点,对于根节点j时,左子树的节点个数为j-1,右子树的节点个数为i-j
//dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]
dp[i] += dp[j-1] * dp[i-j];
}
}
return dp[n];
}
};