这算是编程之美上面一道很经典题目,不过题目还是有几种变形,一种是要求两边有相同个数的元素(开始元素个数保证为偶数,编程之美上的原题),另一道限制较宽松,对两边子数组的元素个数没有要求,只要元素和之间尽可能的接近;
这道题目不是具有很严格的最优子结构,但是按照下面所摘录的博客思路增大一维的状态空间 逼近一个不确定的目标值,(而不是固定的sum/2)可以对应到动态规划求解,但是觉得这种思路不太优雅,而且在元素值比较大时算法复杂度太大(O(n*sum) sum>>n),其实按照作者思路稍微变通一下,舍去扫描一个元素更新整个状态空间的,直接将已有的状态值加上当前元素插入新状态就好,因为这样我们记录所有可达的状态,不用去优化任意一个状态S(1-sum/2)的逼近值,算法效率会大大提升;
综上所述,我们可以按题目中的两种要求分别设计方案:
1.对元素个数没有限制 : 只需使用set记录所有当前可达的部分和状态即可(如果要求出一个最优解的话使用map,key为状态值,val为前驱状态值,回溯回去可得一个可行序列)
set<int> partialSumSet;//元素每一趟扫描,不能一个个插入新值,只能在一趟更新结束后一次性加入
set<int> newStateSet;
for(int i=0; i<sizeof(A)/sizeof(A[0]); i++)
{
for(set<int>::iterator it=partialSumSet.begin(); it!=partialSumSet.end(); it++)
newStateSet.insert(*it+A[i]);
newStateSet.insert(A[i]);//别忘了插入单个元素值,在一开始插入一个0等效
//一次性插入新元素
for(set<int>::iterator it=newStateSet.begin(); it!=newStateSet.end(); it++)
partialSumSet.insert(*it);
newStateSet.clear();
}
如果要保证元素相同可以考虑同时记录每个状态对应的元素个数(可能有多个值,用set记录)map<int sum, set<val_num> >,具体代码类似
下面的讨论引自一篇文章,作者分析的很到位,但是还是有不少逻辑不严密的地方,算法复杂度也偏大,但分析思路值得借鉴的,原帖地址:http://blog.youkuaiyun.com/ultrani/article/details/7409584
在iteye看到一个问答(iteye被csdn收编了,该不算广告吧),大致是:给出一个数组和一个数字target,问数组那几个数之和与target相等。
问题看起来还挺简单。不过代码却不是一步到位立马能写出的。想着想着,突然发现这个问题和我之前发的博文中描述的问题基本是同一个类型的问题(见回溯算法复习)。于是由自然而然的想用回溯进行穷举了。不过在这个问题的回答者中,有一个人回答说用动态规划解即可,这时就勾起我的兴趣了,难道这类题本来就可以通过动态规划解答?而本文后续给出的答案表明,这是肯定的。
在介绍该题解答之前,首先简单回顾下动态规划是怎么解题的。根据算法导论所介绍,该算法一般分为4个步骤:
- 定义最优解结构
- 递归定义最优解的值
- 自底向上计算最优解的值
- 由计算出的结果构造一个最优解
- 对每个j循环for (j=2..n)
- 对每个s循环for(s=1..Sum(A))
- if (s=Aj) W[s][j] = Aj 并到下一个s
- if (s<Aj)
- set 选择Aj的最接近和=Aj
- else
- set 选择Aj的最接近和=W[s-Aj][j-1]+ Aj;
- end if
- set 不选择Aj的最接近和=W[s][j-1]
- if (选择Aj使得更接近s) {
- set W[s][j]=不选择Aj的最接近和
- else
- set W[s][j]=选择Aj的最接近和
- end if
- end for
- end for
具体实现代码在文章最后给出。
问题解决了,但是,这个父子关系的递归式只有这一个吗?为什么用s参数和j参数来限定子问题?类似的,我们还可以用下面这个递归式表示:
W(s, {M})=APPR {W(s-Ai, {M-Ai})+Ai : Ai属于{M})}
其中{M}表示一个若干Ai的集合。这个递归式定义W(s,{M})为从{M}中取若干个元素使其相加最接近s时的和。这个和原来的其实很像,但是区别在于,后者中每个拥有n个元素{M}的父问题都有n-1个子问题。这样递归到最底层就有n!个子问题需要解决。而本质上原问题假使用穷举的方法枚举所有可能性,也只有2的n次方个问题,说明第二种子结构的划分要解决大量重复的子问题。因为W(s, {M})中引入的集合具有无序性,而第一个W(s,j)却利用了有序性,由此可见不同的子结构在解决问题的范围还是有很大差异,关键是要提高子问题在甄别问题的解的效率。关于这个问题可以参考下面这篇文章:http://mindhacks.cn/2010/11/14/the-importance-of-knowing-why-part2/。其实文章所讨论的问题的解决思路也是借鉴于这篇文章的^_^。
附程序(该程序求解是回溯算法复习里面的题目,原理一样,本篇文章开头问题的代码就不另外贴出了):
- package puzzle;
- /**
- * 给出一个数组,要怎么划分成2个数组使得2数组和之差最少<br/>
- * 本质上就是,从数组中如何取数使其和等于某个target值,这里分割后的2个数组的平均值就是target值
- * @author nizen
- *
- */
- public class ArrayCutting {
- private int avg;
- private int[][] k;
- private void checkit(int[] array){
- if (array == null || array.length==0) {
- throw new IllegalArgumentException();
- }
- }
- // 初始化定义target值和边界值
- private void init(int[] array) {
- int sum = 0;
- for(int i=0;i<array.length;i++) {
- sum += array[i];
- }
- avg = Math.round(sum / 2);
- k = new int[avg+1][array.length+1];
- for (int w=1; w<=avg; w++) {
- for(int j=1; j<=array.length; j++) {
- if (j==1){
- k[w][j]=getValueJ(array,j);
- continue;
- }
- }
- }
- }
- public int[] cutit(int[] array) {
- checkit(array);
- init(array);
- // 自底向上构造矩阵
- for (int j=2; j<=array.length; j++) {
- for (int w=1; w<=avg; w++) {
- int valueAfterCutJ = w-getValueJ(array,j);
- int lastJ = j-1;
- if (valueAfterCutJ == 0) {
- k[w][j] = getValueJ(array,j); //选择J后差值为0则选择J为结果值
- continue;
- }
- int valueChooseJ = 0;
- if (valueAfterCutJ < 0) {
- valueChooseJ = getValueJ(array, j); //期望值比J小则取J为选择J后的值
- } else {
- valueChooseJ = k[valueAfterCutJ][lastJ] + getValueJ(array,j);
- }
- if (Math.abs(k[w][lastJ]-w) < Math.abs(valueChooseJ-w) ) {
- k[w][j]=k[w][lastJ];
- } else {
- k[w][j]=valueChooseJ;
- }
- }
- }
- return findPath(array);
- }
- // 最后一步:构造出最优解
- private int[] findPath(int[] array) {
- int[] result = new int[array.length];
- int p=0;
- int j=array.length;
- int w=avg;
- while(j>0){
- int valueAfterCutJ = w-getValueJ(array,j);
- int lastJ = j-1;
- if (valueAfterCutJ == 0) { //清0跳出
- result[p++]=getValueJ(array,j);
- w=w-getValueJ(array,j);
- break;
- }
- int valueChooseJ = 0;
- if (valueAfterCutJ < 0) {
- valueChooseJ = getValueJ(array, j); //期望值比J小则取J为选择J后的值
- } else {
- valueChooseJ = k[valueAfterCutJ][lastJ] + getValueJ(array,j);
- }
- if (Math.abs(k[w][lastJ]-w) > Math.abs(valueChooseJ-w) ) {
- result[p++]=getValueJ(array,j);
- w=w-getValueJ(array,j);
- }
- j=j-1;
- }
- return result;
- }
- public static void main(String[] args) {
- ArrayCutting ac = new ArrayCutting();
- int[] r = ac.cutit(new int[]{87,54,51,7,1,12,32,15,65,78});
- int selectedSum = 0;
- for (int i=0;i<r.length;i++){
- if (r[i]>0){
- selectedSum +=r[i];
- System.out.print(r[i]+"+");
- }
- }
- System.out.println("="+selectedSum+" Target="+ac.avg);
- }
- // 返回第j个数组元素
- private int getValueJ(int[]array, int j){
- return array[j-1];
- }
- }