近日部门搞了个算法比赛,太久没写过算法基本都生疏了。
有道题抽象出来是这么说:
有一个数列,要采取怎样的划分方法把它分成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