近日部门搞了个算法比赛,太久没写过算法基本都生疏了。
有道题抽象出来是这么说:
有一个数列,要采取怎样的划分方法把它分成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.youkuaiyun.com/ljsspace/article/details/6434621
网上类似的题目,见下面链接:
http://blog.youkuaiyun.com/xufei96/article/details/5984647