【问题标题】:Counting paths in a recursive call计算递归调用中的路径
【发布时间】:2016-09-20 22:00:24
【问题描述】:

我正在解决的问题是,来自 Cracking the Coding Interview:

“一个孩子正在跑上 n 步的楼梯,可以跳 1 步,2 步, 或一次 3 个步骤。实现一个方法来计算有多少种可能的方式 孩子可以跑上楼梯。”

来自 C++ 我知道计数器可以作为引用传递,但在 python 中你不能。我也在尝试跟踪导致成功的步骤顺序。我正在编写这样的代码:

def __calculatePaths(currPathLength, paths, currSeries):
  if currPathLength == 0:
    print "successful series is", currSeries
    return 1
  elif currPathLength < 0: return 0

  for i in range(1, 4):
    newSeries = list(currSeries)  # make new series to track steps
    newSeries.append(i)
    paths += __calculatePaths(currPathLength - i, paths, newSeries)
  return paths

def calculatePaths(pathLength):
  paths = __calculatePaths(pathLength, 0, [])
  return paths

if __name__ == '__main__':
    calculatePaths(3)

这个调用的输出是:

successful series is [1, 1, 1]
successful series is [1, 2]
successful series is [2, 1]
successful series is [3]
6

我很困惑,因为我的程序获得了正确的路径序列,但路径数量错误。我应该如何增加我的路径?我知道如何在没有全局变量的情况下执行此操作,但如果不使用全局变量我无法弄清楚。谢谢!

【问题讨论】:

  • 你能用数学吗,还是必须是蛮力?
  • 蛮力解决方案将最适用于其他递归问题(因为我无法理解在递归问题中使用计数器的一般原则)
  • 在 Python 中,如果将计数器设为容器(如列表)并将其作为参数传递,则实际上是通过引用传递它。另外,如果你要编写 Python 代码,我强烈建议你关注PEP 8 - Style Guide for Python Code

标签: python recursion counter


【解决方案1】:

最重要的是,您不必确定这些序列:您只需计算它们。例如,从第 N-1 步开始只有一种方法可以完成:第 1 步。从 N-2 开始,有两种方法:一次跳两个步骤,或者跳 1 步并从那里完成。我们的“完成方式”列表现在看起来像这样,向后工作:

way = [1, 2, ...]

现在,看看步骤 N-3 会发生什么。我们最终有 3 个选择:

  1. 跳 1 步,有 2 种方法完成
  2. 跳 2 步,有 1 种方法完成
  3. 跳 3 步即可完成。

总共是 2+1+1,也就是 4 种完成方式。

这会初始化我们的算法。现在为递归关系。初始列表如下所示:

way = [1, 2, 4, ...]

从这里开始,我们无法单步登顶。相反,我们必须依赖于我们上面的三个步骤。我们从 N-J 步中的选择是:

  1. 跳 1 步,有 way[J-1] 种方法完成
  2. 跳两步,有 way[J-2] 种方式完成
  3. 跳 3 步,有 way[J-3] 种方式完成

因此,对于所有 j >= 3:

way[j] = way[j-1] + way[j-2] + way[j-3]

这会在 O(N) 时间内为您提供解决方案。

【讨论】:

    【解决方案2】:

    在您的函数__calculatePaths 中,您必须在for 循环之前设置paths = 0。否则,它会将值添加到路径的全局实例中,这就是您得到错误答案的原因。

    def __calculatePaths(currPathLength, paths, currSeries):
      if currPathLength == 0:
        print "successful series is", currSeries
        return 1
      elif currPathLength < 0: return 0
      paths = 0
      for i in range(1, 4):
        newSeries = list(currSeries)  # make new series to track steps
        newSeries.append(i)
        paths += __calculatePaths(currPathLength - i, paths, newSeries)
      return paths
    
    def calculatePaths(pathLength):
      paths = __calculatePaths(pathLength, 0, [])
      return paths
    
    if __name__ == '__main__':
        calculatePaths(3)
    

    您可以以非常有效的方式获得方法的数量。通过在 O(N) 中使用动态规划。甚至更有效地使用矩阵求幂 O(logN)。

    【讨论】:

    • 路径的全局实例是什么意思?每次递归调用传入的时候不是都做路径的副本吗?
    • 由于您没有将paths 的值重置为0,它会将递归函数调用返回的值添加到之前使用的相同paths 变量中。因此,由于您来自 C++ 背景,您可以将其视为 paths 变量被视为全局变量。并且每次,任何值被添加到 paths ,该值被添加到全局 paths 变量
    【解决方案3】:

    这应该是使用您的解决方案进行计算的最有效方法:

    from collections import deque
    
    
    def calculate_paths(length):
        count = 0  # Global count
    
        def calcuate(remaining_length):
            # 0 means success
            # 1 means only 1 option is available (hop 1)
            if remaining_length < 2:
                nonlocal count  # Refer to outer count
                count += 1
                return
    
            # Calculates, removing the length already passed.
            # For 1...4 or remaining_length+1 if it's less than 4.
            # deque(, maxlen=0) is the fastest way of consuming an iterator
            # without also keeping it's data. This is the most efficient both
            # memory-wise and clock-wise
            deque((calcuate(remaining_length-i)
                  for i in range(1, min(4, remaining_length+1))), maxlen=0)
    
        calcuate(length)
        return count
    
    >>> calculate_paths(2)
    2
    >>> calculate_paths(3)
    4
    >>> calculate_paths(4)
    7
    

    如您所见,没有必要保留路径,因为只有剩余的长度很重要。


    @Prune 的答案有更好的算法。在这里实现:

    def calculate_paths(length):
        results = deque((1, 2, 4), maxlen=3)
        if length <= 3:
            return results[length-1]
    
        for i in range(3, length):
            results.append(sum(results))
    
        return results.pop()
    

    消除递归也会导致使用更少的帧,并且不会随着最大递归而停止。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-04-05
      • 2018-11-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多