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