为什么针对寻路机器人的这些动态编程解决方案之一比另一个更快?

时间:2019-06-15 00:05:37

标签: python algorithm dynamic-programming memoization

问题

“在网格的左上角有一个机器人。该机器人只能向右或向下移动。网格中可能包含无效/被阻止的单元格,机器人无法踩到这些单元格。请验证是否有通往底部的路径网格左上角的右单元格。”

解决方案

我有两个解决方案,它们都使用备忘录,以防止重新做与天真的实现中相同的工作。

解决方案1:

 1  def findPathCached( cache, grid, dstR, dstC, currentRow, currentColumn ):
 2     if currentRow > dstR or currentColumn > dstC or \
 3        grid[currentRow][currentColumn] == 1:
 4         return False
 5
 6     if currentRow == dstR and currentColumn == dstC:
 7         return True
 8
 9     if cache[currentRow][currentColumn] is None:
 10         cache[currentRow][currentColumn] = \
 11             findPathCached( cache, grid, dstR, dstC, currentRow, currentColumn+1 ) or \
 12             findPathCached( cache, grid, dstR, dstC, currentRow+1, currentColumn )
 13
 14     return cache[currentRow][currentColumn]

解决方案2:

 1 def findPathCachedFailed( cache, grid, dstR, dstC, currentRow, currentColumn ):
 2     if currentRow > dstR or currentColumn > dstC or \
 3        grid[currentRow][currentColumn] == 1:
 4         return False
 5
 6     if cache[currentRow][currentColumn]:
 7         return False
 8
 9     if ( currentRow == dstR and currentColumn == dstC ) or \
 10        findPathCachedFailed( cache, grid, dstR, dstC, currentRow, currentColumn+1 ) or \
 11        findPathCachedFailed( cache, grid, dstR, dstC, currentRow+1, currentColumn ):
 12         return True
 13
 14     cache[currentRow][currentColumn] = True
 15     return False

解决方案2的运行速度比解决方案1可靠。

>在python中使用time.time()对每个函数调用进行一些计时,我可以看到超过10,000次的运行时间(以秒为单位)是

Solution 1: 0.004197101092338562
Solution 2: 0.0036973851680755614

手动运行它,解决方案2很少比解决方案1花费更多的时间。两种解决方案都在同一网格上运行。

我知道差异很小,但是我一直认为解决方案1会比解决方案2更好,因为它可以缓存所有结果,而不仅是失败的路径,因此我感到惊讶的是,解决方案2确实比1更好。 / p>

有人可以帮助我了解为什么解决方案2运行速度更快吗?

3 个答案:

答案 0 :(得分:3)

弄清楚这一点的正确方法是在探查器下运行它(尽管我不知道是否有好的Python探查器)。

但是我认为在解决方案1中有些事情可能效率较低

  1. 在解决方案1中,您首先要检查是否已经到达右下角的单元格,如果是,则要早点返回。如果尚未到达右下角的单元格,则检查缓存并可能跳过一些工作。由于大多数单元格都不是右下角的单元格,因此右下角的测试通常不会导致提前返回。

    在解决方案2中,您首先检查缓存并可能提早返回。如果缓存检查失败,则检查是否到达右下角的单元格。因此,如果经常执行高速缓存检查,则可以跳过在解决方案1中执行的许多右下角检查。

  2. 在解决方案1中,您在第9行和第14行中获取cache[currentRow][currentColumn]。在解决方案2中,您仅在第6行中获取了它。因此,解决方案1的执行次数是其中的两倍作为解决方案2获取。

答案 1 :(得分:1)

原因实际上很简单:当函数返回True时,将结果缓存就没有意义了,因为永远不会读取缓存的结果,因为在此之后将不再发生函数调用,因为当递归调用返回True(表示“我已经找到( dstR dstC ]的路径”)时,整个调用堆栈快速结束,每个呼叫立即返回True(仍然表示“我已经找到了( dstR dstC )的路径”)。

所以这条思路:

  

[…]我以为解决方案1会比解决方案2更好,因为它可以缓存所有结果,而不仅是失败的路径[…]

不起作用,因为那些额外的高速缓存只是无用的写,永远不会被读取(除了立即,因为rob mayoff指出,您在解决方案#中使用return cache[currentRow][currentColumn] 1,但当然可以很容易地更改为只返回一个局部变量。

部分原因是or的短路行为;像

findPathCached( cache, grid, dstR, dstC, currentRow, currentColumn+1 ) or \
findPathCached( cache, grid, dstR, dstC, currentRow+1, currentColumn )

如果第一个函数返回了True,则第二个函数调用不会发生。

如果您希望解决方案1的额外缓存有用,那么您可能会想到一个不可能发生这种短路的问题。例如,不只是返回TrueFalse来指示一条路径是否可能,而是尝试返回多少条路径是可能的-因此,0而不是False,用正数代替True,用+代替or。您会突然发现解决方案1的速度要快得多,因为它可以按时间覆盖整个网格,并且与网格的大小成比例,而解决方案2会重复大量的呼叫,并且花费的时间更长。


偶然地,您可以仅使用 O (min( m n ))多余的空间而不是 O mn )多余的空间,但方法略有不同,仍然只需要 O mn )时间。具体来说:您可以遍历网格的各行,并为每行创建一个列表,列出可以从(0,0)访问该行中的哪些单元格。给定行的列表仅取决于上一行的列表,因此您不需要在迭代时保留所有旧列表。 (实际上是 O n )多余的空间,但我说您可以在 O (min( m n )),因为您可以先检查哪个维度更大,然后在发现行长于列的情况下遍历列而不是行,从而开始额外的空间。

答案 2 :(得分:0)

这更快

 6     if cache[currentRow][currentColumn]:
 7         return False

 6     if currentRow == dstR and currentColumn == dstC:
 7         return True

我只检查对象是否存在。我看不到比较值。我还认为它返回的更快,并且可以停止执行其余的代码。

我还认为,解决方案'1'的这一行也应该比'2'更快,在'2'中,您将获得2个比较和4个布尔运算

 9     if cache[currentRow][currentColumn] is None:

从技术上讲,您有两种优化方法,但是可以检查列表(矩阵)上的操作,或者只是对if语句进行校正。

请记住,某些解决方案可以更快地调用“返回”。

如果没有事件探查器,我只会在一些基本示例中测试一项功能:)