这两天看了一下这个问题,原题是这样的:
有N堆石子,现要将石子有序的合并成一堆,规定如下:每次只能移动相邻的2堆石子合并,合并花费为新合成的一堆石子的数量。求将这N堆石子合并成一堆的总花费最小(或最大)。
这个题目在第一次刚看的时候,一脸懵逼,题目看懂了,但是怎么去求花费怎么都没有想明白,后来在网上看了一下这个算法的别人的理解,才看明白。
大概意思如下:如果有三堆:4,5,6。
第一种方案:文字解释:4和5合并成一堆9,然后9再和6合并成15。花费就是4 + 5 + 9 + 6 = 24。 4 + 5是合并前两堆的花费,在合并后面两堆得时候需要把这个花费算进去。
第二种方案:文字解释:5和6合并成一堆11,然后11再和4合并成15。花费就是5 + 6 + 11 + 4 = 26。同上面的花费解释。
可以明显看出,当选择合并的顺序不同时,所产生的花费是不同的。
上面可以看出来,当三堆进行合并时,就能产生不一样的花费效果。那么在石堆数目约来越多时,不同的方案,花费是越来越不同的。
使用DP算法来解析这个问题:
我们以4 + 5 + 9 + 6 = 24这个方案来解释DP的思想。我们从数字上来进行解释(4 + 5)表示的是合并第1,2时花费的费用、9表示第1、2合并之后那堆石子的数目,6是第三堆石子的数目。
我们把这个数目扩展一下到 4 堆:4,5,6,7(我们这里没有使用最优解) 那么算式就成了
我是用画图的形式来说明的,可以看出,我们每一次就算下一次合并的时候都会把上一次合并的花费计算进去。是不是一目了然,道理原来这么简单。
算式加起来4+5+9+6+15+7=46
这个图并不是最优解,最优解应该是4+5+6+7+9+13=44。数字不同但是原理是一样的。各位可以自己画图去看一下,很简单,这个只是个例子而已。
下面来说说我自己的想法:
从上面看出前三堆所构成的花费是由 三部分构成,1、第一二合并的花费;2、第一二合并的个数和;3:第三堆本身个数
衍生一下,前四堆合并花费也一样由三部分,1、第一二三合并的花费,2、第一二三合并的个数和;3、第四堆本身个数
这样就很明显了,把大问题分解成了小问题,而且大问题包含了小问题的解,大问题和小问题同质同解。完完全全满足dp的性质。
剩下的就是从中找到最优解,因为当前是从1->2->3->4的顺序去合并的,还有(1->2)->(3->4)或者1->(2->3->4)的顺序,原理一样,这里面因为第二第三部分是固定的,所以只需要得到小问题的最优解,就可以求得大问题的最优解了。比如1->(2->3->4),(2->3->4)的合并,到底是先2,3还是先3,4哪个更好,同质的,和上面的想法分析一样。
get it
个人解法:
通过上面的想法,第一步:设计一个函数求出,从start->end的个数和
private int getSum(int[] stones, int start, int end) {
int sum = 0;
for (int a = start; a <= end; a++) {
sum += stones[a];
}
return sum;
}
创建了一个二维数组来存储最优花费。竖轴表示起始堆,横轴表示终止堆,(这个不要纠结,分析时记得是从0开始就好了)
先来一个三阶的花费的记录表格(只有一堆得话,本身不存在花费,满足实际情况)
4 | 5 | 6 | |
4 | 0 | 9 | ? |
5 | / | 0 | 11 |
6 | / | / | 0 |
下面这个表保存的是从竖轴到横轴的个数和(对角线,意思是只有这一堆,那么没有所谓的个数和)
4 | 5 | 6 | |
4 | 4 | 4+5 | |
5 | / | 5 | 5+6 |
6 | / | / | 6 |
两张表格合起来一起看,最终4~6的总花费有两种情况,1、红色相加;2、绿色相加。
衍生到4个数字(左边是最优解,右边是个数和)
4 | 5 | 6 | 7 | 4 | 5 | 6 | 7 | |||
4 | 0 | 9 | ? | 4 | 4 | 9 | 15 | 22 | ||
5 | / | 0 | 11 | ? | 5 | / | 5 | 11 | 18 | |
6 | / | / | 0 | 13 | 6 | / | / | 6 | 13 | |
7 | / | / | / | 0 | 7 | / | / | / | 7 |
这样就有三种情况了(0,1--2,3)(0--1,2,3)(0,1,2--3),不存在(0,3--1,2),因为0和3两个数字并不是相邻数字(在该问题的另一个模式,当处于环形状态时,0,3是相邻的,解法和这个类似,有兴趣的同学可以自己研究一下。)。最后比较一下这三个值哪个最小就好了。
大家有没有发现一个规律,每一个正方形的矩阵,最右上角的值都是由它左侧和下侧的值求出来的,所以我们也需要根据这个顺序来填表。已知的是对角线的数字,所以填表方向就可以从这个角度入手。
(0, 0)->(1, 1)->(2, 2)->(3, 3)---------->横竖轴相差 0
(0, 1)->(1, 2)->(2, 3)---------->横竖轴相差 1
(0, 2)->(1, 3)---------->横竖轴相差 2
(0, 3)------>这个就是我们最终要的值
啰嗦废话了一大堆,直接上代码吧
private void cut(int[] stones) {
int length = stones.length;
int[][] product = new int[length][length];
for (int i = 0; i < length; i++) {
for (int j = 0; j < length && (j + i) < length; j++) {
product[j][j + i] = 500000;
if (i == 0) {
product[j][j + i] = 0;
} else {
for (int split = j; split < j + i; split++) {
product[j][j + i] = Math.min(product[j][j + i], product[j][split]
+ product[split + 1][j + i] + getSum(stones, j, j + i));
}
}
}
}
}
前两层的for循环,就是对角线方向的计算输出表格内的值,第三个split的for循环就是去比较不同分组情况下每一个花费,并且在计算之后进行Math.min()的比较,记录下最小值的情况。
最后的结果如下:
对比我们之前手动画出来的最优解表格,可以看出两者是相同的。方案OK~
-------------------------------------------
上面从一个很啰嗦的角度去分析了这个问题,并且用到了用小问题的解去解决大问题的dp的思想。
记录下来主要是为了理清自己的思路,在刚开始拿到这个题目的时候,走了不少的弯路,老是想走捷径去直接一步登天,现在看来还是得把方法一步步的写出来,画下来,才能理清思路。只有看清了,coding起来也就水到渠成。