分治算法题之数组元素求和

        前几天逛论坛,看到一个帖子里说到一个算法题目: 给出1到19的连续递增自然数数组,求所有和为20的可能元素加法组合。每个加法组合中的元素不能重复使用。 题目只是给出个元素求和一个特例,我们可以把题目定义的更宽泛些。给定一个连续的自然数数组a(a[i]>0,a.length>0),求所有数据元素的加法运算,使得其和等于另一个给定的整数数值m (m>0)。

        这是个分治算法的场景,题目的出发点如下

  • 得到给定的m,可能要通过多步加法运算得到。一次性得出结果往往很难。但我们可以将加法步骤进行拆分,每次拆分可以穷举出所有的可能排列。比如m=10,那么两步加法得到10的排列会有几种? 很明显 [0,10],[1,9],[2,8],[3,7],[4,6],[5,5],[6,4],[7,3],[8,2],[9,1],[10,0] 就是所有的穷举结果。
  • 在每次进行步骤一的加法拆分时,可以将数组同时按照中间位置进行拆分。并将步骤一得到的加法穷举结果代入。如此递归,直至数组中只有一个元素存在。

       如下以a=[1,2,3,4,5,6,7,8,9], m=10为例对步骤进行一下说明

       1. 将m进行两步加法划分得到[0,10],[1,9],[2,8],[3,7],[4,6],[5,5],[6,4],[7,3],[8,2],[9,1],[10,0] 所有的穷举结果。

       2. 将数组从中间数进行划分得到两个子数组left = [1,2,3,4,5] ,right =[6,7,8,9],如下图所示

       3. 对所有加法组合及对应子数组进行递归拆分求解,这里以加法组合[10,0]为例进行展开,如下图示


             如上图所示, 子数组[6,7,8,9] 求和为0的可能组合很明显根本不需要去求。我们只关心子数组[1,2,3,4,5] 求和为10的所有组合即可。那同样对10和子数组[12,3,4,5]进行同样的拆分。我们还可以继续拆分,直到数组中只有一个元素为止。但在这一步,我们直观上已经很明显能看出可能的组合结果如下图


4. 将数组[1,2,3,4,5] = 10,和[6,7,8,9] = 0 两个数组的求解结果进行合并,就是我们所要的对于[10,0]这个加法拆分方式的结果。

代码如下

import java.util.Arrays;

public class EqualsTeller {

	/**
	 * 将两个一维数组left[m],right[j]合并成m[m+j]数组。
	 */
	private int[] merge(int[] left,int[] right){
		int[] m = new int[left.length + right.length];
		System.arraycopy(left,0,m,0,left.length);
		System.arraycopy(right, 0, m, left.length, right.length);
		return m;
	}
	/**
	 * 将二维数组left[m][x] 和 right[n][y]数组合并为二维数组 merge[m*n][x+y]
	 */
	private int[][] crossMerge(int[][] left,int[][] right){
		
		if (left[0][0] == 0)
			return right;
		if (right[0][0] == 0)
			return left;
		
		int[][] m = new int[left.length * right.length][];
		int row = 0;
		for (int i=0;i< left.length;i++){
			for (int j=0;j< right.length;j++){
				int[] l = left[i];
				int[] r = right[j];
				m[row] = merge(l,r);
				row++;
			}
		}
		
		return m;
		
		
	}
	
