刷题心得C++

目录


10、动态规划

  • 基础题目
  • 背包问题
  • 打家劫舍
  • 股票问题
  • 子序列问题

6433.矩阵中移动的最大次数

题目描述:

给你一个下标从 0 开始、大小为 m x n 的矩阵 grid ,矩阵由若干 整数组成。

你可以从矩阵第一列中的 任一 单元格出发,按以下方式遍历 grid

  • 从单元格 (row, col) 可以移动到 (row - 1, col + 1)(row, col + 1)(row + 1, col + 1) 三个单元格中任一满足值 严格 大于当前单元格的单元格。

返回你在矩阵中能够 移动最大 次数。

动态规划五部曲,时间复杂度O(mn) 空间复杂度O(mn) 题目中有许多值得注意的细节问题,需反复仔细斟酌。

class Solution {
public:
    int maxMoves(vector<vector<int>>& grid) {
        /*
            动态规划解决单序列问题:
                根据题目的特点找出当前遍历元素对应的最优解(或解的数目)和前面若干元素(通常是一个或两个)的最优解(或解的数目)的关系,并以此找出相应的状态转移方程。
            
            从题目的描述来看,需要从当前遍历的元素dp更新未来的dp值,这显然不符合动态规划的思想,所以需要将问题进行转换,转换为从对应的三个单元格移动到当前[i, j]
        */

        // 确定行、列数
        int rows = grid.size();
        int cols = grid[0].size();

        // 确定dp数组以及下标含义
        // dp[i][j]表示从[i + 1][j - 1]、[i, j - 1]、[i - 1][j - 1]三个单元格移动到[i, j]位置所能够移动的最大次数
        // 初值都为INT_MIN避免了从其它列出发而出现错误值1,题目要求只能从第一列开始出发移动
        vector<vector<int>> dp(rows, vector<int>(cols, INT_MIN));
        int maxMoveCount = 0;

        // 确定递推公式
        // 总共三个递推公式,其余同理
        // if(grid[i][j] > grid[i + 1][j - 1])
        // {
        //     dp[i][j] = max(dp[i][j], dp[i + 1][j - 1] + 1);
        // }

        // dp数组初始化 求dp[i][j],j为1,三个单元格都为dp[..][j - 1],那么就需要初始化dp[..][0]为0,其余都为INT_MIN,避免了从其它位置出发移动
        for(int i = 0; i < rows; ++i)
        {
            dp[i][0] = 0;
        }

         
        // 确定遍历顺序
        // 因为题目要求从第一列的任一单元格出发,所以先遍历列
        for(int j = 1; j < cols; ++j)
        {
            for(int i = 0; i < rows; ++i)
            {
                // 避免索引越界 && i < rows - 1
                if(i < rows - 1 && grid[i][j] > grid[i + 1][j - 1])
                {
                    dp[i][j] = max(dp[i][j], dp[i + 1][j - 1] + 1);
                }

                if(grid[i][j] > grid[i][j - 1])
                {
                    dp[i][j] = max(dp[i][j], dp[i][j - 1] + 1);
                }

                if(i > 0 && grid[i][j] > grid[i - 1][j - 1])
                {
                    dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + 1);
                }

                // 更新最大的dp值
                maxMoveCount = max(maxMoveCount, dp[i][j]);
            }
        }
        // 举例推导dp数组
        return maxMoveCount;
    }
};

70.爬楼梯

时间复杂度O(n) 空间复杂度O(n)
如果觉得dp[0]没有什么实际意义的话,可以考虑初始化的两个dp为1、2,然后从3开始进行遍历

