回溯并找到等于某个值k的所有数组子集

时间:2014-09-12 22:51:15

标签: java c algorithm recursion backtracking

我早些时候问question,当我去我的终端进行编码时,我以为我理解了这一点,我又一次完全迷失了。我的问题是我有一些数组说[1,2,3,4],我需要找到所有可能的组合,它们将等于目标值5.

我知道有一种回溯方法。我无法在网上获得它很多解决方案的方法正在我脑海中我只需要一个简单的解释或一个非常小的数组的逐步跟踪来可视化正在发生的事情。

我在图书馆度过了最后12个小时,现在感到非常沮丧,因为我无法理解它,我也很欣赏一个简单的方法。另外,我不熟悉C或java以外的许多语言。

3 个答案:

答案 0 :(得分:1)

你已经掌握了Subset Sum Problem的变体。不幸的是问题是NP-Complete,所以如果你希望快速(多项式)解决方案,那你就不走运了。

也就是说,如果您的阵列相当小并且值非常小,那么蛮力解决方案可能仍然足够快。

import java.util.Arrays;
import java.util.LinkedList;


public class SubsetSum {

    /** Helper for the power set generating function.
     *  Starts with a partially built powerSet pSet that includes
     *  numbers with index 0 ... i. For every element in the current power set
     *  Create a new array that is equivalent to the existing array plus it has
     *  the i+1th number (n) concatinated on the end.
     * @param pSet - The completed pSet for a smaller set of numbers
     * @param n - The number of add to this pSet.
     * @return a reference to pSet (not necessary to use). When returning,
     * pSet will have double the size it had when the method began.
     */
    private static LinkedList<Integer[]> addNumb(LinkedList<Integer[]> pSet, int n){
        LinkedList<Integer[]> toAdd = new LinkedList<>();
        for(Integer[] arr : pSet){
            Integer[] arr2 = new Integer[arr.length+1];
            for(int i = 0; i < arr.length; i++){
                arr2[i] = arr[i];
            }
            arr2[arr.length] = n;
            toAdd.add(arr2);
        }

        //Add all of the toAdds to the pSet
        pSet.addAll(toAdd);
        return pSet;
    }

    /** Creates the power set for the given array of ints.
     * Starts by creating a set with the empty array, which is an element of every
     * power set. Then adds each number in the input array in turn to build the final
     * power set.
     * @param numbs - the numbers on which to build a power set
     * @return - the power set that is built.
     */
    private static LinkedList<Integer[]> makePowerSet(int[] numbs){
        LinkedList<Integer[]> pSet = new LinkedList<Integer[]>();
        //Add the empty set as the first default item
        pSet.add(new Integer[0]);

        //Create powerset
        for(int n : numbs){
            addNumb(pSet, n);
        }

        return pSet;
    }

    /** Returns the simple integer sum of the elements in the input array */
    private static int sum(Integer[] arr){
        int i = 0;
        for(int a : arr){
            i += a;
        }
        return i;
    }


    /** Brute-forces the subset sum problem by checking every element for the desired sum.
     */
    public static void main(String[] args) {
        int[] numbs = {1,2,3,4,5,6,7,8}; //Numbers to test
        int k = 7;                 //Desired total value

        LinkedList<Integer[]> powerSet = makePowerSet(numbs);

        for(Integer[] arr : powerSet){
            if(sum(arr) == k)
                System.out.println(Arrays.deepToString(arr));
        }
    }

}

答案 1 :(得分:1)

确实,有一个关于回溯等的故事。

让我们来看一个更复杂的例子:

我们想要达到的值是11,数组是[5,4,8,2,3,6]

以下是可能的算法之一:

我们将列出我们发现的所有方式,以达到小于或等于11的每个可能的数字(我不会谈论我们使用的结构或任何东西,因为我只是解释算法,而不是它的实现)

我们将从零开始,并一次添加一个新数字。在这个算法中,我会认为数组中的每个数字都是正数,因此我不会跟踪到达的数字高于我们想要达到的数字。

所以一开始我们什么都没有。

我们介绍我们的第一个数字:5

我们有一种方法可以达到5,它是5(如果你愿意,可以是5 + 0)

我们介绍我们的第二个数字:4 我们现在可以达到的数字是:

4:{4}
5:{5}
9:{4+5}

我们介绍我们的第三个数字:8 我们现在可以达到的数字是:

4:{4}
5:{5}
8:{8}
9:{4+5}

没有更多,因为8 + 4&gt; 11

我们介绍我们的第四个数字:2 我们现在可以达到的数字是:

2:{2}
4:{4}
5:{5}
6:{4+2}
7:{5+2}
8:{8}
9:{4+5}
10:{8+2}
11:{4+5+2}

