day2【动态规划】最长的递增子序列

给定一个数组,找出其中最长的递增子序列。
例如,输入如下数组:

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

返回最长的递增子序列长度是3

1 枚举法/暴力搜索

1.1 思路

枚举法的思路是依次从第一个数出发,依次遍历这个数可以到达(符合递增规则)的下一个数。
算法分为两个部分:
① 计算最长递增序列长度 L T S LTS LTS,循环数组中的每个数,分别计算从位置 i i i出发能够得到的最长递增子序列长度记为 L [ i ] L[i] L[i] L T S LTS LTS = m a x { L [ i ] } max\{L[i]\} max{L[i]}
② 计算对于每个位置 i i i L [ i ] L[i] L[i],依次遍历之后的位置 j j j,先判断位置 j j j是否可以到达(是否满足递增条件),然后计算所有可达到 j j j L [ j ] L[j] L[j] L [ i ] L[i] L[i] = 最大的 L [ j ] L[j] L[j] + + + 1 1 1

1.2 代码

计算最长递增序列长度 L T S LTS LTS

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

计算对于每个位置 i i i 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 时间复杂度

枚举法无疑是非常慢的。首先,数组中每个数可以取也可以不取,因此所有排列组合有 2 n 2^n 2n种子序列,复杂度是 O ( 2 n ) O(2^n) O(2n);其次,对于每种子序列都需要遍历一遍,复杂度是 O ( n ) O(n) O(n);所以整个算法的时间复杂度是 O ( n 2 n ) O(n2^n) O(n2n)

2 记忆化搜索/剪枝

2.1 思路

在枚举的过程种,存在很多重复计算。如图
在这里插入图片描述
图中, 4 4 4 3 3 3 被计算了两次。如果能够将 4 4 4 3 3 3 的计算结果 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 思路

前面已经提到可以通过缓存计算过的值提高效率。
下面重新整理一下思路。

将从第 i i i个数出发的最长递增子序列长度记为 d p [ i ] dp[i] dp[i]
将第 i i i个数出发能到达的下一个数 j j j的集合记为 f e a s i b l e [ i ] feasible[i] feasible[i]
状态转移函数可以表示为,
L [ i ] L[i] L[i]= m a x { d p [ j ] + 1 } , j ∈ f e a s i b l e [ i ] max\{dp[j] + 1\},j\in feasible[i] max{dp[j]+1},jfeasible[i]

L T S LTS LTS = m a x { L [ i ] } , i ∈ n max\{L[i]\},i\in n max{L[i]},in

其中, L L L 就是缓存下来的结果。

根据该问题的特点,从最后一个位置出发的最长递增序列长度一定是 1 1 1,因此,可以倒着遍历数组依次求出每个位置的最长递增序列长度。

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值