核心:动态规划中每一个状态一定是由上一个状态推导出来的。
1、dp[i]
的定义为:第i
个数的斐波那契数值是dp[i]
2、状态转移方程 dp[i] = dp[i - 1] + dp[i - 2]
;
3、初始化:dp[0] = 0;dp[1] = 1;
4、dp[i]
是依赖 dp[i - 1]
和 dp[i - 2]
,那么遍历的顺序一定是从前到后遍历的、
5、当N
为10
的时候,dp
数组应该是数列:0 1 1 2 3 5 8 13 21 34 55
如果代码写出来,发现结果不对,就把dp数组打印出来看看和推导数列是否一致。
class Solution {
public:
int fib(int n) {
if(n <= 1) return n;
vector<int> dp(n + 1);
dp[0] = 0;
dp[1] = 1;
for(int i = 2;i <= n; i++){
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
//时间复杂度:O(n);空间复杂度:O(n)
当然可以发现,实际上只需要维护两个数值就可以了,不需要记录整个序列。
class Solution {
public:
int fib(int N) {
if (N <= 1) return N;
int dp[2];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= N; i++) {
int sum = dp[0] + dp[1];
dp[0] = dp[1];
dp[1] = sum;
}
return dp[1];
}
};
//时间复杂度:O(n);空间复杂度:O(1)
class Solution {
public:
int fib(int N) {
if (N < 2) return N;
return fib(N - 1) + fib(N - 2);
}
};
//时间复杂度:O(2^n);空间复杂度:O(n),算上了编程语言中实现递归的系统栈所占空间
第三层楼梯的状态可以由第二层楼梯 和 到第一层楼梯状态推导出来,所以可以用动态规划。
1、dp[i]
: 爬到第i
层楼梯,有dp[i]
种方法
2、dp[i] = dp[i - 1] + dp[i - 2]
3、dp[1] = 1
,dp[2] = 2
4、从递推公式中可以看出,遍历顺序一定是从前向后遍历的
5、举例~懒得写了
class Solution {
public:
int climbStairs(int n) {
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) {
if(n <= 1) return n;
int dp[3];
dp[1] = 1;
dp[2] = 2;
for(int i = 3;i <= n;i++){
int sum = dp[1] + dp[2];
dp[1] = dp[2];
dp[2] = sum;
}
return dp[2];
}
};
一步一个台阶,两个台阶,三个台阶,直到 m 个台阶,有多少种方法爬到n阶楼顶?
class Solution {
public:
int climbStairs(int n) {
vector<int> dp(n + 1, 0);
dp[0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
// 把m换成2,就可以AC前面70.爬楼梯的题
if (i - j >= 0) dp[i] += dp[i - j];
}
}
return dp[n];
}
};
1、dp[i]
的定义:到达第i
个台阶所花费的最少体力为dp[i]
。
2、dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
3、dp[0] = cost[0];dp[1] = cost[1];
4、从前到后遍历cost数组
5、举例~懒得写了
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
vector<int> dp(cost.size());
dp[0] = cost[0];
dp[1] = cost[1];
for(int i = 2;i<cost.size();i++){
dp[i] = min(dp[i-1],dp[i-2]) + cost[i];
}
return min(dp[cost.size()-1],dp[cost.size()-2]);
//到达最后一个台阶是不用花费的 所以取倒数第一和第二步的最少值即可。
}
};
//优化
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int dp0 = cost[0];
int dp1 = cost[1];
for(int i = 2;i<cost.size();i++){
int dpi = min(dp0,dp1) + cost[i];
dp0 = dp1;
dp1 = dpi;
}
return min(dp0,dp1);
}
};
class Solution {
private:
int dfs(int i, int j, int m, int n) {
if (i > m || j > n) return 0; // 越界了
if (i == m && j == n) return 1; // 找到一种方法,相当于找到了叶子节点
return dfs(i + 1, j, m, n) + dfs(i, j + 1, m, n); //基于向下走or向右走后的方法
}
public:
int uniquePaths(int m, int n) {
return dfs(1, 1, m, n);
}
};
1、dp[i][j]
:表示从(0 ,0)
出发,到(i, j)
有dp[i][j]
条不同的路径。
2、想要求dp[i][j]
,只能有两个方向来推导出来,即dp[i - 1][j]
和 dp[i][j - 1]
。
dp[i - 1][j]
表示从(0, 0)
的位置到(i - 1, j)
几条路径,dp[i][j - 1]
同理。
所以:dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
。
3、 dp[i][0]
一定都是1,因为从 (0, 0)
的位置到 (i, 0)
的路径只有一条,那么 dp[0][j]
也同理。
for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1;
4、递归公式 dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
, dp[i][j]
都是从其上方和左方推导而来,那么从上到下从左到右遍历就可以了。
5、举例输出dp来验证~
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m,vector<int>(n,0));
// m: 3 和 n: 7 的情况下
// dp: {{0, 0, 0, 0, 0, 0, 0},
// {0, 0, 0, 0, 0, 0, 0},
// {0, 0, 0, 0, 0, 0, 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];
}
}
//dp: {{1, 1, 1, 1, 1, 1, 1},
// {1, 2, 3, 4, 5, 6, 7},
// {1, 3, 6, 10, 15, 21, 28}}
return dp[m-1][n-1];
}
};
//时间复杂度:O(m × n);空间复杂度:O(m × n)
//用一个一维数组(也可以理解是滚动数组)可以优化点空间
class Solution {
public:
int uniquePaths(int m, int n) {
vector<int> dp(n);
for(int i = 0;i < n;i++){
dp[i] = 1;
}
//{1, 1, 1, 1, 1, 1, 1},
for(int i = 1;i < m;i++){
for(int j = 1;j < n;j++){
dp[j] += dp[j-1];
}
// i = 1, {1, 2, 3, 4, 5, 6, 7}
// i = 2, {1, 3, 6, 10, 15, 21, 28}
}
return dp[n-1];
}
};
//时间复杂度:O(m × n);空间复杂度:O(n)
一共m,n的话,无论怎么走,走到终点都需要 m + n - 2 步。在这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
注意:求组合的时候,要防止两个int相乘溢出! 也就是说不能把算式的分子都算出来,分母都算出来再做除法。需要在计算分子的时候,就去除以分母。
假设为 C 3 + 7 − 2 3 − 1 = C 8 2 = 8 ! 2 ! 6 ! = 7 ∗ 8 1 ∗ 2 C_{3+7-2}^{3-1}=C^{2}_{8}=\frac{8!}{2!6!}=\frac{7*8}{1*2} C3+7−23−1=C82=2!6!8!=1∗27∗8
class Solution {
public:
int uniquePaths(int m, int n) {
long long numerator = 1; // 分子
int denominator = m - 1; // 分母
int count = m - 1;
int t = m + n - 2;
while (count--) {
numerator *= (t--); // 1*8 = 8
// 4*7 = 28
while (denominator != 0 && numerator % denominator == 0) {
//denominator = 0 说明分母的数字已经全部除过了
//numerator % denominator == 0 是为了防止出现除不尽被系统直接取整的情况
numerator /= denominator; // 8/2=4 2-1=1
// 4/1=4 1-1=0
denominator--;
}
}
return numerator;
}
};
//时间复杂度:O(m);空间复杂度:O(1)
这道题相对于 62.不同路径 就是有了障碍。62.不同路径 中我们已经详细分析了没有障碍的情况,有障碍的话,其实就是标记对应的dp table
保持初始值0
就可以了。
1、dp[i][j]
:表示从(0 ,0)
出发,到(i, j)
有dp[i][j]
条不同的路径。
2、dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
。但这里需要注意一点,因为有了障碍,(i, j)
如果就是障碍的话应该就保持初始状态(初始状态为0
)。
if (obstacleGrid[i][j] == 0) { // 当(i, j)没有障碍的时候,再推导dp[i][j]
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
3、因为从(0, 0)
的位置到(i, 0)
的路径只有一条,所以dp[i][0]
一定为1
,dp[0][j]
也同理。但如果(i, 0)
这条边有了障碍之后,障碍之后(包括障碍)都是走不到的,所以障碍之后的dp[i][0]
是初始值0。
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;
//注意代码里for循环的终止条件,一旦遇到obstacleGrid[i][0] == 1(有障碍物)的情况就停止dp[i][0]的赋值为1条路径的操作,dp[0][j]同理。只有obstacleGrid[i][0] == 0(无障碍)的情况下才循环赋值。
4、递归公式 dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
, dp[i][j]
都是从其上方和左方推导而来,那么从上到下从左到右遍历就可以了。
5、举例一哈验证~
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
if(obstacleGrid[m-1][n-1] == 1 || obstacleGrid[0][0] == 1) {
return 0; //如果起点or终点出现了障碍物 直接返回
}
vector<vector<int>> dp(m,vector<int>(n,0));
for(int i = 0; i < m && obstacleGrid[i][0] == 0; i++){
dp[i][0] = 1; //一旦出现障碍物,后续都为0,不再赋值为1
}
for(int j = 0; j < n && obstacleGrid[0][j] == 0; j++){
dp[0][j] = 1;
}
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];
}
};
//时间复杂度:O(n × m),n、m 分别为obstacleGrid 长度和宽度;空间复杂度:O(n × m)
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
if(obstacleGrid[m-1][n-1] == 1 || obstacleGrid[0][0] == 1) {
return 0; //如果起点or终点出现了障碍物 直接返回
}
vector<int> dp(n);
for(int j = 0; j < n; j++){ //第一层
if(obstacleGrid[0][j] == 1){
dp[j] = 0; //有障碍物
}
else{
dp[j] = 1; //第一行全是1
}
}
for(int i = 1; i < m; i++){ //开始一层层
for(int j = 0; j < n; j++){ //每层的列
if(obstacleGrid[i][j] == 1){
dp[j] = 0;
}
else if(j != 0){
dp[j] = dp[j] + dp[j-1];
}
}
}
return dp[n-1];
}
};
//优化
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
if(obstacleGrid[m-1][n-1] == 1 || obstacleGrid[0][0] == 1) {
return 0; //如果起点or终点出现了障碍物 直接返回
}
vector<int> dp(n);
for(int j = 0; j < n; j++){ //第一层
if(obstacleGrid[0][j] == 1){
dp[j] = 0; //有障碍物
}
else if(j == 0){
dp[j] = 1; //第一个是1
}
else{
dp[j] = dp[j - 1]; //后一个等于前一个
}
}
for(int i = 1; i < m; i++){ //开始一层层
for(int j = 0; j < n; j++){ //每层的列
if(obstacleGrid[i][j] == 1){
dp[j] = 0;
}
else if(j != 0){
//如果j == 0 && obstacleGrid[i][j] == 0,则每层的第一个会依赖于上一层的数,因此直接保留即可,不需要操作
dp[j] = dp[j] + dp[j-1]; //从第二列开始操作
}
}
}
return dp[n-1];
}
};
//时间复杂度:O(n × m),n、m 分别为obstacleGrid 长度和宽度;空间复杂度:O(m)
1、dp[i]
:分拆数字i
,可以得到的最大乘积为dp[i]
。
2、递推公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
3、初始化:dp[2] = 1
4、dp[i]
是依靠 dp[i - j]
的状态,所以遍历i
一定是从前向后遍历,先有dp[i - j]
再有dp[i]
。
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n + 1);
dp[2] = 1;
for(int i = 3;i <= n;i++){
for(int j = 1; j < i - 1;j++){
dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
//首先,外层的max是为了更新 在取i时拆分不同的j时的最大值
//其次,内层的max中,(i - j) * j表示单纯的拆分为两个数相乘,而dp[i - j] * j表示拆分成两个以上(≥3)的个数相乘
}
}
return dp[n];
}
};
每次拆成n个3,剩下4及其以内的数则保留,然后相乘,这个结论需要数学证明其合理性!我不会证明,但确实合理哈哈哈,代码放下面啦,不证明了。
class Solution {
public:
int integerBreak(int n) {
if (n == 2) return 1;
if (n == 3) return 2;
if (n == 4) return 4;
int result = 1;
while (n > 4) {
result *= 3;
n -= 3;
}
result *= n;
return result;
}
};
1、dp[i]
: 1
到i
为节点组成的二叉搜索树的个数为dp[i]
。
2、递推公式: dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]
,j
相当于是头结点的元素,从1
遍历到i
为止。所以递推公式:dp[i] += dp[j - 1] * dp[i - j]
; ,j-1
为j
为头结点左子树节点数量,i-j
为以j
为头结点右子树节点数量。具体分析过程如下:
3、从定义上来讲,空节点也是一棵二叉树也是一棵二叉搜索树,dp[0]= 1
是可以说得通的;而从递归公式上来讲,dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]
中以j
为头结点左子树节点数量为0
的话,乘法的结果就变成0
了,而所有dp
都需要基于dp[0]
,意味着后续所有dp
全部变成0
,所以能初始dp[0]=0
,需要初始化dp[0] = 1
。
4、首先一定是遍历节点数,从递归公式:dp[i] += dp[j - 1] * dp[i - j]
可以看出,节点数为i
的状态是依靠 i之前节点数
的状态。那么遍历i
里面每一个数作为头结点的状态,用j
来遍历。
class Solution {
public:
int numTrees(int n) {
vector<int> dp(n + 1);
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];
}
};