繁体   English   中英

for循环中的非尾递归

[英]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 循环中递归调用后的代码何时执行。 以下是我的理解:

  1. 一些代码调用lis([0, 8, 4, 12, 2])
  2. arr = [0, 8, 4, 12, 2]不满足两种基本情况中的任何一种。
  3. 当行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作为切片中的上限,因此切片中的最后一个元素对应于索引-1len(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_iarr部分的最后一个元素

if ending_at_i + 1 > max_ending_here = 如果将arr的最后一个元素附加到计算过程中找到的LIS中ending_at_i大于当前最佳LIS

然后递归步骤是:

  1. 让预言机告诉你arr[:i] (= arr[0], arr[1], ..., arr[i-1] ) 中 LIS 的长度

    • 意识到,如果arr的最后一个元素,即arr[-1] ,大于arr[:i]的最后一个元素,那么无论arr[:i]的 LIS 是什么,如果你拿它并追加arr[-1] ,它仍然是一个 LIS,除了它会大一个元素
  2. 检查arr[-1]是否实际上大于arr[i-1] ,(= arr[:i][-1] )

  3. 检查将arr[-1]附加到arr[:i]的 LIS 是否创建了新的最优解

  4. for i in range(len(arr))重复 1., 2., 3. 。

  5. 结果将是arr LIS 长度的知识。

话虽如此,由于该算法的递归子步骤在O(n)运行,因此该问题几乎没有更糟糕的可行解决方案。

您标记了dynamic programming ,但是,这恰恰是这种情况的反例。 Dynamic programming使您可以重用子问题的解决方案,而这正是该算法没有做到的,因此是在浪费时间。 改为查看 DP 解决方案。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM