为啥在这种情况下只有回溯有效?

Posted

技术标签:

【中文标题】为啥在这种情况下只有回溯有效?【英文标题】:Why does only backtracking work in this scenario?为什么在这种情况下只有回溯有效? 【发布时间】:2020-08-12 03:54:32 【问题描述】:

我正在LeetCode.com解决这个问题:

在大小为 m * n 的金矿网格中,该矿井中的每个单元格都有一个整数表示该单元格中的黄金数量,如果为空则为 0。返回您在以下条件下可以收集的最大金币数量:- (a) 每次您位于一个牢房中时,您都会收集该牢房中的所有金币; (b) 从您的位置,您可以向左、向右、向上或向下走一步。 (c) 您不能多次访问同一个牢房; (d) 永远不要访问 0 金币的牢房。 (e) 您可以从网格中任何有黄金的位置开始和停止收集黄金。 对于网格:[[0,6,0],[5,8,7],[0,9,0]],输出为:24

我写了下面的代码:

class Solution 
public:
    int dig(vector<vector<int>>& grid, int i, int j) 
        if(i>=grid.size() || i<0 || j>=grid[0].size() || j<0 || grid[i][j]==0) return 0;
        
        //change begins...
        int gold=0;
        gold+=grid[i][j];
        grid[i][j]=0;
        gold+=max(dig(grid, i+1, j), max(dig(grid, i, j+1), max(dig(grid, i-1, j), dig(grid, i, j-1))));
        return gold;
        //change ends...
    
    
    int getMaximumGold(vector<vector<int>>& grid) 
        vector<vector<int>> gridCopy=grid;
        
        int maxGold=0;
        for(int i=0; i<grid.size(); i++) 
            for(int j=0; j<grid[0].size(); j++) 
                if(grid[i][j]!=0) 
                    maxGold=max(maxGold, dig(gridCopy, i, j));
                    gridCopy=grid;
                
            
        
        
        return maxGold;
    
;

但是,它在输入 [[1,0,7,0,0,0],[2,0,6,0,1,0],[3,5,6,7,4,2],[4,3,1,0,2,0],[3,0,5,0,20,0]] 时中断;产生58 而不是60

我发现了另一个代码here,它是相同的,除了在上面的注释部分,它们有以下几行:

  g[i][j] = -g[i][j];
  auto res = max( dfs(g, i + 1, j), dfs(g, i, j + 1), dfs(g, i - 1, j), dfs(g, i, j - 1) );
  g[i][j] = -g[i][j];
  return g[i][j] + res;

(当然,他们不会在嵌套的 for 循环中将 grid 分配给 gridCopy,因为他们会将修改后的网格恢复为其原始形式)。

我知道他们在回溯,而我没有。但我无法理解我做错了什么,因为从逻辑上讲,我正在做同样的事情。我使用调试语句来跟踪问题,但由于有很多递归调用,因此很难跟进。

有人可以指出我上面的代码中的逻辑谬误是什么吗?

谢谢!

【问题讨论】:

你有输入、算法和动机。当这里的人在戳你给出的内容时,在调试器中运行你的代码,看看你是否能在他们发现问题之前发现问题。 是的,这就是我现在正在尝试的。谢谢,@ user4581301。 :) 【参考方案1】:

您所有的递归调用都可能会修改网格,并且不会恢复它。 这意味着对单元的第一次访问将阻止它进行所有其他后续尝试。

例如,如果首先评估 dig(grid, i+1, j),那么当您执行其他三个方向时,在该计算期间访问的所有单元格都不可用。

如果您的代码恰好从左上角开始并在您的示例中先向下,您将访问 1-2-3-4-3 并卡住。 在那次步行之后,从左上角开始就再也没有路径了,尽管从一开始就有很多路径,其中一些支付的费用远远超过 13。 (你可能认为你会从任一方向找到路径,但这取决于评估顺序。)

共享可变状态和递归是一个非常棘手的组合。

【讨论】:

