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

时间:2017-08-13 21:08:39

标签: c++ algorithm dynamic-programming

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

  

给定amxn网格填充非负数,找到从左上角到右下角的路径,这样可以最小化路径上所有数字的总和。您只能在任何时间点向下或向右移动。

所发布的社论和解决方案都使用动态编程。最受赞誉的解决方案之一如下:

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,2,500]
          [100500500]
          [1,3,4]
       ]

我怀疑是因为在DP中,子问题的解决方案是全局解决方案(最佳子结构)的一部分。但是,如上所示,当我们从2中选择(2,100)进行本地选择时,我们可能会错,因为未来的路径可能过于昂贵(2周围的所有数字都是{{ 1}}为s)。那么,在这种情况下如何使用动态编程是合理的呢?

总结:

  1. 我们不应该使用回溯,因为如果我们之前做出了错误的选择(查看局部最大值),我们可能需要收回路径吗?
  2. 这是一个动态编程问题?
  3. P.S。:上述解决方案肯定会运行。

4 个答案:

答案 0 :(得分:3)

上面说明的示例显示问题的贪婪解决方案不一定能产生最佳解决方案,而且您对此完全正确。

但是,针对此问题的DP解决方案并未完全使用此策略。 DP解决方案背后的想法是为每个位置计算在该位置结束的最短路径的成本。在解决整体问题的过程中,DP算法将最终计算通过网格中2的最短路径的长度,但是当它们不一定使用那些中间最短路径时确定返回的总体最短路径。尝试在示例中跟踪上面的代码 - 您是否看到它如何计算,然后最终不会使用其他路径选项?

答案 1 :(得分:2)

  

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

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

这种DP解决方案可以在处理最坏情况时为您提供最佳性能/内存使用。

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

通过假设每个节点都将被访问,它也摆脱了&#34;我接下来要打开哪个节点&#34;算法的一部分。

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

  

这是一个动态编程问题?

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

编辑:在我开始扣篮之前,是的,有更多内存效率的解决方案,但在最糟糕的情况下,CPU成本非常高。

答案 2 :(得分:2)

供您输入

[
[  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

答案 3 :(得分:1)

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

回溯适用于像八个皇后这样的问题,建议的解决方案既可以使用,也可以不运行。我们尝试了一种可能的解决方案路线,如果失败了,我们会回溯并尝试另一种可能的路线,直到找到一种有效的路线。

然而,在这种情况下,每条可能的路线都会从开始到结束。我们不能尝试不同的可能性,直到找到有效的方法。相反,我们必须从头到尾基本上尝试每条路由,直到找到效果最好的路由(在这种情况下权重最低)。

现在,通过回溯修剪,我们可以(或许)至少在某种程度上改进我们对此解决方案的方法。特别是,让我们假设你做了一个搜索,开始向下看(如果可能的话),然后向侧面看。在这种情况下,使用您第一次尝试的输入将最终成为最佳路线。

问题是它是否可以识别,并修剪树的某些分支而不完全遍历它们。答案是肯定的,它可以。为此,它会跟踪到目前为止找到的最佳路线,并且基于此,它可以拒绝整个子树。在这种情况下,它的第一条路线总重量为109.然后它尝试到第一个节点的右边,这是一个2,到目前为止总重量为3。那比小于109,所以它继续下去。从那里,它向下看,并达到500.这给了503的权重,所以没有做任何进一步的观察,它知道没有合适的路线,所以它停止并修剪从500开始的所有分支然后它从2向右尝试并找到另一个500.这使得它也可以修剪整个分支。因此,在这些情况下,它永远不会查看第三个500或者3个和4个 - 仅仅通过查看500节点,我们可以确定那些可能无法产生最佳解决方案

这对DP战略的真正改进是否主要归结为运营成本是多少的问题。对于手头的任务,它可能无论如何都没有太大的区别。但是,如果您的输入矩阵更大,则可能。例如,我们可能会在tile中存储大量输入。通过DP解决方案,我们可以评估所有可能性,因此我们始终会加载所有磁贴。使用树修剪方法,我们可以完全避免加载一些瓷砖,因为包括那些瓷砖的路线已经被消除。