繁体   English   中英

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

[英]Backtracking and finding all subsets of array that equal some value k

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

我了解有一种回溯方法。 我无法获得它,因为很多解决方案方法都在网上困扰着我,我只需要一个简单的解释或一个非常小的数组的逐步跟踪就可以直观地看到正在发生的事情。

我在图书馆度过了最后12个小时,由于我无法理解它,我现在感到非常沮丧,我也希望采用一种简单的方法。 另外,我不熟悉C或Java以外的许多语言。

您已经掌握了子集和问题的变体。 不幸的是,问题是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));
        }
    }

}

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

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

我们想要达到的值是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> 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}

结论:制作11的方式有4种:4 + 5 + 2; 8 + 3; 5 + 6和2 + 3 + 6

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

它的“回溯”部分是通过递归实现的。 每当您从Java中的方法返回时,您都会在堆栈中“回溯”到调用它的位置。 这使得使用调用堆栈来跟踪您的“回溯”状态确实非常容易。

这是基本思想。 假设我要在数组A中搜索总和n 我们从索引i = 0的数组开始搜索。 然后我们尝试两件事:

  • 尝试将元素A [i]包括在运行总和中。 为此,我们从索引i + 1的数组中搜索值nA [i] 我们需要将此元素记录在包含元素的运行列表中。 我们称此为运行总和xs中包含的所有元素的列表。
  • 尝试在运行总和中包括元素A [i] 为此,我们从索引i + 1的数组中搜索n的当前值。 由于我们没有包括A [i],所以我们不需要更新xs

看看我们如何在第一种情况下搜索整个数组,然后回溯 ,然后在第二种情况下再次搜索?

请注意,您需要在“回溯”之后保留xs的保留副本,以用于第二个搜索。 我认为使用标准Java库执行此操作的最简单方法是在回溯时撤消对xs的更改。 因此,如果您在xs的末尾添加一些元素x来执行“ with”搜索,那么只需在执行“ without”搜索之前从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) ,这有点冗长。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM