解題參考原文如下(請帶着批判的眼光去看,有些細節我認爲是不對的,但是我沒有改動):
<編程之美>數組分割問題
題目概述:有一個沒有排序,元素個數爲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) = {1,2,3};
// 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