class Solution {
   
public:
    int climbStairs(int n) {
   
        // 动态规划五部曲解决动归基础问题

        if(n <= 1) return n;

        // 确定dp数组以及下标含义
        // dp[i]表示有dp[i]种不同的方法可以爬到i层(这里创建数组的大小为n+1,因为需要访问到n)
        vector<int> dp(n + 1, 0);

        // 确定递推公式
        // dp[i]可以从两个方向推出:
        // dp[i - 1],到达i-1层楼梯,有dp[i-1]种方法,再爬1个台阶即可到达第i层
        // dp[i - 2],到达i-2层楼梯,有dp[i-2]种方法,再爬2个台阶即可到达第i层
        // dp[i] = dp[i - 1] + dp[i - 2];

        // dp数组初始化
        // dp[0]如果没有任何意义的话,可以直接从dp[1]dp[2]开始初始化,然后从第3层台阶开始遍历 dp[2]表示有2中不同的方法可以到达第2层
        // dp[0] = 1;
        dp[1] = 1;
        dp[2] = 2;

        // 确定遍历顺序
        for(int i = 3; i <= n; ++i)
        {
   
            dp[i] = dp[i - 1] + dp[i - 2];
        }

        // 举例推导dp数组
        return dp[n];
    }
};

优化空间复杂度为O(1) 时间复杂度为O(n) 只需要在dp对应的索引位置对dp新数组大小进行取余即可,即实现滚动数组

class Solution {
   
public:
    int climbStairs(int n) {
   
        // 动态规划五部曲解决动归基础问题

        if(n <= 1) return n;

        // 确定dp数组以及下标含义
        // dp[i]表示有dp[i]种不同的方法可以爬到i层
        vector<int> dp(3, 0);

        // 确定递推公式
        // dp[i]可以从两个方向推出:
        // dp[i - 1],到达i-1层楼梯,有dp[i-1]种方法,再爬1个台阶即可到达第i层
        // dp[i - 2],到达i-2层楼梯,有dp[i-2]种方法,再爬2个台阶即可到达第i层
        // dp[i] = dp[i - 1] + dp[i - 2];

        // dp数组初始化
        // dp[0]如果没有任何意义的话,可以直接从dp[1]dp[2]开始初始化,然后从第3层台阶开始遍历 dp[2]表示有2中不同的方法可以到达第2层
        // dp[0] = 1;
        dp[1] = 1;
        dp[2] = 2;

        // 确定遍历顺序
        for(int i = 3; i <= n; ++i)
        {
   
            dp[i % 3] = dp[(i - 1) % 3] + dp[(i - 2) % 3];
        }

        // 举例推导dp数组
        return dp[n % 3];
    }
};

70.爬楼梯进阶

将爬楼梯问题转换为 完全背包问题 + 排列 问题进行求解

题目进阶为:一步一个台阶,两个台阶,三个台阶,…,直到 m个台阶。问有多少种不同的方法可以爬到楼顶呢?

时间复杂度O(n) 空间复杂度O(n)

class Solution {
   
public:
    int climbStairs(int n) {
   
        // 爬楼梯问题进阶 转换为完全背包问题
        // 这里物品为1和2,对于物品可以取无限次,背包容量为n,求装满背包有多少种方法

        // 确定dp数组以及下标含义
        // dp[j]表示装满容量为j的背包,可以有dp[j]种方法
        vector<int> dp(n + 1, 0);

        // 确定递推公式
        // dp[j] += dp[j - nums[i]];

        // dp数组初始化
        dp[0] = 1;

        // 确定遍历顺序 因为先爬1阶再爬2阶 与 先2后1 属于两种不同的方法,所以这里为排列问题
        // 先遍历背包容量,再遍历物品
        // 完全背包,需要从前向后遍历背包容量
        for(int j = 0; j <= n; ++j)
        {
   
            // 物品只有 物品1和物品2
            // 如果题目进阶,那么可以取得的物品为[1-m],只需要该这一行即可 时间复杂度O(nm)
            // for(int i = 1; i <= m; ++i)
            for(int i = 1; i <= 2; ++i)
            {
   
                if(j - i >= 0)
                {
   
                    dp[j] += dp[j - i];
                }
            }
        }

        // 举例推导dp数组
        return dp[n];
    }
};

76.使用最小花费爬楼梯(☆☆)

动态规划五步曲 时间复杂度O(n) 空间复杂度O(n) 需要注意这里的楼顶是n,n为数组的大小

