在了解动态规划前,不妨先看看题目 http://acm.hdu.edu.cn/showproblem.php?pid=2046。截图如下.
问你当n为任意数(n>0)时, 骨牌的铺放方案有几种?
咱可以先从归纳法试试,列举出可能出现的解。
当n=1时,只有一种铺放方案,即 |
当n=2时,有两种方案,即 || 或 =
当n=3时,有||| ,|= ,=|
当n=4时,有||||, |=|, =||, ||=, ==
当n=5时,有。。。8种,我不写了。。
看到这里,你发现这数字有什么规律没有?他们的解序列为1,2,3,5,8 。。。下一个数字是?
智商达到正常人水平的已经看出来下个数字是13了,如果没想出来,面壁思过去(▼ヘ▼#)
设解数列表示为X[n], 则对于任意n>2有 X[n] = X[n-1]+X[n-2], X[1] = 1, X[2] = 2
但这只是归纳抖机灵,有什么逻辑推理依据使等式成立吗?
当然有!
仔细想想,X[n]是不是就相当于从X[n-1]的最右摆放上一个|,也相当于从X[n-2]的最右摆放上一个=?
那么这个公式就很明显成立, X[n] = X[n-1] + X[n-2] !
啥,你问我为什么不在X[n-2]的最右摆上一个|| ? 嗯,,你可以画一下,在X[n-2]的最右摆上一个||出现的情况都被 在X[n-1]加上一个 | 的情况包含了。
再来看一道题,小明腿短,他一次只能上一步台阶,或者上两步台阶,问他从第0台阶上到第n台阶有多少种走法?
当n=1时,只有一种,小明往上迈一步。
当n=2时,有两种,一种是直接迈两步,一种是迈完一步在迈两步。
当n=3时,有3种,从n=1的情况迈2步,加上n=2的两种情况。
当n=4时,有5种,从_____________, 加上____________ →_→ (填空题)
如果你没看懂。那就重新看一遍吧(笑哭)
以上两道题都是非常经典的斐波那契数列,即下一个数等于前两项的和。
说了这么多还没说代码。接下来我们来实现一下斐波那契数列吧。
我让老铁用递推的方式实现斐波那契,这时候老铁很自信的写出了如下代码:
def Fibonacci(n):
if n==1:
return 1
if n==2:
return 2
return Fibonacci(n-1)+Fibonacci(n-2)
我问老铁你这时间复杂度,空间复杂度是多少。老铁还蜜汁自信的回答说O(n), O(n).
我气的差点当场去世。。
可以画一下调用树分析分析。只画了n=5的情况如下:
上图可见n=3的情况下还被多次计算,当n更大时重复多次计算会更明显,时间复杂度O(2^n),当n很大时,计算机就算不动了。(实测n=40就要算几十秒了)
我说老铁,你这写的是递归啊,我要的是递推!
何为递推,就像上面我们在分析问题的时候,我们可以从现有的方案中递推到下一个,即为自底向上的思考方式,而不是自顶向下。
这时候老铁有想法了,很快写出了以下代码。
def Fibonacci(n):
if n==1:
return 1
if n==2:
return 2
a, b = 1, 2
for i in range(2, n):
b, a = b+a, b
return b
这下可以了,老铁成功掌握了递推的思想,时间复杂为O(n)
这时候杠精要说了,斐波那契数列有通项公式,你为啥不用通项公式方法去计算呢?这。。我还不是要为了引入下一个关键点嘛。
好,先看例题 https://leetcode-cn.com/problems/longest-increasing-subsequence/
老铁编码能力强悍,直接枚举出所有可能的解,很自信的写下以下代码:
def solve(num: list):
def dfs(depth: int, count: int, last: int):
"""
递归遍历所有可能出现的上升序列
:param depth: 递归深度
:param count: 当前序列的解,即上升子序列长度
:param last: 当前序列的最后一个数字,后面的数字必须比这个数字大
:return: 从数组第depth个元素开头的子序列的解
"""
if depth >= len(num):
return count
t1 = 0
if num[depth] > last:
# 当前数字比上一个数字大,可入队
t1 = dfs(depth + 1, count + 1, num[depth])
t2 = dfs(depth + 1, count, last)
return max(t1, t2)
ans = 0
for idx, item in enumerate(num):
ans = max(ans, dfs(idx, 1, item))
return ans
这个代码基本上就是暴力遍历法的解,但是有个最坏的情况是假如整个数组都是上升序列,那么每种可能出现的排列都会被遍历过,那么整体的时间复杂度就是O(2^n)。有没有什么办法优化呢?
Ok,那么接下来就引入我们本篇的主角了,当当当,动态规划来了。我们想想能否设计一种状态转移方程,就像斐波那契数列那样,问题的解从方程X[n] = X[n-1] + X[n-2]推导得出呢?
我们先从数组长度为1的情况考虑,很明显解为1,我们记下来X[1] = 1.
数组长度为2时呢,当a[2] > a[1] 时,解为2,否则为1.我们记下来X[2]=2 或 X[2]=1
数组长度为3时呢,看看a[3] 是否大于 a[1] 或者a[2],如果都是,则从X[1]中X[2]找到最大值,使X[3] = max(X[1], X[2])。如果a[3] 都比a[1], a[2]小,记下X[3] = 1。 如果a[3]只比a[1]或者a[2]一个大,记下X[3]=2