给定一个数组,找出其中最长的递增子序列。
例如,输入如下数组:
nums = [1, 5, 2, 4, 3]
返回最长的递增子序列长度是3
1 枚举法/暴力搜索
1.1 思路
枚举法的思路是依次从第一个数出发,依次遍历这个数可以到达(符合递增规则)的下一个数。
算法分为两个部分:
① 计算最长递增序列长度LTSLTSLTS,循环数组中的每个数,分别计算从位置iii出发能够得到的最长递增子序列长度记为L[i]L[i]L[i],LTSLTSLTS = max{L[i]}max\{L[i]\}max{L[i]}
② 计算对于每个位置iii的L[i]L[i]L[i],依次遍历之后的位置jjj,先判断位置jjj是否可以到达(是否满足递增条件),然后计算所有可达到jjj的L[j]L[j]L[j],L[i]L[i]L[i] = 最大的L[j]L[j]L[j] +++ 111
1.2 代码
计算最长递增序列长度LTSLTSLTS
def length_of_LIS(nums):
return max(L(nums, i) for i in range(len(nums)))
计算对于每个位置iii的L[i]L[i]L[i]
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
1.3 时间复杂度
枚举法无疑是非常慢的。首先,数组中每个数可以取也可以不取,因此所有排列组合有2n2^n2n种子序列,复杂度是O(2n)O(2^n)O(2n);其次,对于每种子序列都需要遍历一遍,复杂度是O(n)O(n)O(n);所以整个算法的时间复杂度是O(n2n)O(n2^n)O(n2n)
2 记忆化搜索/剪枝
2.1 思路
在枚举的过程种,存在很多重复计算。如图
图中,444 和 333 被计算了两次。如果能够将 444 和 333 的计算结果 L[4]L[4]L[4] 和 L[3]L[3]L[3] 存储下来,将提高算法效率,达到以空间换时间的效果。
这种方法也被称为,记忆化搜索或者剪枝。
2.2 代码
将穷举法中的第二部分修改一下,用字典memo存储计算过的值。
# memo用于存储计算过的 L[i]
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)
# 将当前计算的 L[i] 保存
memo[i] = max_len
return max_len
3 改写成迭代形式
3.1 思路
前面已经提到可以通过缓存计算过的值提高效率。
下面重新整理一下思路。
将从第iii个数出发的最长递增子序列长度记为dp[i]dp[i]dp[i],
将第iii个数出发能到达的下一个数jjj的集合记为feasible[i]feasible[i]feasible[i],
状态转移函数可以表示为,
L[i]L[i]L[i]=max{dp[j]+1},j∈feasible[i]max\{dp[j] + 1\},j\in feasible[i]max{dp[j]+1},j∈feasible[i]
LTSLTSLTS = max{L[i]},i∈nmax\{L[i]\},i\in nmax{L[i]},i∈n
其中,LLL 就是缓存下来的结果。
根据该问题的特点,从最后一个位置出发的最长递增序列长度一定是 111,因此,可以倒着遍历数组依次求出每个位置的最长递增序列长度。
3.2 代码
完整的代码如下
from Tools.TimeTools import time_function
@time_function
def length_of_LIS(nums):
n = len(nums)
L = [1] * n
for i in reversed(range(n)):
for j in range(i + 1, n):
if nums[j] > nums[i]:
L[i] = max(L[i], L[j] + 1)
return max(L)
nums = [1, 5, 2, 4, 3]
print(length_of_LIS(nums))
为了统计算法运行的时间,这里写了个统计时间的装饰器,代码放在最后。
4 总结
动态规划基本思路:
4.1 枚举
首先,可以先将所有的答案枚举出来,并画出递归树,尝试用一个递归函数求解;
4.2 剪枝
如果发现枚举中出现大量的重复计算,可以尝试用哈希表将数据缓存下来,之后遍历到相同的节点就直接查表,避免重复计算;
4.3 非递归
最后,将计算的过程表示出来,观察公式求解的顺序,并尝试将递归形式改写成更简洁高效的迭代形式
5 统计时间装饰器代码
import time
def time_function(function, *args):
"""
统计函数执行时间的装饰器
"""
def wrapper(*args):
start_time = time.time()
result = function(*args)
end_time = time.time()
print(f"函数 {function.__name__} 运行时间为:{(end_time - start_time):.6f} 秒")
return result
return wrapper
@time_function
def my_algorithm(arg1, arg2):
# 执行某些算法操作
pass