回溯算法复习

近日部门搞了个算法比赛,太久没写过算法基本都生疏了。

有道题抽象出来是这么说:

有一个数列,要采取怎样的划分方法把它分成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



你可能感兴趣的:(算法,String,面试,测试,Class,ini)