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

Posted

技术标签:

【中文标题】Java - 为啥必须为每个回溯基本案例创建一个新实例?【英文标题】:Java - Why do you have to create a new instance for each backtracking base case?Java - 为什么必须为每个回溯基本案例创建一个新实例? 【发布时间】:2018-07-14 23:19:43 【问题描述】:

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

例如,这是我编写的一个函数:

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&lt;&gt;(perm),即在每次回溯搜索完成时。我添加了一个打印语句来尝试查看发生了什么,果然排列打印得很好。但是如果我只使用perm_set.add(perm),我最终会得到一个空白列表。

那么,如果每次搜索结束时的当前排列状态是正确的,为什么我必须为每个案例创建一个新副本?

请注意,该示例是任意的。这个问题并不特定于我提供的代码或特定于排列。它更广泛地适用于典型的 DFS 问题。我想了解为什么通常不能简单地直接存储最终结果,以及为什么需要 new 关键字。

谢谢

【问题讨论】:

什么是perm_set?这是发布的代码中唯一一次使用它。另外,nums 是什么?这个问题缺乏很多细节和相关背景。 是的,它缺少细节,因为示例并不重要。该示例仅用于演示有关回溯的一般概念问题。您询问的所有变量都不会改变或协助基本问题。我在问为什么每次搜索结束时都需要 new 关键字。排列示例是任意的,可能是任何其他典型的 DFS 问题。现在由于您不必要的反对,我不太可能得到有助于我了解这种行为的回应。 因为如果你只是添加perm,它仍然可以在外部进行修改。通过创建新列表,您可以确保一旦添加到 perm_set 数组中,对 perm 变量的修改不会影响已添加到 perm_set 的列表 所以对perm 的进一步修改仍然会影响列表中的一些perm?那是因为列表是可变的吗?这是有道理的,那么当问题处理例如字符串时,为什么不需要新实例。 我不相信代码如图所示会真正起作用。首先,缺少perm_simple 的定义。 【参考方案1】:

考虑以下几点:

这将完全按预期工作。

@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 的 副本

【讨论】:

啊,明白了。所以就像没有创建new 实例一样,它们都指向同一个对象,因此所有修改都将应用于每个指针的引用。 正确,使用旧数组创建一个新数组,本质上将创建该数组的副本,因此以后的修改不会影响原始集合。这不仅仅是new,而是您将原点传递给constructor。【参考方案2】:

(这是一个更一般的答案的尝试,认识到该问题通常是关于 DFS 的。)

那么,如果每次搜索结束时当前排列的状态是正确的,为什么我必须为每个案例创建一个 perm 的新副本?

简短回答:这是必要的,因为您要更改perm

更长的答案:您的List&lt;List&lt;Integer&gt; 正在记录正确解决方案的状态快照。如果您没有复制状态(即拍摄快照),您最终会得到包含对同一 ArrayList&lt;Integer&gt; 对象的 N 个引用的 perm_set;即最后一个排列的 N 个副本。

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

【讨论】:

以上是关于Java - 为啥必须为每个回溯基本案例创建一个新实例?的主要内容,如果未能解决你的问题,请参考以下文章

在“分叉”一个进程时,为啥 Linux 内核会为每个新创建的进程复制内核页表的内容?

继承java.lang.Thread类并重写run方法为啥不可以创建一个新线程呢!为啥?

为啥我的 servlet 不根据请求创建新线程? [关闭]

在语言建模中,为啥我必须在每个新的训练时期之前初始化隐藏权重? (火炬)

为啥我使用回溯解决数独的 JAVA 代码没有给出任何解决方案? [关闭]

Java数据结构之回溯算法的递归应用迷宫的路径问题