目录
什么是动态规划
将问题分解为互相重叠的子问题,通过反复求解子问题来解决原问题就是动态规划,如果某一问题有很多子问题,使用动态规划来进行求解就是比较有效的。
求解动态规划的核心问题是穷举,但穷举是比较特殊的,因为是在问题存在重叠子问题的情况下穷举,如果暴力穷举会使得效率及其低下。动态规划问题一定会具备「最优子结构」,才能通过子问题的最值得到原问题的最值。另外,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,只有列出正确的「状态转移方程」才能正确地穷举。重叠子问题、最优子结构、状态转移方程就是动态规划三要素。
动态规划和其他算法的区别
1.和分治的区别:动态规划和分治都有最优子结构 ,但是分治的子问题不重叠
2.和贪心的区别::动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优解,所以它永远是局部最优,但是全局的解不一定是最优的
3.和递归的区别:递归和回溯可能存在非常多的重复计算,动态规划可以用递归加记忆化的方式减少不必要的重复计算
解题方法
递归+记忆化(自顶向下)
动态规划(自底向上)
解题步骤
1.根据重叠子问题定义状态
2.寻找最优子结构推导状态转移方程
3.确定dp初始状态
4.确定输出值
斐波那契的动态规划解题思路
f(0)=0,f(1)=1,f(n)=f(n-1)+f(n-2),这样的数列叫做斐波那契数列。
暴力递归:
//暴力递归复杂度O(2^n)
var fib = function (N) {
if (N == 0) return 0;
if (N == 1) return 1;
return fib(N - 1) + fib(N - 2);
};
递归+记忆化:
var fib = function (n) {
const memo = {}; // 对已算出的结果进行缓存
const helper = (x) => {
if (memo[x]) return memo[x];//避免重复运算
if (x == 0) return 0;
if (x == 1) return 1;
memo[x] = helper(x - 1) + helper(x - 2);
return memo[x];
};
return helper(n);
};
动态规划
const fib = (n) => {
if (n <= 1) return n;
const dp = [0, 1];
for (let i = 2; i <= n; i++) {
//自底向上计算每个状态
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
};
滚动数组优化
const fib = (n) => {
if (n <= 1) return n;
//滚动数组 dp[i]只和dp[i-1]、dp[i-2]相关,只维护长度为2的滚动数组,不断替换数组元素
const dp = [0, 1];
let sum = null;
for (let i = 2; i <= n; i++) {
sum = dp[0] + dp[1];
dp[0] = dp[1];
dp[1] = sum;
}
return sum;
};
动态规划+降维
var fib = function (N) {
if (N <= 1) {
return N;
}
let prev2 = 0;
let prev1 = 1;
let result = 0;
for (let i = 2; i <= N; i++) {
result = prev1 + prev2; //直接用两个变量就行
prev2 = prev1;
prev1 = result;
}
return result;
};
具体力扣题目:
509.斐波那契数
如上已经分析
62.不同路径
问题描述:
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。问总共有多少条不同的路径?
- 思路:由于在每个位置只能向下或者向右, 所以每个坐标的路径和等于上一行相同位置和上一列相同位置不同路径的总和,状态转移方程:
f[i][j] = f[i - 1][j] + f[i][j - 1]
; - 复杂度:时间复杂度
O(mn)
。空间复杂度O(mn)
,优化后O(n)
var uniquePaths = function (m, n) {
const f = new Array(m).fill(0).map(() => new Array(n).fill(0)); //初始dp数组
for (let i = 0; i < m; i++) {
//初始化列
f[i][0] = 1;
}
for (let j = 0; j < n; j++) {
//初始化行
f[0][j] = 1;
}
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
f[i][j] = f[i - 1][j] + f[i][j - 1];
}
}
return f[m - 1][n - 1];
};
//状态压缩
var uniquePaths = function (m, n) {
let cur = new Array(n).fill(1);
for (let i = 1; i < m; i++) {
for (let r = 1; r < n; r++) {
cur[r] = cur[r - 1] + cur[r];
}
}
return cur[n - 1];
};
63.不同路径2
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
思路:和上一题一样,区别就是遇到障碍时需要返回0
- 复杂度:时间复杂度
O(mn)
,空间复杂度O(mn)
,状态压缩之后是o(n)
var uniquePathsWithObstacles = function (obstacleGrid) {
const m = obstacleGrid.length;
const n = obstacleGrid[0].length;
const dp = Array(m)
.fill()
.map((item) => Array(n).fill(0)); //初始dp数组
for (let i = 0; i < m && obstacleGrid[i][0] === 0; ++i) {
//初始列
dp[i][0] = 1;
}
for (let i = 0; i < n && obstacleGrid[0][i] === 0; ++i) {
//初始行
dp[0][i] = 1;
}
for (let i = 1; i < m; ++i) {
for (let j = 1; j < n; ++j) {
//遇到障碍直接返回0
dp[i][j] = obstacleGrid[i][j] === 1 ? 0 : dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
};
总结
同学想爬泰山吃淄博烧烤,就先学到这儿了,动态规划的问题还是很有意思的。不同路径的算法优化和其他动态规划问题的求解就放到下一章吧。