给定一个数字数组,找出数组中最长递增子序列的长度。子序列不一定是连续的。
例如,给定数组[0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15],最长的递增子序列长度为6 : 是 0, 2, 6, 9, 11, 15。
上述问题的解决方案之一在 for 循环中使用非尾递归,但我无法理解它。不明白for循环中递归调用后的代码是什么时候执行的,无法可视化整个方案的整个执行过程。
def longest_increasing_subsequence(arr):
if not arr:
return 0
if len(arr) == 1:
return 1
max_ending_here = 0
for i in range(len(arr)):
ending_at_i = longest_increasing_subsequence(arr[:i])
if arr[-1] > arr[i - 1] and ending_at_i + 1 > max_ending_here:
max_ending_here = ending_at_i + 1
return max_ending_here
解决方案说明如下:
<块引用>假设我们已经有了一个函数,它为我们提供了最长递增子序列的长度。然后我们将尝试将输入数组的某些部分返回给它并尝试扩展结果。我们的基本情况是:空列表,返回 0,以及一个只有一个元素的数组,返回 1。
那么,
i
直到倒数第二个元素,计算 longest_increasing_subsequence
到那里。arr[i]
,我们只能用最后一个元素扩展结果(否则它不会增加)。来源:https://www.dailycodingproblem.com/blog/longest-increasing-subsequence/
我不明白for循环中递归调用后的代码什么时候执行是什么意思。这是我的理解:
lis([0, 8, 4, 12, 2])
。arr = [0, 8, 4, 12, 2]
不满足两种基本情况中的任何一种。i = 0
行中的 ending_at_i = lis([])
时进行第一次调用。这是第一个基本情况,所以它返回 0。我不明白为什么控制不返回到 for 循环,以便 ending_at_i
设置为 0,并执行 if 条件(因为它肯定不是't check else [][-1]
会抛出错误),之后我们可以继续进行 for 循环,在 i = 1
时进行第二次调用,在 i = 2
时进行第三次调用,这将分为两次调用,等等。答案 0 :(得分:2)
以下是此功能的工作原理。首先,它处理列表长度为 0 或 1 的退化情况。
然后寻找当列表长度>=2时的解。最长序列有两种可能:(1)它可能包含列表中的最后一个数字,或者(2)它可能不包含最后一个列表中的编号。
对于情况(1),如果列表中的最后一个数字是最长序列,那么最长序列中它前面的数字必须是较早的数字之一。假设序列中它前面的数字在位置 x。那么最长的序列是从列表中的数字中取出的最长序列,直到并包括 x,加上列表中的最后一个数字。因此,它在 x 的所有可能位置(从 0 到列表长度减 2)上递归。它在 i
上迭代 range(len(arr))
,即从 0 到 len(arr)-1)
。但它随后使用 i
作为切片中的上限,因此切片中的最后一个元素对应于索引 -1
到 len(arr)-2
。在 -1
的情况下,这是一个空切片,它处理列表中最后一个之前的所有值 >= 最后一个元素的情况。
这将处理情况 (1)。对于情况(2),我们只需要从子列表中找到排除最后一个元素的最大序列。但是,发布的代码中缺少此检查,这就是为 [1, 2, 3, 0]
之类的列表给出错误答案的原因:
>>> longest_increasing_subsequence([1, 2, 3, 0])
0
>>>
显然在这种情况下正确答案是3
,而不是0
。这很容易修复,但不知何故被排除在发布的版本之外。
此外,正如其他人指出的那样,每次递归创建一个新切片是不必要且低效的。只需传递子列表的长度即可获得相同的结果。
答案 1 :(得分:0)
这是一个(希望足够好)解释:
ending_at_i
= 在 arr
索引处剪辑 i-th
时 LIS 的长度(即考虑元素 arr[0], arr[1], ..., arr[i-1]
。
if arr[-1] > arr[i - 1] and ending_at_i + 1 > max_ending_here
if arr[-1] > arr[i - 1]
= 如果 arr
的最后一个元素大于对应于 arr
的 ending_at_i
部分的最后一个元素
if ending_at_i + 1 > max_ending_here
= 如果将 arr
的最后一个元素附加到计算过程中找到的 LIS ending_at_i
大于当前最佳 LIS
递归步骤是:
让预言机告诉你 LIS 在 arr[:i]
(= arr[0], arr[1], ..., arr[i-1]
) 中的长度
arr
的最后一个元素,即 arr[-1]
,大于 arr[:i]
的最后一个元素,那么无论 arr[:i]
中的 LIS 是什么,如果你拿它并附加arr[-1]
,它仍然是一个LIS,只是它会大一个元素检查 arr[-1]
是否实际大于 arr[i-1]
,(= arr[:i][-1]
)
检查将 arr[-1]
附加到 arr[:i]
的 LIS 是否创建了新的最优解
重复 1., 2., 3. for i in range(len(arr))
。
结果将是 arr
内 LIS 长度的知识。
话虽如此,由于该算法的递归子步骤在 O(n)
中运行,因此该问题几乎没有更糟糕的可行解决方案。
您标记了 dynamic programming
,然而,这恰恰是这种情况的反例。 Dynamic programming
允许您重用子问题的解决方案,而这正是该算法没有做的,因此浪费时间。改为查看 DP 解决方案。