在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];
}
}