[英]Non-tail recursion within a for loop
给定一个数字数组,找出数组中最长递增子序列的长度。 子序列不一定是连续的。
例如,给定数组 [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]
不满足两种基本情况中的任何一种。ending_at_i = lis([])
中的i = 0
时,for 循环进行第一次调用。 这是第一个基本情况,所以它返回 0。我不明白为什么控制不返回到 for 循环,以便将ending_at_i
设置为 0,并执行 if 条件(因为它肯定没有被检查) [][-1]
会抛出一个错误),之后我们可以继续进行 for 循环,当i = 1
时进行第二次调用,当i = 2
时进行第三次调用,这将分支为两次调用,依此类推。以下是此功能的工作原理。 首先,它处理列表长度为 0 或 1 的退化情况。
然后当列表长度 >= 2 时寻找解。最长序列有两种可能:(1)它可能包含列表中的最后一个数字,或者(2)它可能不包含列表中的最后一个数字列表。
对于情况 (1),如果列表中的最后一个数字是最长序列,则最长序列中它前面的数字必须是较早的数字之一。 假设序列中它前面的数字在位置 x。 那么最长的序列是从列表中的数字中取出的最长序列,直到并包括 x,加上列表中的最后一个数字。 所以它在 x 的所有可能位置上递归,它们是 0 到列表长度减去 2。它在range(len(arr))
迭代i
,它是 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
。 这很容易修复,但不知何故被排除在发布的版本之外。
此外,正如其他人指出的那样,每次递归创建一个新切片是不必要且低效的。 所需要的只是传递子列表的长度以获得相同的结果。
这是一个(希望足够好)解释:
ending_at_i
= 在第i-th
索引处剪辑arr
时的 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
的最后一个元素大于对应ending_at_i
的arr
部分的最后一个元素
if ending_at_i + 1 > max_ending_here
= 如果将arr
的最后一个元素附加到计算过程中找到的LIS中ending_at_i
大于当前最佳LIS
然后递归步骤是:
让预言机告诉你arr[:i]
(= arr[0], arr[1], ..., arr[i-1]
) 中 LIS 的长度
arr
的最后一个元素,即arr[-1]
,大于arr[:i]
的最后一个元素,那么无论arr[:i]
的 LIS 是什么,如果你拿它并追加arr[-1]
,它仍然是一个 LIS,除了它会大一个元素检查arr[-1]
是否实际上大于arr[i-1]
,(= arr[:i][-1]
)
检查将arr[-1]
附加到arr[:i]
的 LIS 是否创建了新的最优解
for i in range(len(arr))
重复 1., 2., 3. 。
结果将是arr
LIS 长度的知识。
话虽如此,由于该算法的递归子步骤在O(n)
运行,因此该问题几乎没有更糟糕的可行解决方案。
您标记了dynamic programming
,但是,这恰恰是这种情况的反例。 Dynamic programming
使您可以重用子问题的解决方案,而这正是该算法没有做到的,因此是在浪费时间。 改为查看 DP 解决方案。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.