1. 动态规划理论基础
(1)什么是动态规划(Dynamic Programming)
如果某一问题有很多重叠子问题,使用动态规划是最有效的。所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。
(2) 动态规划的解题步骤
a. 确定dp数组(dp table)以及下标的含义
b. 确定递推公式
c. dp数组如何初始化
d. 确定遍历顺序
e. 举例推导dp数组
2. 斐波那契数列
思路: 本题利用动态规划,且题目自带状态转移表达式,通过表达式计算即可。
class Solution {
public:
int fib(int n) {
if (n == 0 || 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)
3. 爬楼梯
思路: 与上题类似,利用动态规划。本题要到达第n阶有两种方法,从第n-1阶跨一步 和从第n-2阶跨两步。因此总共的方法数dp[n] = dp[n-1] + dp[n-2],初始化dp[1] = 1,dp[2] = 2
class Solution {
public:
int climbStairs(int n) {
if (n == 1 || n == 2) return n;
vector<int> dp(n + 1); // dp[n]表示到第n阶的方法的种类数
dp[0] = 0;
dp[1] = 1;
dp[2] = 2;
// dp[i] = dp[i-1] + dp[i-2]
for (int i = 3; i <= n; i++)
dp[i] = dp[i - 1] + dp[i - 2];
return dp[n];
}
};
时间复杂度: O(n)
空间复杂度: O(n)
4. 使用最小花费爬楼梯
思路: 利用动态规划,确定dp[i]表示爬到第i阶的最小花费,状态转移方程为dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
// dp[i]表示到达第i阶花费的最低费用
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)
5. 动态规划周总结1
6. 不同路径
思路: 确定状态转移方程,由于机器人只能向右或向下移动,因此dp[i][j] = dp [i-1][j]+dp[i][j-1]
初始化dp数组: 第一行和第一列均为1,因为到达该位置的路径只有直走一条路径
class Solution {
public:
int uniquePaths(int m, int n) {
// dp[x][y] 表示到达坐标为x,y 的方法数
vector<vector<int>> dp(m, vector<int>(n));
// 由于机器人只能向右移或者下移,因此到第一行或第一列均只有一种方法
for (int i = 0; i < n; i++) // 将第一行变为1
dp[0][i] = 1;
for (int i = 0; i < m; i++) // 将第一列变为1
dp[i][0] = 1;
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
// 更新dp
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
};
时间复杂度: O(mn)
空间复杂度: O(mn)
7. 不同路径Ⅱ
思路: 与上题类似,只不过多了路障的判断。将所有值均初始化0,将第一行和第一列中为被路障阻挡的位置修改为1,其他均与上题相同。
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size(), n = obstacleGrid[0].size();
vector<vector<int>> dp(m, vector<int>(n,0));
for (int i = 0; i < n; i++) { // 初始化行
if (obstacleGrid[0][i] == 1) // 路障后面全为0
break;
dp[0][i] = 1;
}
for (int i = 0; i < m; i++) { // 初始化列
if (obstacleGrid[i][0] == 1) // 路障后面全为0
break;
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(mn)
空间复杂度: O(mn)
8. 整数拆分
思路: 动态规划,依次判断每个值符合条件的最大乘积,双重循环遍历,依次取符合条件的最大值
写法1:
class Solution {
public:
int integerBreak(int n) {
// dp[i]表示 i符合条件的数
vector<int> dp(n + 1, 0);
dp[1] = 1;
dp[2] = 1;
for (int i = 1; i < n; i++) {
for (int j = 2; j <= n && i+j<=n; j++) {
dp[i + j] = max(max(dp[i], i)*max(dp[j], j),dp[i+j]);
}
}
return dp[n];
}
};
写法2:
class Solution {
public:
int integerBreak(int n) {
// dp[i]表示 i符合条件的数
vector<int> dp(n + 1, 0);
dp[2] = 1;
for (int i = 2; i <= n; i++)
for (int j = 1; j <= i/2; j++)
dp[i] = max(dp[i], max(dp[i - j] * j, (i - j)*j));
return dp[n];
}
};
问: 为什么j不需要拆分?
答: 由于j是从1开始遍历的,因此,拆分的情况在前面已经考虑到了。
时间复杂度: O(n^2)
空间复杂度: O(n)
9. 不同的二叉搜索树
思路: dp[i]表示节点数为 i 时的 二叉搜索树的类别数。循环遍历1-n,表示中间节点为第几个节点,判断其左右孩子的节点数。通过dp可知其左右孩子的二叉搜索树的类别,二者相乘即可得到当前节点的二叉搜索树总数,累计相加即可得到结果。
class Solution {
public:
int numTrees(int n) {
// dp[i]表示i个节点的二叉搜索树数量
vector<int> dp(n + 1, 0);
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
// l表示左孩子节点数,r表示右孩子节点数(-1是减中间节点)
int l = 0, r = i - l - 1;
while (l < i) {
dp[i] += dp[l] * dp[r];
l++;
r--;
}
}
return dp[n];
}
};
时间复杂度: O(n^2)
空间复杂度: O(n)
10. 动态规划周总结2
11. 0-1背包问题理论基础(一)

-
确定dp数组以及下标的含义
dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少 -
确定递推公式
当遍历到下标为(i,j)时,有两种情况,放入物品i 和不放物品i。如果放物品i就要预先留出物品i的空间。
不放物品i:背包容量为j,里面不放物品i的最大价值是dp[i - 1][j]。
放物品i:背包空出物品i的容量后,背包容量为j - weight[i],dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]且不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值 -
dp数组如何初始化
第一列应全初始化为0,因为第一列的空间为0,必定放不下任何物品,价值为0。
第一行中,当空间大小大于物品0所用空间时,价值为物品0的价值;否则,为0。 -
确定遍历顺序
遍历顺序从第二行开始遍历和第二列开始遍历均可,因为使用的均是左上方和正上方的dp数组。 -
举例推导dp数组
做动态规划的题目,最好的过程就是自己在纸上举一个例子把对应的dp数组的数值推导一下,然后在动手写代码!
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int main() {
int n , space ; // space为最大空间大小
cin >> n >> space;
vector<int> weights(n), value(n);
// dp[i][j]表示物品0-i中,尽量填满空间为j的最大价值,行号为物品,列号为空间
vector<vector<int>> dp(n, vector<int>(space + 1, 0));
for (int i = 0; i < n; i++)
cin >> weights[i];
for (int i = 0; i < n; i++)
cin >> value[i];
// 初始化 dp:第一列均为0(因为空间为0时,价值必定他为0)
// 第一行中空间大于物品0所需空间,则赋值为物品0的价值
for (int i = 0; i <= space; i++)
if (weights[0] <= i)
dp[0][i] = value[0];
// 状态转移方程为:dp[i][j] = max(dp[i-1][j],dp[i-1][j-weights[i]]+value[i])
for (int i = 1; i < n; i++){
for (int j = 1; j <= space; j++) {
if (j >= weights[i])
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weights[i]] + value[i]);
else
dp[i][j] = dp[i - 1][j];
}
}
cout << dp[n - 1][space];
return 0;
}
11. 0-1背包理论基础(二)
整体思想与二维数组类似,将上一层的二维数组进行压缩为一维数组,减小空间复杂度
与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组。这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层
-
确定dp数组的定义
dp[j] 表示 空间为 j 的背包在当前层(每一个物品为一层)的最大价值 -
确定递推公式
dp [j] = max ( dp[j] , dp[j-weight[i]) + value[i]) -
dp数组如何初始化
在物品价值全为正数的情况下,dp数组应全部初始化为0,保证在第一层遍历时,可以将更大的物品成功添加。 -
确定遍历顺序
一维数组的情况下,只能先遍历物品,再遍历空间。因为每一个物品表示一层,更新的dp数组表示经当前物品更新后的最大值,因此必须每一层的遍历,即按物品遍历。
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
同时,在遍历背包空间时,需要从后向前遍历。因为当前dp[j]需按照上一层的dp值进行更新,若从前往后遍历,则后面的dp值会根据前面已被当前层更新的dp值影响。
举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15
如果正序遍历
dp[1] = dp[1 - weight[0]] + value[0] = 15
dp[2] = dp[2 - weight[0]] + value[0] = 30
此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。
- 举例推导dp数组
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main() {
int m, n;// m表示物品种类,n表示背包大小
cin >> m >> n;
vector<int> dp(n + 1, 0);// dp[i]表示当前层,容量为i的最大值
vector<int> weight(m), value(m);
for (int i = 0; i < m; i++)
cin >> weight[i];
for (int i = 0; i < m; i++)
cin >> value[i];
for (int i = 0; i < m; i++) // 遍历物品,每一个物品为一层
// 倒序遍历,保证每层使用的是上一层的数据,即未经当前物品修改后的数据
for (int j = n; j >= weight[i]; j--)
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
cout << dp[n];
return 0;
}
12. 分割等和子集
思路: 本题可以用回溯算法来做,但会超时,因此选择动态规划。本题每一个元素的空间和价值相等,dp[j]表示当前空间为 j 在当前层的最大价值。target为数组元素总和的一半,即为目标值。当dp[target]的最大价值等于target时,则表示存在子集和为target,返回true,反之,返回fase。
class Solution {
public:
bool canPartition(vector<int>& nums) {
sort(nums.begin(), nums.end());
int target = 0;
for (int x : nums)
target += x;
if (target % 2 != 0)
return false;
target /= 2;
vector<int> dp(target + 1, 0);
for (int i = 0; i < nums.size(); i++)
for (int j = target; j >= nums[i]; j--)
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
return dp[target] == target ? true : false;
}
};
时间复杂度: O(n^2)
空间复杂度: O(n)
13. 最后一块石头的重量Ⅱ
思路: 本题主要是要理解清楚什么时候会得到最小石块,即将石堆分为尽可能相等的两部分。因此,本题做法和上题类似。每个石块的空间和价值相等,dp[j]表示在 空间为j的情况下,当层的最大价值。最后返回 另一边价值和-dp【target】。
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int sum = 0, target;
for (int x : stones)
sum += x;
target = sum / 2;
vector<int> dp(target + 1, 0);
for (int i = 0; i < stones.size(); i++)
for (int j = target; j >= stones[i]; j--)
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
return sum - 2 * dp[target];
}
};
时间复杂度: O(n*target)
空间复杂度: O(target)
&spm=1001.2101.3001.5002&articleId=146801778&d=1&t=3&u=a4198460c7d545a29f2f22395b65da5f)
1001

被折叠的 条评论
为什么被折叠?



