问题:在一个圆形操场的四周摆放N堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,合并的花费为这相邻两堆石子的数量之和。试设计算法,计算出将N堆石子合并成一堆的最小花费。
在解题之前,介绍一下“四边形不等式优化”,关于这个优化方法的证明,csdn以及网上其他博客上详细介绍了很多,查了很多资料还是有点一知半解,再次归纳简述如下:
即在DP问题中,经常可以解得如下的转移方程:
dp[i][j]=min{dp[i][k]+dp[k+1][j]+cost[i][j]}
求解这个方程需要for一遍i,for一遍j,再for一遍k,则算法时间复杂度为O(n^3),此时如果该方程满足四边形不等式,那就可以优化为O(n^2)。四边形不等式描述为(详细证明可以参考https://blog.csdn.net/noiau/a...):
对于( a < b <= c< d )
如果有 f[a][c] + f[b][d] <= f[b][c] + f[a][d],那么f满足四边形不等式。
由四边形不等式可以给出2个定理(不作证明):
给出两个定理:
- 如果上述的cost函数同时满足区间包含单调性和四边形不等式性质,那么函数dp也满足四边形不等式性质 。
- 定义s(i,j)表示 dp(i,j) 取得最优值时对应的下标(即 i≤k≤j 时,k 处的 dp 值最大,则 s(i,j) = k此时有如下定理:假如dp(i,j)满足四边形不等式,那么s(i,j)单调,即 s(i,j)≤s(i,j+1)≤s(i+1,j+1)
那么状态转移方程可以由 m[i,j] = min{m[i,k] + m[k,j]} (i ≤ k ≤ j)转化为
m[i,j] = min{m[i,k] + m[k,j]} (s[i,j-1] ≤ k ≤ s[i+1,j])
那么在对k循环的时候,就不比每次都从当前的i遍历到j,而是以s[i,j-1]和s[i+1,j]替换,即缩小了k的取值范围,s[i,j] 的值在 m[i,j] 取得最优值时,保存和更新,因此 s[i,j-1] 和 s[i+1,j] 都在计算 dp[i][j-1] 以及 dp[i+1][j] 的时候已经计算出来了。
当区间长度为(L+1)时,在n个元素中,所有可能的k全部遍历次数叠加为:
(s[2,L+1]-s[1,L])+(s[3,L+2]-s[2,L+1])…+(s[n-L+1,n]-s[n-L,n-1])=s[n-L+1,n]-s[1,L] ≤ n,所以遍历k产生的代价可以省略,复杂度降低到O(n^2)。
准备工作完毕回到石子合并问题,如果所有石子排成一列,那么解法很简单,DP转移公式易得为:
当石子是环形排列,例如0,1,2,3,4,5 五堆石子排成圆环, 合并的最后一步一定是两个的石堆合并成一个石堆,那么就相当于一开始的时候将圆环切成2块,得到2条排成列的石子,再对每条序列分别使用上述的DP策略计算局部最优解。此时的状态转移方程为:
但是此时的状态转移方程的含义有所变化,dp[i][j]表示从index为i的石堆开始,后面按序加上j堆石子的最优合并,一共是(j+1)堆。因此,sum[i][j]定义为:
N堆石子成列,那么起始index是第0堆,成环的话那么起始位置可能是0~N-1一共N种可能,例如0,1,2...8,9共10堆石子成环摆放,那么其解应该为min(dp[i][9]),i为起始index值域为0~9。
直线型的石子合并问题JAVA代码如下:
import java.util.Scanner;
public class StoneLine {
public static int dpMethod(int[] stones, int i, int j) {
if (i == j) {
return 0;
}else {
int min = Integer.MAX_VALUE;
for (int k = i; k <j; k++) {
//递归求解
int tmp = dpMethod(stones, i, k) + dpMethod(stones, k + 1, j) + dpSum(stones, i, j);
if (min > tmp)
min = tmp;
}
return min;
}
}
public static int dpSum(int[] stones, int i, int j) {
int sum = 0;
for (int k = i ; k <= j; k++) {
sum += stones[k];
}
return sum;
}
public static void main(String[] args) {
//第一行输入石子堆数n,第二行输入每堆石子的数量,用空格分开
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int[] stones = new int[n];
for (int i = 0; i < n; i++) {
stones[i] = in.nextInt();
}
System.out.println(dpMethod(stones, 0, n - 1));
}
}
使用四边形不等式优化之后(由于优化过程需要配合新的数组s,所以要将递归体拆开)
import java.util.Scanner;
public class StoneLine {
public static int dpSum(int[] stones, int i, int j) {
int sum = 0;
for (int k = i ; k <= j; k++) {
sum += stones[k];
}
return sum;
}
public static int dpOptimization(int[] stones) {
int n = stones.length;
//由于s[i,j-1] ≤ k ≤ s[i+1,j],所以数组index:i和j要加1
int[][] dp = new int[n + 1][n + 1];//dp存i到j的最优值
int[][] s = new int[n + 1][n + 1];//s存i到j的最优情况下的k值
for (int i = 0; i < n; i++) {
dp[i][i] = 0;
s[i][i] = i;
}
//i是倒序的,这样才能在求dp[i][j]的时候调用dp[i+1][j]的最优决策s[i+1][j];
//而j是顺序的,这样才能在求dp[i][j]的时候调用dp[i][j-1]的最优决策s[i][j-1];
for (int i = n - 1; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
int tmp = Integer.MAX_VALUE;
int fence = 0;
for (int k = s[i][j - 1]; k <= s[i + 1][j]; k++) {
int sum = dp[i][k] + dp[k + 1][j] + dpSum(stones, i, j);
if (tmp > sum) {
tmp = sum;
fence = k;
}
}
dp[i][j] = tmp;
s[i][j] = fence;
}
}
return dp[0][n-1];
}
public static void main(String[] args) {
//第一行输入石子堆数n,第二行输入每堆石子的数量,用空格分开
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int[] stones = new int[n];
for (int i = 0; i < n; i++) {
stones[i] = in.nextInt();
}
System.out.println(dpOptimization(stones));
}
}
环形石子合并问题,递归解法(优化前)
import java.util.Scanner;
public class StoneCircle {
public static int dpMethod(int[] stones, int i, int j) {
if (i == j) {
return 0;
} else {
int min = Integer.MAX_VALUE;
for (int k = i; k < j; k++) {
//递归求解
int tmp = dpMethod(stones, i, k) + dpMethod(stones, k + 1, j) + dpSum(stones, i, j);
if (min > tmp)
min = tmp;
}
return min;
}
}
public static int dpSum(int[] stones, int i, int j) {
int sum = 0;
for (int k = i; k <= j; k++) {
sum += stones[k];
}
return sum;
}
public static void main(String[] args) {
//第一行输入石子堆数n,第二行输入每堆石子的数量,用空格分开
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int[] stones = new int[n * 2];
for (int i = 0; i < n; i++) {
stones[i] = in.nextInt();
}
for (int i = n; i < n * 2; i++) {
stones[i] = stones[i - n];
}
int min = Integer.MAX_VALUE;
for (int i = 0; i < n; i++) {
int tmp = dpMethod(stones, i, (n - 1 + i));
if (min > tmp) {
min = tmp;
}
}
System.out.println(min);
}
}
用递归环形问题有一个缺点,那就是要分别以N个位置为起始,那么需要增加一层循环,复杂度提高。
如下代码是最终解法,拆开递归,并且将N个石堆复制一份为2N堆,化环为直,如1,2,3的环转化为1,2,3,1,2,3的直线。直接对这2N个直线石子列求解,其中每个子问题的解都保存在dp[i][j]中。
import java.util.Scanner;
public class StoneCircle {
public static int dpSum(int[] stones, int i, int j) {
int sum = 0;
for (int k = i; k <= j; k++) {
sum += stones[k];
}
return sum;
}
public static int dpOptimization(int[] stones) {
int n = stones.length;
int[][] dp = new int[n + 1][n + 1];
int[][] s = new int[n + 1][n + 1];
for (int i = 0; i < n; i++) {
dp[i][i] = 0;
s[i][i] = i;
}
for (int i = n - 1; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
int tmp = Integer.MAX_VALUE;
int fence = 0;
for (int k = s[i][j - 1]; k <= s[i + 1][j]; k++) {
int sum = dp[i][k] + dp[k + 1][j] + dpSum(stones, i, j);
if (tmp > sum) {
tmp = sum;
fence = k;
}
}
dp[i][j] = tmp;
s[i][j] = fence;
}
}
int min = Integer.MAX_VALUE;
for (int i = 0; i < n / 2; i++) {
if (min > dp[i][i + (n / 2 - 1)]) {
min = dp[i][i + (n / 2 - 1)];
}
}
return min;
}
public static void main(String[] args) {
//第一行输入石子堆数n,第二行输入每堆石子的数量,用空格分开
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int[] stones = new int[n * 2];
for (int i = 0; i < n; i++) {
stones[i] = in.nextInt();
}
for (int i = n; i < n * 2; i++) {
stones[i] = stones[i - n];
}
System.out.println(dpOptimization(stones));
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。