动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的!
对于动态规划问题,拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!
做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果。
确定dp数组以及下标的含义
dp[i]: 爬到第i层楼梯,有dp[i]种方法
确定递推公式
如何可以推出dp[i]呢?
从dp[i]的定义可以看出,dp[i] 可以有两个方向推出来。
首先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种方法,那么再一步跳一个台阶不就是dp[i]了么。
还有就是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[i]的时候,一定要时刻想着dp[i]的定义,否则容易跑偏。
这体现出确定dp数组以及下标的含义的重要性!
例如强行安慰自己爬到第0层,也有一种方法,什么都不做也就是一种方法即:dp[0] = 1,相当于直接站在楼顶。
但总有点牵强的成分。
那还这么理解呢:我就认为跑到第0层,方法就是0啊,一步只能走一个台阶或者两个台阶,然而楼层是0,直接站楼顶上了,就是不用方法,dp[0]就应该是0.
其实这么争论下去没有意义,大部分解释说dp[0]应该为1的理由其实是因为dp[0]=1的话在递推的过程中i从2开始遍历本题就能过,然后就往结果上靠去解释dp[0] = 1。
从dp数组定义的角度上来说,dp[0] = 0 也能说得通。
需要注意的是:题目中说了n是一个正整数,题目根本就没说n有为0的情况。
所以本题其实就不应该讨论dp[0]的初始化!
我相信dp[1] = 1,dp[2] = 2,这个初始化大家应该都没有争议的。
所以我的原则是:不考虑dp[0]如何初始化,只初始化dp[1] = 1,dp[2] = 2,然后从i = 3开始递推,这样才符合dp[i]的定义。
确定遍历顺序
从递推公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,遍历顺序一定是从前向后遍历的
举例推导dp数组
举例当n为5的时候,dp table(dp数组)应该是这样的
版本一:
class Solution {
public:
int climbStairs(int n) {
if(n <= 1)
return 1;
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];
}
};
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
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()];
}
};
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m + 1,vector<int>(n + 1));
// 1. 含义:到第m行n列共有几条路。
// 2. 公式:dp[i - 1][j] + dp[i][j - 1]
// 3. 初始化: for j in n dp[0][j] = 1; for i in m dp[i][0] = 1;
// 4. 方向:从前向后
// 5. 打印:
dp[0][0] = 1;
for (int j = 1; j < n; j++) {
dp[0][j] = 1;
}
for (int i = 1; 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];
}
}
return dp[m - 1][n - 1];
}
};
时间复杂度: O ( m × n ) O(m × n) O(m×n)
空间复杂度: O ( n ) O(n) O(n)
// 1. 含义:到第m行n列共有几条路。
// 2. 公式:dp[i - 1][j] + dp[i][j - 1]
// 3. 初始化: for j in n dp[0][j] = 1; for i in m dp[i][0] = 1; // 注意代码里for循环的终止条件,一旦遇到obstacleGrid[i][0] == 1的情况就停止dp[i][0]的赋值1的操作,dp[0][j]同理
// 4. 方向:从前向后
// 5. 打印:
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
vector<vector<int>> dp(m + 1,vector<int>(n + 1));
if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) //如果在起点或终点出现了障碍,直接返回0
return 0;
// 1. 含义:到第m行n列共有几条路。
// 2. 公式:dp[i - 1][j] + dp[i][j - 1]
// 3. 初始化: for j in n dp[0][j] = 1; for i in m dp[i][0] = 1;
// 4. 方向:从前向后
// 5. 打印:
dp[0][0] = 1;
for (int j = 1; j < n && obstacleGrid[0][j] == 0; j++) { // 注意代码里for循环的终止条件,一旦遇到obstacleGrid[i][0] == 1的情况就停止dp[i][0]的赋值1的操作,dp[0][j]同理
dp[0][j] = 1;
}
for (int i = 1; i < m && obstacleGrid[i][0] == 0; i++) {
dp[i][0] = 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 ( m × n ) O(m × n) O(m×n)
空间复杂度: O ( n ) O(n) O(n)
看到这道题目,都会想拆成两个呢,还是三个呢,还是四个…
我们来看一下如何使用动规来解决。
class Solution {
public:
int integerBreak(int n) {
// 1.确定dp数组(dp table)以及下标的含义 dp[i]:分拆数字i,可以得到的最大乘积为dp[i]。
// 2.公式:一个是j * (i - j) 直接相乘。一个是j * dp[i - j],相当于是拆分(i - j)。
// 3.dp的初始化 只初始化dp[2] = 1
// 4.dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
// 5.
vector<int> dp(n + 1);
dp[2] = 1;
for (int i = 3; i <= n ; i++) {
for (int j = 1; j <= i / 2; j++) {
dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
}
}
return dp[n];
}
};
贪心:
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;
}
};
时间复杂度: O ( n 2 ) O(n^2) O(n2)
空间复杂度: O ( n ) O(n) O(n)
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
二维dp数组01背包
依然动规五部曲分析一波。
那么可以有两个方向推出来dp[i][j],
首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。如图:
看到这道题目,都会想拆成两个呢,还是三个呢,还是四个…
我们来看一下如何使用动规来解决。
dp[i]示i之前包括i的以nums[i]结尾的最长递增子序列的长度
为什么一定表示 “以nums[i]结尾的最长递增子序” ,因为我们在 做 递增比较的时候,如果比较 nums[j] 和 nums[i] 的大小,那么两个递增子序列一定分别以nums[j]为结尾和 nums[i]为结尾, 要不然这个比较就没有意义了,不是尾部元素的比较那么如何算递增呢。
所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
注意这里不是要dp[i] 与 dp[j] + 1进行比较,而是我们要取dp[j] + 1的最大值。
dp[i]的初始化
每一个i,对应的dp[i](即最长递增子序列)起始大小至少都是1.
确定遍历顺序
dp[i] 是有0到i-1各个位置的最长递增子序列 推导而来,那么遍历i一定是从前向后遍历。
j其实就是遍历0到i-1,那么是从前到后,还是从后到前遍历都无所谓,只要吧 0 到 i-1 的元素都遍历了就行了。 所以默认习惯 从前向后遍历。
class Solution {
public:
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;
}
};
时间复杂度: O ( n 2 ) O(n^2) O(n2)
空间复杂度: O ( n ) O(n) O(n)
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
// 1. dp[i]的定义,以i结尾的最长连续递增子序列长度
// 2. 方程 if (nums[i] > nums[i - 1]) dp[i] = dp[i - 1] + 1;
// 3. 初始 dp[0] = 1;
// 4. 顺序 从左到右
// 5. 打印
if (nums.size() == 0) return 0;
int result = 1;
vector<int> dp(nums.size() ,1);
for (int i = 1; i < nums.size(); i++) {
if (nums[i] > nums[i - 1]) { // 连续记录
dp[i] = dp[i - 1] + 1;
}
if (dp[i] > result) result = dp[i];
}
return result;
}
};
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)
// 1. dp[i][j] 表示,nums1以i - 1结尾,nums2以j - 1结尾的最长重复子数组的长度
// 2. 方程:根据dp[i][j]的定义,dp[i][j]的状态只能由dp[i - 1][j - 1]推导出来。
即当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;
根据递推公式可以看出,遍历i 和 j 要从1开始! dp[i][j] = dp[i - 1][j - 1] + 1;
// 3. 初始化 所以dp[i][0] 和dp[0][j]初始化为0。
// 4. 顺序 外层for循环遍历A,内层for循环遍历B。
// 5. 打印
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
// 1. dp[i][j] 表示,nums1以i - 1结尾,nums2以j - 1结尾的最长重复子数组的长度
// 2. 方程: dp[i][j] = dp[i - 1][j - 1] + 1;
// 3. 初始化 所以dp[i][0] 和dp[0][j]初始化为0。
// 4. 顺序 外层for循环遍历A,内层for循环遍历B。
// 5. 打印
vector<vector<int>> dp (nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
int result = 0;
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 (dp[i][j] > result) result = dp[i][j];
}
}
return result;
}
};
时间复杂度: O ( n ∗ m ) O(n * m) O(n∗m)
空间复杂度: O ( n ∗ m ) O(n * m) O(n∗m)
// 1. dp[i][j] 表示,长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
// 2. 方程: if (text1[i] == text2[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
// else dp[i][j] = max(dp[i - 1][j],dp[i][j - 1])
// 3. 初始化 if (text1[0] == text2[0]) dp[0][0] = 1; else dp[0][0] = 0;
// 4. 顺序 外层for循环遍历A,内层for循环遍历B。
// 5. 打印
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
// 1. dp[i][j] 表示,长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
// 2. 方程: if (text1[i] == text2[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
// else dp[i][j] = max(dp[i - 1][j],dp[i][j - 1])
// 3. 初始化 if (text1[0] == text2[0]) dp[0][0] = 1; else dp[0][0] = 0;
// 4. 顺序 外层for循环遍历A,内层for循环遍历B。
// 5. 打印
vector<vector<int>> dp(text1.size() + 1,vector<int>(text2.size() + 1, 0));
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()];
}
};
时间复杂度: O ( n ∗ m ) O(n * m) O(n∗m)
空间复杂度: O ( n ∗ m ) O(n * m) O(n∗m)
时间复杂度: O ( n 2 ) O(n^2) O(n2)
空间复杂度: O ( n ) O(n) O(n)
// 1. dp[i] 包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]
// 2. dp[i] = max(nums[i],dp[i - 1] + nums[i]); dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和
// nums[i],即:从头开始计算当前连续子序列和
// 3. 从递推公式可以看出来dp[i]是依赖于dp[i - 1]的状态,dp[0]就是递推公式的基础。
// 4. 1 - n
// 5. 方程
class Solution {
public:
int maxSubArray(vector<int>& nums) {
// 1. dp[i] 包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]
// 2. dp[i] = max(nums[i],dp[i - 1] + nums[i]); dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和
// nums[i],即:从头开始计算当前连续子序列和
// 3. 从递推公式可以看出来dp[i]是依赖于dp[i - 1]的状态,dp[0]就是递推公式的基础。
// 4. 1 - n
// 5. 方程
if (nums.size() == 0) return 0;
vector<int> dp(nums.size(),0);
dp[0] = nums[0];
int result = dp[0];
for (int i = 1; i < nums.size(); i++) {
dp[i] = max(dp[i - 1] + nums[i],nums[i]);
if (dp[i] > result) result = dp[i];
}
return result;
}
};
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)
时间复杂度: O ( n 2 ) O(n^2) O(n2)
空间复杂度: O ( n ) O(n) O(n)