"...您所有的递归调用都可能修改网格..." 那么,这不应该是故意的吗?基于规则 (c),我不想再重蹈覆辙。 嗯,那么链接代码有什么不同呢? grid[i][j]=-grid[i][j] 不应该导致相同的情况吗?此更改是在递归调用(以及后续恢复)之前进行的。 一次“步行”期间您不能多次访问它,但有很多可能的步行。 能否详细说明?【参考方案2】:

您的代码中似乎有一点错误是:

//change begins...
        int gold=0;
        gold+=grid[i][j];
        grid[i][j]=0;
        gold+=max(dig(grid, i+1, j), max(dig(grid, i, j+1), max(dig(grid, i-1, j), dig(grid, i, j-1))));
        return gold;
        //change ends...

在这里,您更改了 grid[i][j] 的值,并且更改的值正在影响您的输入,即您的输入集现在是错误的。由于grid[i][j] 的值已更改,因此这将影响其余的计算。

您可以做的是将grid[i][j] 的初始值存储在该递归堆栈中的某个位置,并在您探索完该节点的所有路径后重新分配回grid[i][j]

例如:对你的逻辑稍作修改

//change begins...
        int gold=0;
        gold+=grid[i][j];
        int temp = grid[i][j];
        grid[i][j]=0;
        gold+=max(dig(grid, i+1, j), max(dig(grid, i, j+1), max(dig(grid, i-1, j), dig(grid, i, j-1))));
        grid[i][j] = temp;
        return gold;
        //change ends...

如果您在问题中使用解决方案,您还可以在递归堆栈中创建内存。

只是为了回答你的问题,为什么只有回溯才能解决这个问题:

您需要了解您的解决方案,如下所示:

查看每个网格项是否可以成为解决方案集的一部分。 为此,您选择一个网格项(其中项值应大于 0) 然后你从那个项目探索所有环境。使用 DFS,您可以继续探索他们的周围环境等,直到满足退出条件。 现在,在您的解决方案中,您已经改变了 grid[i][j] 的值(为了便于理解,假设 grid[2][3] 发生了突变)当且仅当该项目被选中时才可以存在于最终解决方案集中。 但是,在探索其他可能性时,如果您可能碰巧发现存在更多金币的可能性。然后grid[2][3] 也将参与其中。您已将其标记为0,即该节点的计算将出错。

因此,您需要将原始值恢复到网格项grid[i][j]。您制作0 的原因是您不想再次包含它,因为您已经访问过。但是对于其他解决方案集,您需要在其中存在原始值。

【讨论】:

“这里你已经改变了grid[i][j] 的值,而改变的值正在影响你的输入”,这不应该是故意的吗?基于规则 (c),我不想再重蹈覆辙。 无论如何你都需要重新审视(探索)它。您是否尝试过为此创建递归树?递归正在帮助您探索所有可能性,而您的 max() 方法正在帮助您选择最大的一个。规则(c)是说您不需要再次选择相同的网格,但根据您的解决方案需要探索周围环境以了解最大值。探索的并不一定意味着它在您的解决方案集中被选中。 @UmedhSinghBundela 我明白你的意思,但是在链接的解决方案中,我们也将其设为负数。 不会使其对其他解决方案集不可用吗? 那个链接的解决方案在递归调用之后再次否定了它,这使它回到了原来的值。否定的否定是一样的,对吧? @UmedhSinghBundela 是的,但是在进行递归调用之后完成了恢复。因此,我认为,当递归调用开始执行时,恢复的值对他们来说还不是可用的。你现在明白我困惑的根源了吗?

以上是关于为啥在这种情况下只有回溯有效?的主要内容,如果未能解决你的问题,请参考以下文章

为啥尾随 %20 (在这种情况下为有效数据)会杀死 asp.net mvc 路由

为啥 glreadpixels 仅在某些情况下有效?

为啥 this.setState 在我的情况下有效

为啥以下在红宝石中有效?

为啥#map 比#each 更有效?

为啥如果有一个无效的字段,需要有效的表单 2 次才能获得有效的下一个字段?