# 第 4 节 几点说明帮助理解「回溯算法」
思考以下几个问题,更深入理解「回溯算法」。
# 每一次尝试都「复制」,则不需要回溯
如果在每一个 非叶子结点 分支的尝试,都创建 新的变量 表示状态,那么
- 在回到上一层结点的时候不需要「回溯」;
- 在递归终止的时候也不需要做拷贝。 这样的做法虽然可以得到解,但也会创建很多中间变量,这些中间变量很多时候是我们不需要的,会有一定空间和时间上的消耗。为了验证上面的说明,我们写如下代码进行实验:
参考代码 3:
Java 代码:
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List<List<Integer>> permute(int[] nums) {
// 首先是特判
int len = nums.length;
// 使用一个动态数组保存所有可能的全排列
List<List<Integer>> res = new ArrayList<>();
if (len == 0) {
return res;
}
boolean[] used = new boolean[len];
List<Integer> path = new ArrayList<>();
dfs(nums, len, 0, path, used, res);
return res;
}
private void dfs(int[] nums, int len, int depth,
List<Integer> path, boolean[] used,
List<List<Integer>> res) {
if (depth == len) {
// 3、不用拷贝,因为每一层传递下来的 path 变量都是新建的
res.add(path);
return;
}
for (int i = 0; i < len; i++) {
if (!used[i]) {
// 1、每一次尝试都创建新的变量表示当前的"状态"
List<Integer> newPath = new ArrayList<>(path);
newPath.add(nums[i]);
boolean[] newUsed = new boolean[len];
System.arraycopy(used, 0, newUsed, 0, len);
newUsed[i] = true;
dfs(nums, len, depth + 1, newPath, newUsed, res);
// 2、无需回溯
}
}
}
}
Python 代码:
from typing import List
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
def dfs(nums, size, depth, path, state, res):
if depth == size:
res.append(path)
return
for i in range(size):
if ((state >> i) & 1) == 0:
dfs(nums, size, depth + 1, path + [nums[i]], state ^ (1 << i), res)
size = len(nums)
if size == 0:
return []
state = 0
res = []
dfs(nums, size, 0, [], state, res)
return res
这就好比我们在实验室里做「对比实验」,每一个步骤的尝试都要保证使用的材料是一样的。我们有两种办法:
- 每做完一种尝试,都把实验材料恢复成做上一个实验之前的样子,只有这样做出的对比才有意义;
- 每一次尝试都使用同样的 新的材料 做实验。
在生活中做实验对材料有破坏性,这个过程通常不可逆。而在计算机的世界里,「恢复现场」和「回到过去」是相对容易的。
在一些字符串的搜索问题中,有时不需要回溯的原因是这样的:字符串变量在拼接的过程中会产生新的对象(针对 Java 和 Python 语言,其它语言我并不清楚)。如果您使用 Python 语言,会知道有这样一种语法:[1, 2, 3] + [4]
也是创建了一个新的列表对象,我们已经在「参考代码 2」中展示这种写法。
# 为什么不是广度优先遍历
- 首先是正确性,只有遍历状态空间,才能得到所有符合条件的解,这一点 BFS 和 DFS 其实都可以;
- 在深度优先遍历的时候,不同状态之间的切换很容易 ,可以再看一下上面有很多箭头的那张图,每两个状态之间的差别只有
处,因此回退非常方便,这样全局才能使用一份状态变量完成搜索; - 如果使用广度优先遍历,从浅层转到深层,状态的变化就很大,此时我们不得不在每一个状态都新建变量去保存它,从性能来说是不划算的;
- 如果使用广度优先遍历就得使用队列,然后编写结点类。队列中需要存储每一步的状态信息,需要存储的数据很大,真正能用到的很少 。
- 使用深度优先遍历,直接使用了系统栈,系统栈帮助我们保存了每一个结点的状态信息。我们不用编写结点类,不必手动编写栈完成深度优先遍历。
# 不回溯可不可以
可以。搜索问题的状态空间一般很大,如果每一个状态都去创建新的变量,时间复杂度是
就本题而言,只需要叶子结点的那个状态,在叶子结点执行拷贝,时间复杂度是
最后,由于回溯算法的时间复杂度很高,因此在遍历的时候,如果能够提前知道这一条分支不能搜索到满意的结果,就可以提前结束,这一步操作称为 剪枝。
作者:liweiwei1419 链接:https://suanfa8.com/backtracking/questions 来源:算法吧 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
← 第 3 节 理解回溯 第 5 节 剪枝 →