簡體   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