回溯的经典示例是生成整数列表的所有(不一定是不同的)排列。
例如,这是我编写的执行此操作的功能:
List<List<Integer>> perm_set = new ArrayList<>(); // store permutations
private void permute(int[] nums, List<Integer> perm, boolean[] used) {
if (perm.size() == nums.length) {
// System.out.println(perm);
perm_set.add(new ArrayList<>(perm)); // why is it necessary to add new?
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) {
continue;
}
used[i] = true;
perm.add(nums[i]);
permute_simple(nums, perm, used);
used[i] = false;
perm.remove(perm.size() - 1);
}
}
上面的函数有效,但是我不知道为什么每次要添加排列时(即每次回溯搜索完成时)都需要new ArrayList<>(perm)
。我添加了一条打印语句,以尝试查看正在发生的情况,并确保足够正确地排列排列。但是,如果我只使用perm_set.add(perm)
,我将得到一列空白列表。
那么,如果每次搜索结束时当前排列的状态正确,那么为什么有必要为每种情况创建一个新副本?
请注意,该示例是任意的。这个问题不特定于我提供的代码,也不特定于排列。它更广泛地适用于典型的DFS问题。我想了解为什么通常不能简单地直接存储最终结果,以及为什么new
关键字是必需的。
谢谢
答案 0 :(得分:0)
请考虑以下内容:
这将完全按预期工作。
@Test
public void testPass() {
ArrayList<String> listA = new ArrayList<String>();
listA.add("a");
ArrayList<String> listB = new ArrayList<String>(listA);
Assert.assertTrue(listB.get(0).equals("a"));
}
这将失败,因为在复制后修改了原点列表。
@Test
public void testFail() {
ArrayList<String> listA = new ArrayList<String>();
ArrayList<String> listB = new ArrayList<String>(listA);
// A, modified after the copy has been made
listA.add("a");
Assert.assertTrue(listB.get(0).equals("a"));
// This will throw and IndexOutOfBoundsException, as listB is empty.
}
这将失败,因为listB
为空。在修改之前,给它提供了listA的副本。
答案 1 :(得分:0)
(这是尝试获得更笼统的答案,因为认识到该问题通常与DFS有关。)
因此,如果每次搜索结束时当前排列的状态正确,为什么每种情况下都需要创建
perm
的新副本?
简短的回答:这是必要的,因为您将要更改perm
。
更长的答案:您的List<List<Integer>
正在记录正确的解决方案状态快照。如果您不复制状态(即拍摄快照),则最终将以perm_set
包含对同一ArrayList<Integer>
对象的N个引用;即最后排列的N个副本。
请注意,在示例代码中,策略是在每次递归调用之前修改状态(perm
和used
),然后在返回时撤消修改。另一种策略是在递归之前快照状态,然后丢弃该状态。如果您要尝试并行化算法,或者撤消的成本或复杂性过高,则该方法会更好。