class Solution {
   
public:
    int minCostClimbingStairs(vector<int>& cost) {
   
        // 动态规划五部曲

        // 确定dp数组以及下标含义
        // dp[i]表示到达第i个台阶所需要支付的最低花费为dp[i](这里的顶部为cost.size(),所以需要创建dp数组的大小为n+1,需要能访问到n)
        vector<int> dp(cost.size() + 1, 0);

        // 确定递推公式
        // dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);

        // dp数组初始化 从下标为0或者1的台阶出发开始爬楼梯,初始化为0
        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]);
        }

        // 举例推导dp数组
        return dp[cost.size()];
    }
};

优化空间复杂度O(1) 时间复杂度O(n) 因为当前状态是由前面两个状态推导而来,所以只需要创建大小为2的数组即可,最终第n层台阶的花费也为dp[n % 2]

class Solution {
   
public:
    int minCostClimbingStairs(vector<int>& cost) {
   
        // 动态规划五部曲

        // 确定dp数组以及下标含义
        // dp[i]表示到达第i个台阶所需要支付的最低花费为dp[i](这里的顶部为cost.size(),所以需要创建dp数组的大小为n+1,需要能访问到n)

        // 优化空间复杂度 在求dp[i]之前需要保存dp[i-1]与dp[i-2]的值,只需要一个长度为2的数组即可
        vector<int> dp(2, 0);

        // 确定递推公式
        // dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);

        // dp数组初始化 从下标为0或者1的台阶出发开始爬楼梯,初始化为0
        dp[0] = 0;
        dp[1] = 0;

        // 确定遍历顺序
        for(int i = 2; i <= cost.size(); ++i)
        {
   
            dp[i % 2] = min(dp[(i - 1) % 2] + cost[i - 1], dp[(i - 2) % 2] + cost[i - 2]);
        }

        // 举例推导dp数组
        return dp[cost.size() % 2];
    }
};

62.不同路径

动态规划五部曲,需要注意dp值的定义以及dp值的推导方法(机器人每次只能向下或者向右移动一步),从而清楚地知道dp数组初始化以及dp推导的方向

注意在适当的时候使用双层for循环进行遍历(拆分类问题或者二维类问题)

时间复杂度O(mn) 空间复杂度O(mn)

class Solution {
   
public:
    int uniquePaths(int m, int n) {
   
        // 动态规划五步曲

        // 确定dp数组以及下标含义
        // dp[i][j]表示机器人从[0,0]出发到达[i, j]位置总共有dp[i][j]种不同的路径
        vector<vector<int>> dp(m, vector<int>(n, 0));

        // 确定递推公式
        // dp[i][j] 可以由两个方向的dp值推导而来:机器人要么从上一层的位置dp[i-1][j]移动到当前层;要么从前一列dp[i][j - 1]移动到当前列,将上一层与前一层的dp值相加,就是当前位置的dp[i][j]
        // dp[i][j] = dp[i - 1][j] + dp[i][j - 1];

        // dp数组初始化
        // 需要对第0行与第0列进行初始化,因为[i,j]位置的dp值是由左上角的dp值推导而来,并且第0行与第0列分别对应一种路径
        // 对第0行进行初始化
        for(int j = 0; j < n; ++j) dp[0][j] = 1;
        // 对第0列进行初始化
        for(int i = 0; i < m; ++i) dp[i][0] = 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数组
        // 返回dp[m - 1][n - 1]表示移动到右下角的路径数目
        return dp[m - 1][n - 1];
    }
};

63.不同路径II

大体思路同62题,关于障碍物处理的细节都添加了备注,需要仔细看写的题解
时间复杂度O(mn) 空间复杂度O(1) 因为这里是在原数组上进行的修改,如果在笔试的时候,需要询问面试官是否可以修改原数组,如果不可以的话,就需要重新声明mn大小的数组

class Solution {
   
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
   
        // 动态规划五步曲

        // m x n
        int m = obstacleGrid.size();
        int n = obstacleGrid[0].size();
        // 确定dp数组以及下标含义 这里已经存在mxn的数组,所以可以将这个数组作为dp数组
        // obstacleGrid[i][j]表示机器人从[0, 0]位置移动到[i, j]位置存在obstacleGrid[i][j]条路径

        // 确定递推公式
        // if(obstacleGrid[i][j] == 1) obstacleGrid[i][j] = 0;
        // else obstacleGrid[i][j] = obstacleGrid[i - 1][j] + obstacleGrid[i][j - 1];

