这不应该使用回溯算法吗?

Shouldn't this be using a backtracking algorithm?

本文关键字:回溯算法 不应该      更新时间:2023-10-16

我正在解决LeetCode上的一些问题。其中一个问题是:

给定一个充满非负数的m x n网格,从左上到右下找到一条路径,该路径使其路径上所有数字的总和最小化。您只能在任何时间点向下或向右移动。

社论以及发布的解决方案都使用动态编程。投票最多的解决方案之一如下:

class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size(); 
vector<vector<int> > sum(m, vector<int>(n, grid[0][0]));
for (int i = 1; i < m; i++)
sum[i][0] = sum[i - 1][0] + grid[i][0];
for (int j = 1; j < n; j++)
sum[0][j] = sum[0][j - 1] + grid[0][j];
for (int i = 1; i < m; i++)
for (int j = 1; j < n; j++)
sum[i][j]  = min(sum[i - 1][j], sum[i][j - 1]) + grid[i][j];
return sum[m - 1][n - 1];
}
};

我的问题很简单:这难道不应该用回溯法来解决吗?假设输入矩阵类似于:


[1,2500]
[1000500500]
[1,3,4]
>

我的怀疑是因为在DP中,子问题的解是全局解(最优子结构)的一部分。然而,如上所述,当我们从(2,100)中选择2时,我们可能是错误的,因为未来的路径可能过于昂贵(2周围的所有数字都是500)。那么,在这种情况下,如何使用动态编程是合理的呢?

总结:

  1. 我们不应该使用回溯吗?因为如果我们之前做出了错误的选择(查看局部最大值),我们可能不得不收回我们的路径
  2. 这怎么是一个动态编程问题

p.S.:上述解决方案肯定有效。

上面所示的例子表明,贪婪的问题解决方案不一定会产生最优解,这一点你是绝对正确的。

然而,这个问题的DP解决方案并没有完全使用这种策略。DP解决方案背后的思想是为每个位置计算在该位置结束的最短路径的成本。在解决整体问题的过程中,DP算法最终会计算通过网格中2的一些最短路径的长度,但在确定返回的整体最短路径时,它不一定使用这些中间最短路径。试着在你的例子中跟踪上面的代码——你看到它是如何计算的,然后就不使用其他路径选项了吗?

我们不应该使用回溯吗?因为如果我们之前做出了错误的选择(查看局部最大值),我们可能不得不收回我们的路径?

在现实世界中,有很多因素将决定哪种算法更适合解决这个问题。

这个DP解决方案是可以的,因为它将在处理最坏的情况时为您提供最佳的性能/内存使用率。

任何回溯/dijkstra/A*算法都需要维护一个完整的矩阵以及一个开放节点列表。这个DP解决方案只是假设每个节点最终都会被访问,所以它可以放弃打开的节点列表,只保留成本缓冲区。

通过假设每个节点都会被访问,它还去掉了算法中"下一个打开哪个节点"的部分。

因此,如果我们正在寻找最坏情况下的最佳性能,那么这种算法实际上很难被击败。但这是否是我们想要的是另一回事。

这是一个动态编程问题吗?

这只是一个动态编程问题,因为它存在动态编程解决方案。但DP决不是解决它的唯一方法。

编辑:在我陷入困境之前,是的,有更高效的内存解决方案,但在最坏的情况下,CPU成本非常高。

对于您的输入

[
[  1,   2, 500]
[100, 500, 500]
[  1,   3,   4]
]

sum阵列结果到

[
[  1,   3,  503]
[101, 503, 1003]
[102, 105,  109]
]

我们甚至可以回溯最短路径:

109, 105, 102, 101, 1

算法不检查每条路径,而是使用它可以采用先前最佳路径的特性来计算当前成本:

sum[i][j] = min(sum[i - 1][j], // take better path between previous horizontal
sum[i][j - 1]) // or previous vertical
+ grid[i][j]; // current cost

回溯本身并不特别适合这个问题。

回溯对于八皇后这样的问题很有效,在这些问题中,所提出的解决方案要么有效,要么无效。我们尝试一条通往解决方案的可能途径,如果失败,我们会回溯并尝试另一条可能途径,直到找到一条可行的途径。

然而,在这种情况下,每一条可能的路线都会让我们从头走到尾。我们不能只是尝试不同的可能性,直到找到一个可行的。相反,我们基本上必须从头到尾尝试每条路线,直到找到效果最好的路线(在这种情况下,权重最低)。

现在,通过回溯修剪,我们可以(也许)在一定程度上改进我们的解决方案。特别是,让我们假设您进行了一次搜索,首先向下看(如果可能的话),然后向侧面看。在这种情况下,对于您第一次尝试的输入,最终将成为最佳路线。

问题是它是否能识别,并在不完全遍历的情况下修剪一些树枝。答案是可以的。要做到这一点,它会跟踪到目前为止找到的最佳路径,并在此基础上,它可以拒绝整个子树。在这种情况下,它的第一条路线给出了109的总重量。然后,它尝试到第一个节点的右侧,该节点是一个2,到目前为止总权重为3。这比109小,所以它继续前进。从那里,它向下看,到达500。这给出了503的重量,所以在不做任何进一步研究的情况下,它知道从那里出发的任何路线都不合适,所以它停下来修剪掉了从500开始的所有树枝。然后它从2开始向右尝试,又找到了500。这样就可以修剪整个枝条。因此,在这些情况下,它从不查看第三个500,或者根本不查看3和4——只需查看500节点,我们就可以确定这些节点不可能产生最优解。

这是否真的是对DP战略的改进,很大程度上取决于运营成本的问题。对于手头的任务来说,这两种方式可能都没有太大区别。然而,如果你的输入矩阵要大得多,它可能会。例如,我们可能在tile中存储了大量输入。使用DP解决方案,我们评估所有可能性,因此我们总是加载所有瓦片。通过修剪树的方法,我们可能能够完全避免加载一些瓦片,因为包括这些瓦片在内的路由已经被消除。