算法:动态规划
本文介绍一种经典的算法思想:动态规划
官方文邹邹定义:
将一个问题拆成几个子问题,分别求解这些子问题,即可推断出大问题的解。
个人理解:未来取决于当下,脱钩于过去
使用DP的场景:能将大问题拆成几个小问题,且满足无后效性、最优子结构性质。
经典算法必须有经典场景:斐波那契数列。
这种数列的一个最大特点: 初始值:f(1) = 1, f(2) = 1,之后的每一项都是前两项的和。所以f(3) = f(1) + f(2) = 2….
1. 爬楼梯
爬楼梯的方式有两种,一次一级或者两级,那么跳到n级,有多少种跳法?
实际就是一个斐波拉契数列,f(1) = 1, f(2) = 1…
const climbStairs = function(n) {
if (n <= 1) return 1
const f = [1,2]
for (let i = 2; i < n; i++) {
f[i] = f[i-1] + f[i-2]
}
return f[n-1]
};
这里可能难以理解的是,f(i)为什么要等于f(i -1) + f(i-2),这俩为什么要相加?列出所有可能性解释
f(1): [1]
f(2): [1,1], [2]
f(3): [1,2], [1,1,1], [2,1]
…
…
排列出来就看得很清晰了。要想到达f(3),只有两种可能:要么在第一级跳两级,要么在第二级出发跳一级,完毕
2. 最长公共子串
双指针逼近:暴力超时版本
function LCS( str1 , str2 ) {
// write code here
const min = str1.length < str2? str1: str2
const max = str1.length < str2? str2: str1
let res = ''
for (let i =0; i < min.length; i++) {
for (let j = min.length - 1; j>= 0; j--) {
const cur = min.slice(i, j+ 1)
if (max.includes(cur)) {
if (res.length < cur.length) {
res = cur
}
}
}
}
return res
}
// 复杂度o(n平方)
动态规划版本
function LCS( str1 , str2 ) {
// write code here
const dp = new Array(str1.length + 1);
let max = 0;
const map = new Map()
for (let i = 0; i <= str1.length; i++) { // 初始化整个dp矩阵,每个值为0
dp[i] = new Array(str2.length + 1).fill(0);
}
for(let i = 1;i<=str1.length;i++){
for(let j = 1;j<=str2.length;j++){
if(str1[i-1] === str2[j-1]){
dp[i][j] = dp[i - 1][j - 1] + 1
max = Math.max(max,dp[i][j])
if(!map.has(max)) map.set(max,i) //避免重复
}
}
}
let startIndex = map.get(max) - max;
let endIndex = map.get(max) ;
return str1.substring(startIndex,endIndex) //截取字符串
}
脑容量有限,愣是没看懂…
3. 不同路径: 一个机器人在m×n大小的地图的左上角(起点)。机器人每次可以向下或向右移动。机器人要到达地图的右下角(终点)。可以有多少种不同的路径从起点走到终点?
思路: 因为只能往右或者往下,因此所有第一行i=0或者第一列j=0的路径均为1.那么从(1,1)开始的每一个格子的路径数目,取决于[i -1, j]和[i, j -1].代码如下:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param m int整型
* @param n int整型
* @return int整型
*/
function uniquePaths( m , n ) {
// write code here
const res = Array(m).fill(Array(n).fill(1))
const arr = Array(m).fill(Array(n).fill(1))
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
res[i][j] = res[i-1][j] + res[i][j-1]
}
}
return res[m - 1][n -1]
}
module.exports = {
uniquePaths : uniquePaths
};
4. 矩阵的最小路径和。给定一个 n * m 的矩阵 a,从左上角开始每次只能向右或者向下走,最后到达右下角的位置,路径上所有的数字累加起来就是路径和,输出所有的路径中最小的路径和。
思路:此问题的思路同3是一致的。代码如下:
function minPathSum( matrix ) {
// write code here
const m = matrix.length
const n = matrix[0].length
// const res = Array(m).fill(Array(n).fill(0))
const res = Array(matrix.length).fill(0).map(() => Array(matrix[0].length).fill(0));
res[0][0] = matrix[0][0]
for (let i = 1; i < n; i++) {
res[0][i] = res[0][i-1] + matrix[0][i]
}
for (let i = 1; i < m; i++) {
res[i][0] = res[i-1][0] + matrix[i][0]
}
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
res[i][j] = Math.min(res[i-1][j], res[i][j-1]) + matrix[i][j]
}
}
return res[m- 1][n-1]
}
此题实际上同上一题几乎一样,单独拎出来的原因,在于注释掉的那一行。
const res = Array(m).fill(Array(n).fill(0))
这种初始化声明的方式会产生一个奇怪的现象:
res[0][0] = matrix[0][0]
一个简单的赋值发现,每一个内层数组的第一项,都被自动赋值…原因暂未查明-_-
最后,把坑填上。之前贪心算法篇中遗留的一个问题: 贪心算法的鼠目寸光,导致的结果错误。出一个动态规划的方案
// 一个土豪,身上带了足够的1、5、10、20、50、100元面值的钞票。现在您的目标是凑出某个金额w,需要用到尽量少的钞票。
// 这里默认三项哈
const f = (arr, target) => {
const all = [0]
arr.sort((a, b) => {
return b - a
})
for (let i = 1; i <= target; i++) {
all[i] = Math.min((i -arr[0]) >= 0? all[i -arr[0]]: 1, (i -arr[1]) >= 0? all[i -arr[1]]: 1, (i -arr[2]) >= 0? all[i -arr[2]]: 1) + 1
}
return all[target]
}
const arr = [1, 5, 11]
let target = 15
console.log(f(arr, target))
// 输出最优解:3
无后效性:如果给定某一阶段的状态,则在这一阶段以后过程的发展不受这阶段以前各段状态的影响。
5. 求出最大和的连续子数组
// 找出和最大的连续字数组
const arr = [-1, 4, -5, 3, 9, -1]
// 1. 复杂度n平方,双层for循环
const f = () => {
let max = -Infinity
let res = null
for (let i = 0; i < arr.length; i++) {
for (let j = i; j < arr.length; j++) {
const cur = arr.slice(i, j + 1)
const sum = cur.reduce((prev, item, index) => {
return prev + item
}, 0)
if (sum >= max ) {
res = [...cur]
max = sum
}
}
}
return res
}
// console.log(f())
// 2. 复杂度n
const f2 = () => {
let sum = 0
let ans = arr[0]
for (let i = 0; i < arr.length; i++) {
if (sum >= 0) {
sum = sum + arr[i]
} else {
sum = arr[i]
}
ans = Math.max(ans, sum)
console.log('sum:',sum)
}
return ans
}
console.log(f2())
总结:仔细思考会发现,所谓的动态规划实际上是存在上帝视角的,为了达到某一个目标,我们索性罗列出了几乎所有的可能,然后选取了最优解。而贪心算法则是每一步都选取了最优解,以期达到最终的最优解。拿人生举例脑海中突然出现了一个画面:一个站在上帝视角玩人生这个主进程游戏,一出场便光速的通过动态规划走完了全程,然后选去了一个最优解。另一个则是一步一个脚印,摸着石头过河,每次选择最优解,直到生命的尽头。在人生的这个特定场景下,贪心算法,才是最优解,因为不存在上帝,我们自己就是上帝。
