动态规划

1.< 首先,什么是动态规划呢?>

答:他并不是一种算法,而是一种思想或是策略。是求解决策过程最优解的过程。

2.举个栗子:<找出最长的递增的子序列>

nums = 【1,5,2,4,3】

比如:124/123,但是此处只要求返回最长的递增子序列长度即可

其实最容易想到的方法就是***<暴力枚举/暴力搜索>***

(1)从1出发,循环遍历,得到最长子序列是3,并且在循环过程中实时记录当前最长的子序列长度。

|---|-----|---|---|
| | 1 | | |
| 5 | 2 | 4 | 3 |
| | 4/3 | | |

(2)同样,以相同的方法从5开始循环,发现子序列长度仅为1。

(3)接着,从2开始,最长子序列长度为2。

|---|---|
| 2 | |
| 4 | 3 |

(4)然后,从4出发,从3出发,依次得到1,1的最长子序列。

(5)最后,选出最长的子序列长度,即3

max_len

|------|----|----|----|-----|
| 3 | 1 | 2 | 1 | 1 |
| [1, | 5, | 2, | 4, | 3] |

接下来,我们就来康康算法如何实现。

(1)首先,定义一个函数,他将会返回从数组第i个数字开始的最长子序列长度

def L(nums , i):

(2) 然后,检查i后面的所有数字,将索引标记为j

for j in range (i + 1,len(nums)):

(3)然后,只要这个数比当前数大,也就是说可以得到递增的子序列。之后,我们就可继续调用函数自身,得到j之后的最长子序列长度,然后+1就是总的子序列长度

if nums[j] > nums[i]:

max_len = L(nums, j ) + 1

(4)接着,循环i之后的每个元素

取最大子序列长度 max_len = max(max_len,L(nums, j) + 1)

过程如下:(i=1)

【1,5,2,4,3】L(nums, j ) + 1 = 2 (j=2) max_len=2

i j

【1,5,2,4,3】L(nums, j ) + 1 =2, L(nums, j' ) = L( nums, j ) + 1 (j=3) max_len=3

i j j'

【1,5,2,4,3】L(nums, j ) + 1 = 2  (j=4) max_len=3

i j

【1,5,2,4,3】L(nums, j ) + 1=2 (j=5) max_len=3

i j

(5)最后,return max_len。but这个循环需要有终止条件:if i == len (nums) -1:

return 1

(6)以上五步只是第一个数字的循环过程,所以接下来我们讨论整个数组的循环过程:

在这里,定义另一个函数:

def length_of_LIS(nums) :

return max(L(nums, i) for i in range (len(nums)))

这样,我们就可以遍历整个数组,最后只要在每个索引循环后产生的最长子序列长度取最大值作为整个数组的最大子序列长度即可完成

(7)最后,完整代码如下:

def L(nums , i):

if i == len (nums) -1:

return 1

max_len = 1

for j in range (i + 1,len(nums)):

if nums[j] > nums[i]:

max_len = max(max_len,L(nums, j) + 1)

return max_len

def length_of_LIS(nums) :

return max(L(nums, i) for i in range (len(nums)))

nums = [1,5,2,4,3]

print(length_of_LIS(nums))

但是,考虑到时间复杂度的问题,也就是此方法耗时过多,所以我们可以思考如何进行算法优化

<算法优化>

回到循环过程,我们发现:有重复过程

|---|-----|---|---|
| | 1 | | |
| 5 | 2 | 4 | 3 |
| | 4/3 | | |

就是我们在124的循环过程中到4的时候执行一次判断,在14的循环过程中到4的时候也判断了一次,这样显然执行过程完全相同,因此可以想到不妨第一次就把此执行结果保存下来,这样就可以不用重复计算了。

这里,我们可以使用一个字典(哈希表)memo,记录下从i开始的最长子序列长度,也就是max_len。

在程序运行前先判断这个索引是否在之前的执行过程中出现过。

  • 函数将计算得到的 max_len 存储在 memo 字典中,并以当前元素的索引 i 作为键。这样,下次如果需要计算以相同元素结尾的最长递增子序列的长度,就可以直接从 memo 中获取,而不需要重复计算。这就是 memo[i] = max_len 这行代码的作用。

最后,完整代码如下:

memo = {}

def L(nums , i):

if i in memo:

return memo[i]

if i == len (nums) -1:

return 1

max_len = 1

for j in range (i + 1,len(nums)):

if nums[j] > nums[i]:

max_len = max(max_len,L(nums, j) + 1)

memo[i] = max_len

return max_len

def length_of_LIS(nums) :

return max(L(nums, i) for i in range (len(nums)))

nums = [1,5,2,4,3]

print(length_of_LIS(nums))

这样,我们就得到了动态规划的核心思想:避免重复节点的计算,进而加速整个计算的过程。由于其中用到了字典/哈希表保存中间结果,所以它也称之为"记忆化"搜索,也就是所谓"空间"换"时间",或者"带备忘录"的递归,或者说递归树的"剪枝"(pruning)(即不需对重复的分支计算 )

<迭代/非递归的实现>

避免递归时候的函数调用开销

def length_of_LTS(num) :

n = len(nums)

L = [1] * n # initial value: [1,1,1,1,1]

for i in reversed(range(n)): # i-> 4,3,2,1,0

for j in range(i + 1, n):

if nums[j] > nums[i]:

L[i] = max(L[i], L[j] + 1)

return max(L)
有两层循环,外层从下到上,内层就是图中所示取每次最大子序列长度