动态规划问题在各种面试中花式提问,实际工作中许多问题也需要动态规划思想编写代码,最近学习了动态规划思想,和大家分享一下。
一、走楼梯问题
一座10层楼梯一次只允许走 1 步 或者 2 步,那么有多少种方法走完这10层楼梯。
先分析问题:
从数学角度上讲就寻找1 与 2 的排列组合,相加等于 10,统计有多少种组合。
1、排列组合
对于每一阶,都有两种走法,走 1 步 或者 2 步,为了遍历所有的走法,写一个循环
对于时间复杂度 O(2^N),空间复杂度 O(N)
func getCombination(level,N int)(res int){ //level表最大的步长,N表示总共有多少层台接
//当步长小于0 返回失败
if level <= 0{
return -1
}
//level表示最大的步长,
for i := 1; i <= level; i++{
if i == N {
res += 1
break
}
for j := 1; j <= level; j++{
if N == i + j{
res++
break
}
for k := 1; k <= level; k++{
if N == i + j + k{
res++
break
}
for m := 1; m <= level; m++{
if N == i + j + k + m{
res++
break
}
}
}
}
}
return res
}
这段代码实现4层台阶时,获取排列组合的种数。
2、动态规划(递归)
走到第 10 级,必须走到第 8 级 或者第 9 级,而走到第 9 级 又需要走到8 或者 7,循此下去
以F(N)表示 N级楼梯的走法数量,有以下规律:
F(10) = F(9) + F(8)
F(9) = F(8) + F(7)
…
F(2) = 2
F(1) = 1
这种其实就是斐波那契数列
func Fibo(N int)(int){
if N == 1{
return 1
}
if N == 2{
return 2
}
return Fibo(N -1) + Fibo(N -2)
}
由于每次调用Fibo()都只计算了一次,因此递归调用次数就是其时间复杂度。又由于并没有额外分配空间,因此最深的递归所使用的最大栈的数量,就是其空间复杂度。
递归调用图示如下
可见这是一个完全二叉树,深度为 N -1,最大时间复杂度位 O(N ),其节点数量不好估量,但也可以当深度区域无尽大时,时间复杂度O(2^N)
对比排列组合和递归式的动态规划发现除了代码减少之外在时间复杂度和空间复杂度上没有多大优化,所以是否有优化空间呢。仔细观察递归调用的二叉树,可以发现:其中有很多的重复计算,如:计算F(9)时,调用了一次F(7),而计算F(8)时,又调用了一次F(7)。如图所示,颜色相同的都属于重复计算。
为了减少重复计算的,我们可以将已经计算过的放在全局变量中,每次进来先判断
3、动态规划(备忘录法)
var mnote sync.Map //存放当前已经计算出的结果
func Fibo(N int)(int){
if N == 1{
return 1
}
if N == 2{
return 2
}
//之前如果已经算出通过N直接获取,并返回当前N的排列组合种数
if res, ok := mnote.Load(N);ok{
if Ret,ok := res.(int);ok{
return Ret
}
}
tep := Fibo(N -1) + Fibo(N -2)
//将已经计算过的N的排列组合种数存入同步map
mnote.Store(N, &tep)
return tep
}
这样我们每一种只计算一次,当最大深度位N时,时间复杂度位 O(N),空间复杂度并未发生更改O(N)。
4、动态规划(自底向上法)
对于方法三,是否还有优化的方法?暂时脱离楼梯算法,只考虑斐波那契数列的,我们要计算第10个数,通常是从第一个开始计算,而不会为了计算Fibo(10),先计算 Fibo(9),这种情况下,为了计算第N个,只需记录N-1和N-2两个数即可,无需将所有的结果都保存下来。此即为动态规划实现之自底向上思想。
func BottomtoUp(N int)(int){
if N == 1{
return 1
}
if N == 2{
return 2
}
a := 1
b := 2
for index := 3; index <= N; index++{
tmp := a + b
a = b
b = tmp
}
return b
}
这样时间复杂度上O(N),空间复杂度 O(1).
现在总结一下动态规划思路:
- 动态规划思想
把一个问题划分为不同阶段,根据前一阶段的最优解,计算后一阶段的最优解,最终得到一个全局最优解。 - 适用范围
最优子问题:最优解可以从子问题的最优解推断出来 - 求解步骤
(1) 划分子问题:一个技巧是,确定根据输入变量,分别尝试
(2) 确定状态转移方程:如斐波那契数列的F(N) = F(N-1) + F(N-2)
(3) 确定边界条件:初始阶段的子问题的解,如斐波那契数列的F(1) = 1,F(2) = 2