【问题标题】:Approaches to understand backtracking better更好地理解回溯的方法
【发布时间】:2022-01-18 21:58:35
【问题描述】:

我想问是什么帮助您更好地掌握了回溯的概念。

我认为我理解它背后的想法和递归足够好,但是,我很难理解为什么回溯会导致想要的结果。我尝试在纸上“试运行”代码,更好地理解程序流程,但几乎无济于事。

所以,很自然地,我很难想出自己的回溯解决方案。

我想我理解为什么基本情况是有意义的,为什么需要 if 调用,并且看到每个选项都被检查(通过使用调试器),但我不明白为什么 java 在内部以这种方式计算代码.

例如这里:https://codingbat.com/prob/p145416:

java 

**
  // Base case: if there are no numbers left, then there is a
  // solution only if target is 0.
  if (start >= nums.length) return (target == 0);
  
  // Key idea: nums[start] is chosen or it is not.
  // Deal with nums[start], letting recursion
  // deal with all the rest of the array.
  
  // Recursive call trying the case that nums[start] is chosen --
  // subtract it from target in the call.
  if (groupSum(start + 1, nums, target - nums[start])) return true;
  
  // Recursive call trying the case that nums[start] is not chosen.
  if (groupSum(start + 1, nums, target)) return true;
  
  // If neither of the above worked, it's not possible

  System.out.println("test"); // Why does it reach that point?
  return false;
}
**

【问题讨论】:

  • 放一些代码让我们理解你的问题。并指出你面临的困难。
  • 我真的不知道从哪里开始,我不明白,为什么我的程序流程是这样的。例如,java 是如何记住一条路径被采用但没有导致想要的解决方案的?哪一行代码告诉我为什么决定被推翻?
  • 为什么我会到达一个我会发出“test”字符串的部分,然后继续递归调用?跟调用栈有关系吗?
  • 我不太明白你的问题。但这里有一些见解。当一个函数被调用时,它的内部内容(变量)被存储在一个叫做堆栈(主存储器)的地方。如果您递归调用该函数,堆栈将充满您的函数状态。当该函数调用完成时,程序会从堆栈中弹出该元素状态并继续该过程。这就是函数调用在任何编程语言中的工作方式。
  • Java 不会记住路径,它会在您调用递归函数时存储它们。

标签: java backtracking


【解决方案1】:

我想问是什么帮助您更好地掌握了回溯的概念。

为了掌握总体思路,它帮助我观看了使用回溯的解决方案的可视化(视频或带有可视化的页面)。 为了掌握调用和递归调用的工作原理,我只是通过调试做了很多步骤并观看了一些可视化。

从您添加到代码中的 cmets,我可以看出,您已经掌握了回溯的一般概念以及为什么存在这些条件和递归调用。

那么计算机(并非特定于 Java)如何执行调用和递归调用,更重要的是,它们如何跟踪调用完成后返回的位置?

他们使用调用堆栈。来自维基百科

调用堆栈用于多个相关目的,但使用调用堆栈的主要原因是跟踪每个活动子例程在完成执行时应返回控制权的点。活动子程序是一个已被调用但尚未完成执行的子程序,此后控制权应交还给调用点。子程序的这种激活可以嵌套到任何级别(递归作为一种特殊情况),因此是堆栈结构。

调用堆栈通过在每次调用发生时将堆栈帧推入(添加)堆栈帧并在活动调用返回时弹出(删除)它们来跟踪仍在进行中的调用。

堆栈帧包含以下信息(简化):

  1. 调用的参数值
  2. 返回地址 = 调用完成后要返回的代码位置
  3. 被调用方法的局部变量,上下文/作用域

堆栈和堆栈帧使递归调用成为可能。

我知道您已经使用调试器在代码执行时单步执行代码,但让我们在“纸”上再做一次。

我将在您的代码中使用行号(我还删除了 cmets),以便更轻松地引用这些行。将被递归调用的方法有 5 行代码。

   boolean groupSum(int start, int[] nums, int target) {

1:   if (start >= nums.length) return (target == 0); 

2:   if (groupSum(start + 1, nums, target - nums[start])) return true;

3:   if (groupSum(start + 1, nums, target)) return true;

4:   System.out.println("test"); // Why does it reach that point?

5:   return false;

   }

我们以拨打groupSum(0, [2, 4, 8], 9) 为例。

当您的代码的某些部分调用groupSum(0, [2, 4, 8], 9) 时,它将将此调用推送到调用堆栈(创建堆栈框架)并开始执行方法体。

Stack frame 1 created: groupSum(0, [2, 4, 8], 9)
Parameters: start = 0, target = 9, nums = [2, 4, 8]

// Line 1 condition is false, so it keeps going
// Line 2 calls groupSum(0 + 1, [2, 4, 8], 9 - 2)
// which pushes a new stack frame on the call stack

