任意2n個整數,從其中選出n個整數,使得選出的n個整數和同剩下的n個整數之和的差最小

解題參考原文如下(請帶着批判的眼光去看,有些細節我認爲是不對的,但是我沒有改動)

<編程之美>數組分割問題

題目概述:有一個沒有排序,元素個數爲2N的正整數數組。要求把它分割爲元素個數爲N的兩個數組,並使兩個子數組的和最接近。
假設數組A[1..2N]所有元素的和是SUM。模仿動態規劃解0-1揹包問題的策略,令S(k, i)表示前k個元素中任意i個元素的和的集合。顯然:

S(k, 1) = {A[i] | 1<= i <= k}
S(k, k) = {A[1]+A[2]+…+A[k]}
S(k, i) = S(k-1, i) U {A[k] + x | x屬於S(k-1, i-1) }

按照這個遞推公式來計算,最後找出集合S(2N, N)中與SUM最接近的那個和,這便是答案。這個算法的時間複雜度是O(22N).
因爲這個過程中只關注和不大於SUM/2的那個子數組的和。所以集合中重複的和以及大於SUM/2的和都是沒有意義的。把這些沒有意義的和剔除掉,剩下的有意義的和的個數最多就是SUM/2個。所以,我們不需要記錄S(2N,N)中都有哪些和,只需要從SUM/2到1遍歷一次,逐個詢問這個值是不是在S(2N,N)中出現,第一個出現的值就是答案。我們的程序不需要按照上述遞推公式計算每個集合,只需要爲每個集合設一個標誌數組,標記SUM/2到1這個區間中的哪些值可以被計算出來。關鍵代碼如下:

for(i = 0; i < N+1; i++)  
    for(j = 0; j < sum/2+1; j++)  
        flag[i][j] = false;  
flag[0][0] = true;  
for(int k = 1; k <= 2*N; k++) {  
    for(i = k > N ? N : k; i >= 1; i--) {  
        //兩層外循環是遍歷集合S(k,i)  
        for(j = 0; j <= sum/2; j++) {  
            if(j >= A[k] && flag[i-1][j-A[k]])  
                flag[i][j] = true;  
        }  
    }  
}  
for(i = sum/2; i >= 0; i--) {  
    if(flag[N][i]) {  
        cout << "minimum delta is " << abs(2*i - sum) << endl;  
        break;  
    }  
} 

正文

如果上面的內容你看懂了,那麼萬事大吉。你可以關閉網頁了。
像我一樣不夠聰明的人,跟着我的思路往下看吧。

解題思路:

2N個整數,分成2組,使和之差最小。設2N個整數的和是NUM。
關鍵點:讓N個整數的和最接近NUM/2,那麼顯然另外N個整數的和也是最接近NUM/2,顯然這種情況和之差是最小的。

公式

依然是上面的公式。
令S(k, i)表示前k個元素中任意i個元素的和的集合。顯然:
注意公式中的數組序號是按照常規思路,從1開始的。

S(k, 1) = {A[i] | 1<= i <= k}
S(k, k) = {A[1]+A[2]+…+A[k]}
S(k, i) = S(k-1, i) U {A[k] + x | x屬於S(k-1, i-1) }

看不懂?舉個例子就好懂了。
A[] = {1, 2, 3, 4, 5, 6, 7, 8};

S(3, 1) = {123};
// S(3, 3):在前三個數中任意選擇三個數
S(3, 3) = {1+2+3} = {6};
// S(4, 3):在前四個數中任意選擇三個數
// 相比S(3, 3)多了哪幾種情況呢
/* 首先S(4, 3)集合肯定是包含S(3, 3)的,另外還多出來A[4]分別替換掉A[1]、A[2]、A[3]的情況,即{4+2+3,1+4+3,1+2+4} = {7, 8, 9}*/
// 觀察規律,{2+3,1+3,1+2}=S(3, 2)集合,於是得到上面的公式
S(4, 3) = S(3, 3) U {4 + x | x屬於S(3, 2) = {6, 7, 8, 9};

算法設計

源碼來源
針對源代碼,繼續解釋。

public class Main {
    // 題目:任意2n個整數,從其中選出n個整數,使得選出的n個整數和同剩下的n個整數之和的差最小。
    public static void main(String[] args) {
        int A[] = { 1, 2, 3, 5, 7, 8, 9 };
        // int A[] = { 1, 5, 7, 8, 9, 6, 3, 11, 20, 17 };
        func(A);
    }

    static void func(int A[]) {
        int i;
        int j;
        // 下面的變量聲明地很直白,不解釋
        int n2 = A.length;
        int n = n2 / 2;
        int sum = 0;
        // 計算數組總和
        for (i = 0; i < A.length; i++) {
            sum += A[i];
        }

        /*還記得編程之美中的話嗎?
        我們的程序不需要按照上述遞推公式計算每個集合,只需要爲每個集合設一個標誌數組,標記SUM/2到1這個區間中的哪些值可以被計算出來。
        flag[i][j]:任意i個整數之和是j,則flag[i][j]爲true。換言之,flag[i][j]爲true,那麼一定能找到一組整數,使它們的和是j。
        下面的代碼將對flag數組進行初始化*/
        boolean flag[][] = new boolean[A.length + 1][sum / 2 + 1];
        for (i = 0; i < A.length; i++)
            for (j = 0; j < sum / 2 + 1; j++)
                flag[i][j] = false;

        flag[0][0] = true;
        // 重點來了
        for (int k = 0; k < n2; k++) {
            //i取k和n中的較小值,我們的目的是找出集合S(2N, N)中與SUM最接近的那個和,所以k>n時,取到n就足夠了。k<n時,我們顯然無法從3個數中任意選擇4個數,所以取k值
            for (i = k > n ? n : k; i >= 1; i--) {
                // 兩層外循環是遍歷集合S(k,i),遍歷順序S[1][1],S[2][2],S[2][1],S[3][3]……特殊的依賴關係導致必須這樣設計算法
                // 內層循環計算將A[k]加入到集合中能取到的可能的j值
                for (j = 0; j <= sum / 2; j++) {//j是i個任意整數可能的和,從0遍歷到sum / 2,判斷能否得到
                    // 得到j值的條件,j是和,A[k]只是其中一個,肯定需要j >= A[k],否則,取flag[i - 1][j - A[k]]的值的時候會發生越界情況。flag[i - 1][j - A[k]] = true代表可以找到i - 1個數,使它們的和爲j - A[k],所以此條件滿足時意味着flag[i][j] = true
                    if (j >= A[k] && flag[i - 1][j - A[k]])
                        flag[i][j] = true;
                }
            }
        }
        // 終於計算完了,現在找到最合適的結果就好了,要找到最接近SUM / 2的和,倒着找最好了
        for (j = sum / 2; j >= 0; j--) {
            if (flag[n][j]) {
                System.out.println("sum is " + sum);
                System.out.println("sum/2 is " + sum / 2);
                System.out.println("j is " + j);
                System.out.println("minimum delta is " + Math.abs(2 * j - sum));
                break;
            }
        }
    }
}

運行結果:

sum is 35
sum/2 is 17
j is 17
minimum delta is 1
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章