        // dp数组初始化
        // 如果起点或者终点存在障碍物,则直接返回0
        if(obstacleGrid[0][0] == 1 || obstacleGrid[m - 1][n - 1]) return 0;

        // 起点和终点都没有障碍物,障碍物在中间位置,在对数组进行初始化的时候进行处理(对0行与0列进行初始化,同62题,但是这里初始化的时候也需要考虑障碍物对初始化的影响)
        // 对0行进行初始化
        for(int j = 0; j < n; ++j)
        {
   
            // 当前位置无障碍物
            if(obstacleGrid[0][j] == 0) obstacleGrid[0][j] = 1;
            // 当前位置有障碍物,从当前位置之后的所有列都必须初始化为0
            else
            {
   
                while(j < n)
                {
   
                    obstacleGrid[0][j] = 0;
                    ++j;
                }
            }
        }

        // 同理,对0列进行初始化操作
        // 在对0列进行初始化的时候需要注意,如果第0行已经对dp[0][0]已经初始化为1,那么在对0列从第0行开始初始化的时候,会"误"把dp[0][0]位置的1当做障碍物处理,所以这里需要从1行开始初始化第0列
        for(int i = 1; i < m; ++i)
        {
   
            if(obstacleGrid[i][0] == 0) obstacleGrid[i][0] = 1;
            else
            {
   
                while(i < m)
                {
   
                    obstacleGrid[i][0] = 0;
                    ++i;
                }
            }
        }

        // 确定遍历顺序 
        for(int i = 1; i < m; ++i)
        {
   
            for(int j = 1; j < n; ++j)
            {
   
                // 如果当前位置有障碍物,则当前位置的dp值为0,即无法到达当前位置
                if(obstacleGrid[i][j] == 1) obstacleGrid[i][j] = 0;
                // 如果当前位置无障碍物,则可以从两个方向求出当前位置的dp值
                else obstacleGrid[i][j] = obstacleGrid[i - 1][j] + obstacleGrid[i][j - 1];
            }
        }

        // 举例推导dp数组
        return obstacleGrid[m - 1][n - 1];
    }
};

343.整数拆分 (剪绳子类问题)

动归五部曲,这里需要理解dp[i]的意义以及如何对每一个dp数组中的元素进行拆分与推导

注意在适当的时候使用双层for循环进行遍历(拆分类问题或者二维类问题)

时间复杂度O(n^2) 空间复杂度O(n)

class Solution {
   
public:
    int integerBreak(int n) {
   

        // 确定dp数组以及下标含义
        // dp[i]表示将i拆分成k个正整数的和,这些整数的乘积最大为dp[i]
        vector<int> dp(n + 1, 0);

        // 确定递推公式 如何得到dp[i]最大乘积
        // 一个是 j * (i - j):单纯把整数拆分为两个数相乘
        // 一个是 j * dp[i - j],相当于拆分(i - j):拆分成两个及两个以上的个数相乘(对i-j再次进行了拆分操作)
        // dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]));

        // dp数组初始化 n >= 2
        dp[2] = 1;

        // 确定遍历顺序
        for(int i = 3; i <= n; ++i)
        {
   
            // 从1遍历j,对i进行拆分
            // 拆分一个数n使之乘积最大,一定是拆分成m个近似相同的子数乘积才是最大,这里m一定大于等于2,那么最差情况下也就是拆分成两个相等的,可能乘积为最大值
            for(int j = 1; j <= i / 2; ++j)
            {
   
                // dp[3] = max(dp[3], 1 * 2, 1 * dp[2]);
                dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]));
            }
        }
        // 举例推导dp数组
        return dp[n];
    }
};

若测试用例为2-1000,则上述方法会溢出,需要使用贪心算法,时间复杂度为O(n)

