for循环中的非尾递归

时间:2021-07-11 14:54:52

标签: python recursion

<块引用>

给定一个数字数组,找出数组中最长递增子序列的长度。子序列不一定是连续的。

例如,给定数组[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. for 循环在 i = 0 行中的 ending_at_i = lis([]) 时进行第一次调用。这是第一个基本情况,所以它返回 0。我不明白为什么控制不返回到 for 循环,以便 ending_at_i 设置为 0,并执行 if 条件(因为它肯定不是't check else [][-1] 会抛出错误),之后我们可以继续进行 for 循环,在 i = 1 时进行第二次调用,在 i = 2 时进行第三次调用,这将分为两次调用,等等。

2 个答案:

答案 0 :(得分:2)

以下是此功能的工作原理。首先,它处理列表长度为 0 或 1 的退化情况。

然后寻找当列表长度>=2时的解。最长序列有两种可能:(1)它可能包含列表中的最后一个数字,或者(2)它可能不包含最后一个列表中的编号。

对于情况(1),如果列表中的最后一个数字是最长序列,那么最长序列中它前面的数字必须是较早的数字之一。假设序列中它前面的数字在位置 x。那么最长的序列是从列表中的数字中取出的最长序列,直到并包括 x,加上列表中的最后一个数字。因此,它在 x 的所有可能位置(从 0 到列表长度减 2)上递归。它在 i 上迭代 range(len(arr)),即从 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。这很容易修复,但不知何故被排除在发布的版本之外。

此外,正如其他人指出的那样,每次递归创建一个新切片是不必要且低效的。只需传递子列表的长度即可获得相同的结果。

答案 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 的最后一个元素大于对应于 arrending_at_i 部分的最后一个元素

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

递归步骤是:

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

    • 意识到,如果 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. 重复 1., 2., 3. for i in range(len(arr))

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

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

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