回溯算法复习

来源:互联网 发布:苹果怎么只用2g网络 编辑:程序博客网 时间:2024/05/16 11:42

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

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

有一个数列,要采取怎样的划分方法把它分成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的元素indexint 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) {//成功尝试选择下一个taskcurrPos++;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];//还原当前taskcurrSum -= 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