class Solution {
   
public:
    int cuttingRope(int n) {
   
        // 动态规划不能克服大数越界的问题

//  其实在 剑指 Offer 14- I. 剪绳子 分析动态规划时,我们已经得到结论:

// 最优解就是尽可能地分解出长度为 3 的小段。 但是我们要防止长度为 1 的小段出现。

// 那么,当剩余长度为多少的时候我们就不继续分割了呢?

// 当剩余长度 <= 4 时,便不再分割。 在此之前,执行贪心策略:不断地切割出 3 的小段。we

// 那么最后一次分割出 3 片段以后,剩余长度有会有什么情况呢?
        if (n == 2) return 1;
        if (n == 3) return 2;
        if (n == 4) return 4;
        long long res = 1;
        while (n > 4) {
   
            n -= 3;
            res = res * 3 % 1000000007;
        }
        res = res * n % 1000000007;
        return (int) res;
    }
};

96.不同的二叉搜索树

需要注意dp[i]的定义以及递推公式是如何适配dp[i]的定义推导出的,注意在适当的时候使用双层for循环进行遍历(拆分类问题或者二维类问题)
时间复杂度O(n^2) 空间复杂度O(n)

class Solution {
   
public:
    int numTrees(int n) {
   
        // 确定dp数组以及下标含义
        // dp[i]表示由i个节点组成且节点值从1-i互不相同的二叉搜索树个数为dp[i]
        vector<int> dp(n + 1, 0);

        // 确定递推公式
        // dp[3],就是 元素1为头节点搜索树的数量 + 元素2为头节点搜索树的数量 + 元素3为头节点搜索树的数量
        // 例如 n = 3,那么由3个节点组成且节点值从1-3互不相同的二叉搜索树有dp[3]种
        // 以1为根节点,左子树0个节点,右子树2个节点 dp[0] * dp[2]
        // 元素1为头节点搜索树的数量 = 左子树有0个元素的搜索树数量 * 右子树有2个元素的搜索树数量
        // 以2为根节点,左子树1个节点,右子树1个节点 dp[1] * dp[1]
        // 以3为根节点,左子树2个节点,右子树0个节点 dp[2] * dp[0]
        // 累加即可得到dp[3]
        // for(int i = 1; i <= n; ++i)
        // {
   
        //     for(int j = 1; j <= i; ++j)
        //     {
   
        //         dp[i] += dp[j - 1] * dp[i - j];
        //     }
            
        // }

        // dp数组初始化
        dp[0] = 1;

        // 确定遍历顺序
        for(int i = 1; i <= n; ++i)
        {
   
            // j是头节点元素,从1遍历到i
            for(int j = 1; j <= i; ++j)
            {
   
                // dp[i] += dp[以j为头节点左子树节点个数] + dp[以j为头节点右子树节点个数]
                dp[i] += dp[j - 1] * dp[i - j];
            }
            
        }

        // 举例推导dp数组
        return dp[n];
    }
};

72.编辑距离

编辑距离类问题,求 将 word1 转换成 word2 所使用的最少操作数
时间复杂度 空间复杂度 O(mn)

class Solution {
   
public:
    int minDistance(string word1, string word2) {
   
        // 确定dp数组以及下标含义
        // dp[i][j]表示以i-1为结尾的字符串A 和 以j-1为结尾的字符串B,最近编辑距离为dp[i][j]
        vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1, 0));
        
        // 确定递推公式
        // 相等,说明不用进行编辑操作
        // 不相等,则需要进行三种操作
        // 1. word1删除一个元素,那就是以下标i-2为结尾的word1 与 j-1为结尾的word2的最近编辑距离 再加上1个操作 dp[i][j] = dp[i - 1][j] + 1;
        // 2. word1添加一个元素,相当于word2删除一个元素 dp[i][j] = dp[i][j - 1] + 1;
        // 3. 进行一次替换操作,让word1[i - 1]与word2[j - 1]相等 dp[i][j] = dp[i - 1][j - 1] + 1;

        // if(word1[i - 1] == word2[j - 1]) dp[i][j] = dp[i - 1][j - 1];
        // else dp[i][j] = min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]}) + 1;


        // dp数组初始化 dp[i][j]由左上角推导,那么就需要初始化左上角的dp值
        // dp[i][0]表示以i-1为结尾的字符串word1 和 空字符串word2,最近编辑距离为i,即对word1里的元素全部做删除操作
        for(int i = 0; i <= word1.size(); ++i) dp[i][0] = i;
        // dp[0][j]同理
        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], dp[i][j - 1], dp[i - 1][j - 1]}) + 1;
            }
        }
        // 举例推导dp数组
        return dp[word1.size()][word2.size()];
    }
};

