回溯算法複習

近日部門搞了個算法比賽,太久沒寫過算法基本都生疏了。

有道題抽象出來是這麼說:

有一個數列,要採取怎樣的劃分方法把它分成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



發佈了38 篇原創文章 · 獲贊 19 · 訪問量 44萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章