Java-为什么您必须为每个回溯基本案例创建一个新实例?

时间:2018-07-14 23:19:43

标签: java object permutation backtracking

回溯的经典示例是生成整数列表的所有(不一定是不同的)排列。

例如,这是我编写的执行此操作的功能:

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关键字是必需的。

谢谢

2 个答案:

答案 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个副本。

请注意,在示例代码中,策略是在每次递归调用之前修改状态(permused),然后在返回时撤消修改。另一种策略是在递归之前快照状态,然后丢弃该状态。如果您要尝试并行化算法,或者撤消的成本或复杂性过高,则该方法会更好。