我们介绍我们的第五个数字:3 我们现在可以达到的数字是:

2:{2}
3:{3}
4:{4}
5:{5 ; 2+3}
6:{4+2}
7:{5+2 ; 4+3}
8:{8 ; 5+3}
9:{4+5 ; 4+2+3}
10:{8+2 ; 5+2+3}
11:{4+5+2 ; 8+3}

我们介绍我们的第六个数字:6 我们现在可以达到的数字是:

2:{2}
3:{3}
4:{4}
5:{5 ; 2+3}
6:{4+2 ; 6}
7:{5+2 ; 4+3}
8:{8 ; 5+3 ; 2+6}
9:{4+5 ; 4+2+3 ; 3+6}
10:{8+2 ; 5+2+3 ; 4+6}
11:{4+5+2 ; 8+3 ; 5+6 ; 2+3+6}

结论:有4种方法可以使11:4 + 5 + 2; 8 + 3; 5 + 6和2 + 3 + 6

答案 2 :(得分:1)

下面是一些简单(但效率极低)的代码来解决这个问题。

这是通过递归实现的“回溯”部分。有时候你从Java中的方法返回,你会“回溯”到堆栈中的任何地方。这使得使用调用堆栈可以很容易地跟踪您的“回溯”状态。

这是基本想法。假设我正在搜索一些 A 数组 n 。我们在索引 i = 0的数组中开始搜索。然后我们尝试两件事:

  • 尝试在运行总和中包含元素 A [i] 。我们通过从索引 i + 1 搜索数组来获取值 n-A [i] 。我们需要在包含元素的运行列表中记录此元素。我们将此列表中包含运行总和 xs
  • 中包含的所有元素
  • 在运行总和中尝试包含元素 A [i] 。我们通过从索引 i + 1 搜索数组来获取 n 的当前值。由于我们未包含 A [i] ,因此我们无需更新 xs

看看我们如何搜索整个数组中的第一个案例,回溯,然后再次搜索第二个案例?

请注意,您需要在“回溯”之后保留 xs 的副本,以便在第二次搜索中使用。我认为使用标准Java库执行此操作的最简单方法是在回溯时撤消对 xs 的更改。因此,如果您在 xs 的末尾添加一些元素 x 来执行“with”-search,那么只需从 xs 就在你做“没有” - 搜索之前。

我没有尝试将所有答案存储在数据结构中,而是在找到答案后立即打印答案。这也是为了简化此解决方案的逻辑。

import java.util.Deque;
import java.util.ArrayDeque;

public class SubarraySums {

    /** Program entry point */
    public static void main(String[] args) {
        int[] array = { 1, 8, 7, 9, 5, 2 };
        findSubarraySums(12, array);
    }

    /** Wrapper function for the search */
    public static void findSubarraySums(int goal, int[] array) {
        // Search the whole array with an empty starting set
        search(goal, new ArrayDeque<Integer>(), array, 0);
    }

    /** Helper for printing an answer */
    private static void printAnswer(Deque<Integer> xs) {
        // Print the sum
        int sum = 0;
        for (int x : xs) sum += x;
        System.out.printf("%d =", sum);
        // Print the elements
        for (int x : xs) {
            System.out.printf(" %d", x);
        }
        System.out.println();
    }

    /**
     * Search the array, starting from index i,
     * for a subset summing to n.
     * The list xs includes all of the elements that are already
     * assumed to be included in this answer
     */
    private static void search(int n, Deque<Integer> xs, int[] array, int i) {
        // Base case: we've reached zero!
        if (n == 0) {
            printAnswer(xs);
            return;
        }
        // Base case: solution not found
        if (n < 0 || i >= array.length) return;
        // Recursive case: try searching with and without current element
        // with:
        xs.addLast(array[i]);
        search(n-array[i], xs, array, i+1);
        // without:
        xs.removeLast();
        search(n, xs, array, i+1);
    }

}

上面的代码在数组中有6个元素,因此它将对search进行2 6 = 64个递归调用。这就是为什么它“超级低效”。但它也非常简单,所以这应该有助于你理解它。您应该使用调试器逐步执行代码以查看发生的情况,或者只是在一张纸上查找执行情况。应该非常明显的是,执行“回溯”调用堆栈以在搜索期间尝试两个选项(包括/不包括)。

我在上面的代码中使用了ArrayDeque来存储我的 xs 列表,因为Deque界面具有addLastremoveLast方法。 LinkedList也可以工作(因为它还实现了Deque接口)。 ArrayList也可以使用,但您需要使用addremove(list.size()-1),这有点冗长。