先说些题外话:
作为一个算法小白,我在领到最短编辑距离这个讲题的时候头就秃了一半,在后面看动态规划的时候剩下一半还不够我秃的👨🦲。
今天的题目虽然是从最短编辑距离到初识动态规划,但实际上,要解最短编辑距离,还是得从动态规划开始。
动态规划(Dynamic Programming),是一种用于求解最优问题的算法,相对其他算法(如回溯算法:穷举所有可能,时间复杂度为指数级),动态规划可以显著地降低时间复杂度。但是动态规划并不好学,而且因为某种莫名的力量,是众所周知的难学。主要是因为动态规划“求解问题的过程不太符合人类常规的思维方式”。
动态规划将问题分为多个决策阶段,每个阶段做一个决策(选择),记录每个阶段做完决策的状态,通过上一个阶段的状态推导出下一个阶段的状态,动态地推演。(划重点)
0-1背包问题
学习动态规划,主要还是多练,~~所以这是一节习题课…~~下面我们先通过一个非常著名的0-1背包问题,认识一下动态规划。
问:
有一个承重为w的背包,可装载的物品共有n个,在重量分别为weight(一个代表不同物品重量的数组)的物品中,可装载的重量最大值是多少?
如:背包可装9kg,5个物品,提供的物品有[2,2,4,6,3]kg
解:
我们将问题分解为物品总个数个阶段,每个阶段决策当前物品是否装入背包,每个阶段状态即当前总重量是否可达。即,挨个选择要不要这个物品。这个过程可以画一张表格来表示,1表示状态可达,0表示状态不可达。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
---|---|---|---|---|---|---|---|---|---|---|
w=2 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
w=2 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
w=4 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
w=6 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
w=3 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
这个表格我们称为状态转移表,有了状态转移表,就可以用代码来描述这个表格:
定义数组=>初始化边界值=>推演
// weight:物品重量,n:物品个数,w:背包可承载重量
function knapsack(weight = [2,2,4,6,3], n = 5, w = 9) {
// 定义二维数组
let state = []
for(let i = 0; i < n; i++) {
state[i]=[]
}
// 初始化第一行数据
state[0][0]=true
if(weight[0] < w){
state[0][weight[0]] = true
}
// 动态规划状态转移
for(let i = 1; i < n; i++) {
// 决策为不放入背包
for(let j = 0; j <= w; j++) {
if(state[i-1][j]) state[i][j]=state[i-1][j]
}
// 决策为放入背包
for(let j = 0; j <= w - weight[i]; j++) {
if(state[i-1][j]) state[i][j+weight[i]]=true
}
}
for(let i = w; i >= 0; i--) {
if(state[n-1][i]) return i
}
return 0
}
结论:
这串代码的时间复杂度为O(n*w),但是额外申请了一个n*(w+1)的数组。
所以有时候我们说动态规划是拿空间来换时间。
优化:
实际上,这段代码还可以优化成一维数组:
function knapsack2(weight = [2,2,4,6,3], n = 5, w = 9) {
// 定义数组
let state = []
// 初始化
state[0]=true
if(weight[