	private int[][] appendMerge(int[][] left,int[][] right){
		int[][] m = new int[left.length + right.length][];
		for (int i=0;i< left.length;i++){
			     m[i] = left[i];
		}
		for (int i=left.length;i< m.length;i++){
			    m[i] = right[i - left.length];

		}
		
		return m;
		
		
	}
	
	
	/**
	 * 给定数组 a,和一个大于零的数值sum. 找出所有合计值为sum的加法运算,加法运算中所有的元素必须来自数组a,且不能重复。
	 * @param a 给定的数组。数组中的元素必须是连续的,递增的。例如[1,2,3,4,5,6,7,8,9]
	 * @param sum 假定的合计值。要求大于零。
	 * @return 二维数组,含有所有可能加法组合的方案。
	 */
	private int[][] findEqualFormula(int[] a,int sum){
		boolean valid_flag = false;
		int[][] m = new int[0][0];
		
		if (sum == 0){ //如果传入的sum是零的话,则无需再当前数组a中寻找可能的加法解。直接返回{{0}};
			m = new int[1][1];
		    m[0] = new int[]{0};
		}
		else
        if (a.length > 1){
            if (sum < a[0]){ //小于数组中最小值,返回{{-1}}即当前数组无法得出对应的Sum值。
            	m = new int[1][1];
    		    m[0] = new int[]{-1};
            }else if (sum > (a[0] + a[a.length -1])*(a.length)/2){ //大于数组和,返回{{-1}}即当前数组无法得出对应的Sum值。
            	m = new int[1][1];
    		    m[0] = new int[]{-1};
            }else{
             int middle = (a.length -1)/2; //取中间数,然后对数组进行左右划分。
    		 int[] left  = Arrays.copyOfRange(a, 0, middle+1);
    	     int[] right = Arrays.copyOfRange(a, middle+1, a.length);
    		 for (int i=0;i<=sum;i++){//通过for循环将Sum值得所有两步运算进行穷举。
    			int[][] la = findEqualFormula(left,i);//递归调用,直到传入的数组中只有一个元素为止
    			if (la[0][0] == -1) //如果返回{{-1}},则说明上述数组left元素无法通过加法运算得到对应的Sum值。这时,则说明当前for循环中的两步运算组合无效。忽略当前运算组合,继续下一个组合的尝试。
    				continue;
    			int[][] ra= findEqualFormula(right,sum - i);//递归调用,直到传入的数组中只有一个元素为止
                if (ra[0][0] == -1) //如果返回{{-1}},则说明上述数组right元素无法通过加法运算得到对应的Sum值。这时,则说明当前for循环中的两步运算组合无效。忽略当前运算组合,继续下一个组合的尝试。 			
    			    continue;
                int[][] ma = crossMerge(la,ra);//如果上述两个递归方法返回有效的元素组合,则将元素进行交叉合并,得出当前数组元素中可用的加法组合
                m = appendMerge(m,ma);
                valid_flag = true;
    		 }
    		 if (!valid_flag){ // 如果for循环中所有穷举的两步运算组合都不适用,则返回{{-1}}
    			 m = new int[1][1];
    			 m[0] = new int[]{-1};
    		 }
    		 
            }

        }else if (a.length == 1){
        	m = new int[1][1];
        	if (a[0] == sum) //如果分解到数组中只有一个元素时,如果与sum值相等,则是我们需要找的值。返回数组{{a[0]}}
        		m[0] = new int[]{a[0]};
        	else
        		m[0] = new int[]{-1};//如果分解到数组中只有一个元素时,如果与sum值不相等,则说明数组中的值无法通过求和得到我们的预期结果。返回数组{{-1}}
        }
        
     return m;   

	}
	
}

运行结果如下 以[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19] = 20 为例


1 19 
2 18 
3 17 
1 2 17 
4 16 
1 3 16 
5 15 
1 4 15 
2 3 15 
6 14 
1 5 14 
2 4 14 
1 2 3 14 
7 13 
1 6 13 
2 5 13 
3 4 13 
1 2 4 13 
8 12 
1 7 12 
2 6 12 
3 5 12 
1 2 5 12 
1 3 4 12 
9 11 
1 8 11 
2 7 11 
3 6 11 
1 2 6 11 
4 5 11 
1 3 5 11 
2 3 4 11 
1 9 10 
2 8 10 
3 7 10 
3 8 9 
1 2 7 10 
1 2 8 9 
4 6 10 
4 7 9 
1 3 6 10 
1 3 7 9 
5 6 9 
5 7 8 
1 4 6 9 
1 4 7 8 
2 3 6 9 
2 3 7 8 
1 5 6 8 
2 4 6 8 
1 2 3 6 8 
2 5 6 7 
3 4 6 7 
1 2 4 6 7 
1 4 5 10 
2 3 5 10 
1 2 3 4 10 
2 4 5 9 
1 2 3 5 9 
3 4 5 8 
1 2 4 5 8 
1 3 4 5 7 
2 3 4 5 6 


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值