前几天逛论坛,看到一个帖子里说到一个算法题目: 给出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