Stack frame 2 created: groupSum(1, [2, 4, 8], 7)
Parameters: start = 1, target = 7, nums = [2, 4, 8]
// This creates a local scope (context) for frame 2.
// Frame 1 is isolated from changes to variables in Frame 2

// Line 1 condition is false, so it keeps going
// Line 2 calls groupSum(1 + 1, [2, 4, 8], 7 - 4)
// which pushes a new stack frame on the call stack

Stack frame 3 created: groupSum(2, [2, 4, 8], 3)
Parameters: start = 2, target = 3, nums = [2, 4, 8]

// Line 1 condition is false, so it keeps going
// Line 2 calls groupSum(2 + 1, [2, 4, 8], 3 - 8)
// which pushes a new stack frame on the call stack

Stack frame 4 created: groupSum(3, [2, 4, 8], -5)
Parameters: start = 3, target = -5, nums = [2, 4, 8]

// Line 1 condition is true. So the method returns (-5 == 0),
// which means it returns false.

这是第一次实际回溯发生的地方。它已经到达数组的末尾,但它没有找到有效的解决方案,因为 -5 不是 0,所以它返回并返回调用堆栈 - 它回溯

return 语句从调用堆栈中弹出一个帧,并在返回地址处继续执行代码 - 在代码中调用我们现在返回的方法的点处。

在这种情况下,它弹出第 4 帧,恢复第 3 帧的局部范围(恢复所有局部变量的值),并在第 2 行(第 3 帧)继续执行。 这使得回溯“工作”,因为它在调用站点恢复本地范围的先前状态,然后从那里继续执行。

Stack frame 3 restored:
Parameters: start = 2, target = 3, nums = [2, 4, 8]

// Line 2 condition is false (because false was returned) so it keeps going
// Line 3 calls groupSum(2 + 1, [2, 4, 8], 3)
// which pushes a new stack frame on the call stack

Stack frame 4 created: // this is a completely new frame 4,
// has no relation to frame 4 from before
Parameters: start = 3, target = 3, nums = [2, 4, 8]

// Line 1 condition is true, it returns (3 == 0), so it returns false.

这里再次回溯。这会再次从调用堆栈中弹出一个帧,因此第 3 帧的范围被恢复,但这一次它继续执行第 3 行

Stack frame 3 restored:
Parameters: start = 2, target = 3, nums = [2, 4, 8]

// Line 3 condition is false (because false was returned) so it keeps going
// Line 4 prints out "test". This is when and how this line is executed.
// Line 5 returns false.

这里也是回溯,因为两条调用路径(来自第 2 行和第 3 行的调用)都没有从当前状态找到解决方案,都不得不回溯。

这会从调用堆栈中弹出一个帧,因此第 2 帧的范围被恢复,它在第 2 行继续。

Stack frame 2 restored:
Parameters: start = 1, target = 7, nums = [2, 4, 8]

// Line 2 condition is false (because false was returned), so it keeps going
// Line 3 calls groupSum(1 + 1, [2, 4, 8], 7)
// which pushes a new stack frame on the call stack

创建堆栈帧 3,整个过程重复,帧 4 的目标 7 - 8 = -17 的数字略有不同,但结果相同(返回 false,因此回溯)。

它在第 4 帧第 4 行再次打印“test”,并在第 5 行返回 false

然后第 3 帧第 4 行打印“test”并在第 5 行返回 false(回溯),第 2 帧在第 3 行再次恢复

Stack frame 2 restored:
Parameters: start = 1, target = 7, nums = [2, 4, 8]

// Line 3 condition is false (because false was returned) so it keeps going
// Line 4 prints "test"
// Line 5 returns false (backtracks)

Stack frame 1 restored:
Parameters: start = 0, target = 9, nums = [2, 4, 8]

// Line 2 condition is false, so it keeps going
// Line 3 repeats the whole thing with frames 2, 3 and 4,
// outcomes are still false (backtracks a few times),
// so after a few more "test" are printed, frame 1 is restored again,
// but this time on line 3 and since the returned value is false,
// the condition is false, so it keeps going
// Line 4 prints "test
// Line 5 returns false (backtracks)

此时从调用堆栈中弹出第 1 帧,并将执行返回到方法 groupSum(0, [2, 4, 8], 9) 的原始调用者,因此我们可以说“根帧”(第 0 帧)被恢复并且值false 被调回原来的调用者。

整个执行的结果是false,这是意料之中的,因为2, 4, 8中没有加到9的组和。

编辑:这是另一个可能会派上用场的提示(也许当您提出回溯算法时)。在 (Java) IDE 中,当调试器在断点处停止时,您还可以在调试器窗口中看到当前调用堆栈的帧以及当前范围内的变量值。您还可以选择其他框架并查看调用开始的位置以及该框架范围内的变量值。一些 IDE 甚至在代码中内联显示局部变量(如下图所示)。

这是来自 IDEA 社区版的示例

【讨论】:

  • @John 我很高兴有帮助,我记得另一个提示并将其添加到答案的末尾。祝你回溯算法好运。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-04-09
  • 2018-11-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多