647.回文子串

计算字符串中回文子串的个数,时间复杂度O(n^2) 空间复杂度O(n^2)
首先一定要找到一种递归关系,也就是判断一个子字符串(字符串的下表范围[i,j])是否回文,依赖于,子字符串(下标范围[i + 1, j - 1])) 是否是回文,这样才能进行不断的规划更新

class Solution {
   
public:
    int countSubstrings(string s) {
   
        // 计算字符串中回文子串的个数
        int result = 0;

        // 确定dp数组以及下标含义
        // dp[i][j]表示区间范围[i, j](左闭右闭)的子串是否是回文子串
        vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));

        // 确定递推公式
        // 找到一种递归关系,也就是判断一个子字符串(字符串的下表范围[i,j])是否回文,依赖于,子字符串(下标范围[i + 1, j - 1])) 是否是回文
        // if(s[i] == s[j])
        // {
   
        //     // 单个字符'a' 或者 两个字符'aa'
        //     if(j - i <= 1)
        //     {
   
        //         ++result;
        //         dp[i][j] = true;
        //     }
        //     // i与j相差大于1,那么就需要判断ij中间[i+1, j-1]之间的子串是否是回文串
        //     else if(dp[i + 1][j - 1] == true)
        //     {
   
        //         ++result;
        //         dp[i][j] = true;
        //     }
        // }

        // dp数组初始化 全初始化为false

        // 确定遍历顺序
        // dp[i][j]是由左下角递推而来,所以遍历应该由下往上,由左往右遍历,这样dp[i + 1][j - 1]一定是计算之后的值
        for(int i = s.size() - 1; i >= 0; --i)
        {
   
            for(int j = i; j < s.size(); ++j)
            {
   
                if(s[i] == s[j])
                {
   
                    // 单个字符'a' 或者 两个字符'aa'
                    if(j - i <= 1)
                    {
   
                        ++result;
                        dp[i][j] = true;
                    }
                    // i与j相差大于1,那么就需要判断ij中间[i+1, j-1]之间的子串是否是回文串
                    else if(dp[i + 1][j - 1] == true)
                    {
   
                        ++result;
                        dp[i][j] = true;
                    }
                }
            }
        }

        // 举例推导dp数组
        return result;
    }
};

516.最长回文子序列

求解最长回文子序列,主要递推思路类似647,还需要注意遍历顺序

class Solution {
   
public:
    int longestPalindromeSubseq(string s) {
   
        // 求最长不连续回文子序列的长度

        // 确定dp数组以及下标含义
        // dp[i][j]表示字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]
        vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));

        // 确定递推公式
        // if(s[i] == s[j]) dp[i][j] = dp[i + 1][j - 1] + 2;
        // // 对应字符不相等,说明s[i]和s[j]的同时加入并不能增加[i, j]区间回文子序列的长度,那么就需要分别加入两个字符,看哪一个可以组成最长的回文子序列
        // // dp[i + 1][j]不加s[i],加s[j] (后面同理)
        // else dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);

        // dp数组初始化
        for(int i = 0; i < s.size(); ++i) dp[i][i] = 1;// i与j相同的时候初始化为1

        // 确定遍历顺序
        // 因为dp[i][j]由左下角推导而出,所以需要从下向上,从左向右遍历
        for(int i = s.size() - 1; i >= 0; --i)
        {
   
            for(int j = i + 1; j < s.size(); ++j)
            {
   
                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]);
            }
        }
        // 举例推导dp数组
        return dp[0][s.size() - 1];
    }
};

offer10.斐波那契数列

动态规划 时间复杂度O(n) 空间复杂度O(n)

class Solution {
   
public:
    int fib(int n) {
   
        // 确定dp数组以及下标含义
        // dp[i]表示第i项的dp值为dp[i]
        vector<int> dp(n + 1, 0);

        // 确定递推公式
        // dp[i] = dp[i - 1] + dp[i - 2];

        // dp数组初始化
        dp[0] = 0;
        if(n < 1) return dp[0];

        dp[1] = 1;

        // 确定遍历顺序
        for(int i = 2; i <= n; ++i)
        {
   
            dp[i] = (dp[i - 1] + dp[i - 2]) % 1000000007;
        }
        // 举例推导dp数组
        return dp[n];
    }
};

