石子合并 动态规划 java_动态规划求解:环形石子合并问题

问题:在一个圆形操场的四周摆放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转移公式易得为:

bc96faeacc33ca16116d489edac94c6a.png

当石子是环形排列,例如0,1,2,3,4,5 五堆石子排成圆环, 合并的最后一步一定是两个的石堆合并成一个石堆,那么就相当于一开始的时候将圆环切成2块,得到2条排成列的石子,再对每条序列分别使用上述的DP策略计算局部最优解。此时的状态转移方程为:

ba286f8ffbca2813bf19a6576cc00e92.png

但是此时的状态转移方程的含义有所变化,dp[i][j]表示从index为i的石堆开始,后面按序加上j堆石子的最优合并,一共是(j+1)堆。因此,sum[i][j]定义为:

石子合并 动态规划 java_动态规划求解:环形石子合并问题_第1张图片

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

//递归求解

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));

}

}

你可能感兴趣的:(石子合并,动态规划,java)