python_algorithm
2023/03/17
2-minute read
Python 算法及数据结构
记不得过去的人,注定要重蹈覆辙
动态规划的本质就是用空间换时间,很多时候我们不在乎程序浪费了多少空间,因为现在的存储空间很廉价,但是我们很在乎时间,它非常昂贵。
斐波那契数列
def fib(n):
if n < 0:
return 0
if n == 1:
return 1
return fib(n - 1) + fib(n - 2)
def fib_dy(n):
if n < 0:
return 0
dp = [-1] * (n + 1)
print(dp)
dp[1] = dp[2] = 1
for i in range(3, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
print(dp)
return dp[n]
start = time.time()
print(fib_dy(1000))
end = time.time()
print(end - start)
斐波那契数列理解起来非常简单,如果不是亲自实验,我从未想过简单的改变能对性能有巨大的提升。
fib 函数是根据定义写出来的,我们通常都是使用递归的方式去实现,当然代码不一定这么写,但是思想基本都是递归的思想。试着想一下,用递归思想,当我要计算 fib(1000) 的时候,我要先计算 999 和998,而计算 999 需要先计算998和997,那么仅仅这里就不难看到计算 1000 需要计算 998,计算 999 也需要计算 998,也就是计算了两次,依此类推,递归计算量是很大的。
fib_dy 函数是根据动态规划来计算的,先建立一个 n+1 的数组,每一次的数据都保存下来了,不需要重复的计算。
fib_dy(1000) 所花费的时间也不过 0.0059s
fib(1000) 则直接无法计算,递归的嵌套太深了,我们当然可以通过优化代码来使其生效,但是计算 fib(20) 所花费的时间就已经达到了 0.0029s,计算 fib(30) 达到了 0.337s
动态规划升级版本,从一维到二维
一个机器人位于 m 乘以 n 个网格左上角,机器人每次只能向下或者向右移动一步 到达右下角有几种路径?
m 为行数 n 为列数
解题思路:
由于机器人只能向右或者向下行动,不可以逆
状态定义:dp(i,j)表示从左上角走到(i,j)路径的数量,例如一个2*2的网格,从左上角到右下角只有两种可能,先右后下,先下后右。
状态转移方程:dp(i,j) = dp(i-1,j)+dp(i,j-1)
m, n = 3, 7
dp = [[0] * n for _ in range(m)]
# 初始化dp数组
dp[0][0] = 1
for i in range(m):
dp[i][0] = 1
for j in range(n):
dp[0][j] = 1
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
# 数组下标是从 0 开始的,通过查表的方式得到结果
print(dp)
print(dp[m - 1][n - 1])
m, n = 3, 7
dp = np.ones([m, n])
print(dp)
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
print(dp)
numpy 的方式看的更清楚一些, numpy 结果
[[ 1. 1. 1. 1. 1. 1. 1.]
[ 1. 2. 3. 4. 5. 6. 7.]
[ 1. 3. 6. 10. 15. 21. 28.]]
我们可以看到,从左上角到右下角有28种方式,28实际上是21+7的结果,因为要到达28这个点,根据题意,只能是从21或者是从7这个点到达。
让我们看看7*7的表格
[[ 1. 1. 1. 1. 1. 1. 1.]
[ 1. 2. 3. 4. 5. 6. 7.]
[ 1. 3. 6. 10. 15. 21. 28.]
[ 1. 4. 10. 20. 35. 56. 84.]
[ 1. 5. 15. 35. 70. 126. 210.]
[ 1. 6. 21. 56. 126. 252. 462.]
[ 1. 7. 28. 84. 210. 462. 924.]]
我们通过这种方式,无论是想要查询哪一个都是非常轻而易举的行为 print(dp[m-1][n-1])