动态规划 使用滚动数组优化空间复杂度O(1) 时间复杂度O(n)
注意:两个程序对于n为0的时候要单独讨论,不能在n为0时对dp下标0/1同时赋值,会出错,此时数组大小为1,无法访问下标1

class Solution {
   
public:
    int fib(int n) {
   
        // 确定dp数组以及下标含义
        // dp[i]表示第i项的dp值为dp[i]
        vector<int> dp(2, 0);

        // 确定递推公式
        // dp[i] = dp[i - 1] + dp[i - 2];

        // dp数组初始化
        dp[0] = 0;
        if(n < 1) return dp[0];

        dp[1] = 1;

        // 确定遍历顺序
        for(int i = 2; i <= n; ++i)
        {
   
            dp[i % 2] = (dp[(i - 1) % 2] + dp[(i - 2) % 2]) % 1000000007;
        }
        // 举例推导dp数组
        return dp[n % 2];
    }
};

offer93.最长斐波那契数列

class Solution {
   
public:
    int lenLongestFibSubseq(vector<int>& arr) {
   
        // 创建哈希表
        unordered_map<int, int> hash;
        for(int i = 0; i < arr.size(); i++)
        {
   
            hash[arr[i]] = i;// key为数组元素 value为元素下标
        }

        // 确定dp数组以及下标含义
        // dp[i][j]表示以arr[i]为最后一个数字,arr[j]为倒数第二个数字的斐波那契数列的长度
        vector<vector<int>> dp(arr.size(), vector<int>(arr.size(), 2));
        int result = 2;
        // 确定递推公式
        // dp数组初始化
        // 不能形成数列,则初始化为2,虽然目前不能形成对应数列,但是之后有可能形成目标数列
        // 确定遍历顺序
        for(int i = 1; i < arr.size(); i++)
        {
   
            for(int j = 0; j < i; j++)
            {
   
                auto it = hash.find(arr[i] - arr[j]);
                // 如果数组中存在数字下标k,使得arr[i] = arr[j] + arr[k],那么f(i,j) = f(j,k)+1
                if(it == hash.end())
                    continue;// 为找到目标元素
                
                // 找到目标元素
                int k = it->second;
                if(k < j)
                {
   
                    // 即以arr[j]为最后一个数字,arr[k]为倒数第二个数字的斐波那契数列的基础上加上一个数字arr[i]
                    dp[i][j] = dp[j][k] + 1;
                }

                result = max(result, dp[i][j]);
            }
        }
        // 举例推导dp数组
        return result > 2 ? result : 0;
    }
};

打家劫舍

198.打家劫舍

dp[i]的状态取决于第i间房屋是偷还是不偷,题目要求相邻的房屋不能同时偷
时间复杂度O(n) 空间复杂度O(n)

class Solution {
   
public:
    int rob(vector<int>& nums) {
   

        if(nums.size() < 2) return nums[0];
        // 确定dp数组以及下标含义
        // dp[i]表示偷窃[0..i]包括下标i以内的房屋,所得到的的最高金额
        vector<int> dp(nums.size(), 0);

        // 确定递推公式 偷第i间房屋:偷了第i-1间房屋,第i间不能偷;偷了第i-2间房屋,第i间可以偷
        // dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);

        // dp数组初始化
        dp[0] = nums[0];
        dp[1] = max(nums[0], nums[1]);

        // 确定遍历顺序
        for(int i = 2; i < nums.size(); ++i)
        {
   
            dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
        }

        // 举例推导dp数组
        return dp[nums.size() - 1];
    }
};

优化空间复杂度O(1) 时间复杂度O(n) 其本质为滚动数组

class Solution {
   
public:
    int rob(vector<int>& nums) {
   

        if(nums.size() < 2) return nums[0];
        // 确定dp数组以及下标含义
        // dp[i]表示考虑下标i以内的房屋,所得到的的最高金额
        vector
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值