3

问题:在一个圆形操场的四周摆放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个定理(不作证明):
给出两个定理:

  1. 如果上述的cost函数同时满足区间包含单调性和四边形不等式性质,那么函数dp也满足四边形不等式性质 。
  2. 定义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转移公式易得为:

clipboard.png

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

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

clipboard.png

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

    }
}

阮家炜
12 声望2 粉丝

程序猿顶呱呱