近日部門搞了個算法比賽,太久沒寫過算法基本都生疏了。
有道題抽象出來是這麼說:
有一個數列,要採取怎樣的劃分方法把它分成2個數列,使得2個數列的各自之和的差最小。(其實是微軟面試題)
比如一個數列(5,8,13,27,14),通過分成(5,13,14)和(8,27),這樣2個數列各自的和再作差就等於35-32=3,這樣差值最小。
一開始以爲用貪心法可以,但是用下面這個測試用例就給否定了:10個數時候最優解假如是(5)和(1,1,1,1,1)那麼,當加入一個5的時候,得出解結果肯定不是最優解。
想來想去還是把問題轉換爲:如何選出若干個數作爲第一個數列,使得其和S1與求數列分成2個後的平均值差距SUM/2最小。
可以這麼證明:
1.假如存在最佳劃分法使得2個數列之差最少,記最佳劃分法Mbest,其2數列之差爲Dmin
2.存在某個劃分法使得其中一個數列之和Ssome1與SUM/2的差值|Ssome1-SUM/2| 最小(記爲Davgmin)
3.則Mbest中所劃分的任一數列的Sbest1與SUM/2的差值最少也比Davgmin大,即|Sbest1-SUM/2| > |Ssome1-SUM/2|
4.因爲無論哪種劃分方法其任一數列之和與平均值SUM/2的差值絕對值都是相等的,所以可以假設Sbest1>SUM/2,Ssome1>SUM/2
5.則推出Sbest1>Ssome1>SUM/2 => 2Sbest1>2Ssome1>SUM => 2Best1-SUM > 2Some1-SUM
6.最佳劃分法的2數列之差Dmin=2Sbest1 - SUM > 2Best1-SUM ,即與最佳分發的Dmin最小矛盾。
所以只要找到一個子數列之和與SUM/2最接近,那麼就是最佳分法。
這時候我第一時間想到的是用回溯法。思路是不斷從原數列由左往右取,直到所有情況遍歷完。
非遞歸方式:
初始化
循環開始
獲取下一步
能繼續往下走
更新當前狀態信息(壓棧)
不能繼續
回滾當前狀態信息(出棧)
繼續循環
循環結束
public void doit(){
int[] task = new int[]{5,8,13,27,14}; //數列
int N = task.length;
int[] pos = new int[N]; //保存每次獲取的task的元素index
int SUM=0;
for(int i=0;i<N;i++){
SUM+=task[i];
}
int AVG = Math.round(SUM / 2); //計算2個數列平均值
int result = 0; //結果
int min=SUM;
int currPos = -1; //currPos爲當前取到第幾個數減一,如currPos=2意思爲已經取了3個數,currPos=-1意味取了0個
int currSum = 0; //當前所取的元素之和
int lastTried = -1; //上一次取的元素
while (pos[0] != N-1) {
int tryTask = lastTried + 1; //選擇下一步的邏輯比較簡單,在這道題只是在上次去的元素中+1即可(既可保證不會重複取已在所選數列的數,也能保證不會去取遍歷過得數)
if (tryTask < N) {
//成功嘗試選擇下一個task
currPos++;
currSum = currSum + task[tryTask];
pos[currPos] = tryTask; //把當前狀態信息入棧
lastTried = tryTask; //需要維護上一次取的元素
printit(task, pos, currSum);
if (Math.abs(currSum - AVG) < min) { //替換平均值差距最小的值
min = Math.abs(currSum - AVG);
result = Math.abs(2*currSum - SUM);
}else if (currSum > AVG){ //加入大過平均值則不繼續累加
currSum -= task[tryTask];
pos[currPos--] = 0;
}
} else {
//選擇下一步失敗,則回溯
lastTried = pos[currPos]; //還原當前task
currSum -= task[pos[currPos]];
pos[currPos--] = 0; //出棧
}
}
System.out.println(result);
}
遞歸方式:
初始化
處理當前步(1)
假如當前步不能走則回退
循環所有可能的下一步
把每個下一步當做當前步遞歸處理,就如從(1)開始處理一樣
public class Argo {
static int[] task = new int[] { 5, 8, 13, 27, 14 };
static int N = task.length;
static int[] pos = new int[N];
static int posIdx = -1;
static int SUM = 0;
static int AVG = 0;
static int result = 0;
static int min = 0;
static {
for (int i = 0; i < N; i++) {
SUM += task[i];
pos[i] = 0;
}
AVG = Math.round(SUM / 2);
min = SUM;
}
private static void doit2() {
for (int i=0;i<N;i++) {
tryRecurm(i,0);
pos[posIdx--] = 0;
}
System.out.println(result);
}
private static void tryRecurm(int tryTask, int currSum){
if (tryTask == N) { //沒法放則返回
return ;
}
pos[++posIdx] = tryTask; //pos只是用來調試用記錄變化
currSum += task[tryTask];
if (Math.abs(currSum - AVG) < min) {
min = Math.abs(currSum - AVG);
result = Math.abs(2 * currSum - SUM);
}
printit(task, pos, currSum);
//準備便利並嘗試每個下一步
for (int i=tryTask+1; i<N; i++) {
tryRecurm(++tryTask, currSum);
pos[posIdx--] = 0;
}
}
private static void printit(int[] task, int[] pos, int currSum) {
int n = pos.length;
System.out.println("currSum:" + currSum);
System.out.print("pos:\t");
for (int i = 0; i < n; i++) {
System.out.print(pos[i] + ",");
}
System.out.print("\ntask:\t");
for (int i = 0; i < n; i++) {
System.out.print(task[pos[i]] + ",");
}
System.out.println();
}
public static void main(String[] args) {
doit2();
}
}
遞歸法程序簡潔易懂,有天然的程序堆棧,不需要自己維護棧結構,可惜就是性能比不上非遞歸
還有另一個也是回溯的,沒細看:
http://blog.csdn.net/ljsspace/article/details/6434621
網上類似的題目,見下面鏈接:
http://blog.csdn.net/xufei96/article/details/5984647