石子归并问题(难度:简单->一般->困难)-- 涉及贪心算法、矩阵连乘、动态规划

初阶问题:(贪心算法求解)
(1)有N堆石子,现要将石子有序的合并成一堆,规定如下:每次只能移动任意的2堆石子合并,合并花费为新合成的一堆石子的数量。求将这N堆石子合并成一堆的最低花费

		//有N堆石子,现要将石子有序的合并成一堆,
		// 规定如下:
		// 每次只能移动任意的2堆石子合并,合并花费为新合成的一堆石子的数量。求将这N堆石子合并的最低花费
		//可以认为是一个森林变树的过程(哈夫曼树)
		
		//贪心算法:将石子堆按石子数进行排序,每次取出最小值和次最小值进行归并
		//将归并结果继续排序
		public class Simple {
		
		    public static void main (String[] args) {
		        int[] arr = {1, 2, 2, 5, 8};
		        int cost = 0;//记录花费
		        //需要记录当前有几个数需要进行排序
		        int count = arr.length;
		        if (count > 1) qSort(arr, 0, count-1);
		
		        //这里使用快排还是太浪费了,使用插入排序比较合适。毕竟快排真正有效只在第一次比较时,之后都是有序的,只有一个数插入
		        while (count > 1){//需要归并n-1次
		            count--;
		            //取出最小的两个数相加,再放回
		            cost += arr[count]+arr[count-1];
		            iSort(arr, count-1, arr[count]+arr[count-1]);
		        }
		        System.out.print(cost);//输出最小花费次数
		    }
		
		    //需要一个排序来得到最小和次最小值
		    //选择使用快排(堆排或其他排序也行) --- 从大到小排
		    //快排需要选取一个目标数,目标数左边的数值都比其小,右边的都比他大
		    public static void qSort (int[] arr, int lf, int rt) {
		        int tmp = arr[lf];//选取左边第一个数作为快排目标数
		        int low = lf;
		        int high = rt;
		
		        while (low < high){
		            //从右往左比较,直到出现比目标数小的值
		            while (high > lf && arr[high] <= tmp)
		                high--;
		            if (arr[high] > tmp) {
		                arr[low] = arr[high];
		                low++;
		            }
		
		            //从左往右比较,直到出现比目标大的值
		            while (low < rt && arr[low] >= tmp)
		                low++;
		            if (arr[low] < tmp) {
		                arr[high] = arr[low];
		                high--;
		            }
		
		        }
		
		        //找到目标位置,存入
		        arr[low] = tmp;
		
		        //继续递归
		        if (low-1 > lf) {
		            qSort(arr, lf, low-1);
		        }
		
		        if (high+1 < rt) {
		            qSort(arr, high+1, rt);
		        }
		    }
		
		    public static void iSort (int[]arr, int len, int num) {
		        int poi = 0;
		        for (int i=0; i < len; i++){
		            if (arr[i] < num) break;
		            poi++;
		        }
		
		        //整体后挪
		        for (int i=len; i > poi; i--){
		            arr[len] = arr[len-1];
		        }
		        arr[poi] = num;
		
		    }
		
		}

问题进阶: 有N堆石子,现要将石子有序的合并成一堆,规定如下:每次只能移动相邻的2堆石子合并,合并花费为新合成的一堆石子的数量。求将这N堆石子合并成一堆的总花费最小(或最大)

在讲这一问题前需要借助矩阵乘法,所以先科普一下矩阵乘法怎么计算:
简单概括,左行右列对应相乘再相加(要求左边矩阵的列数等于右边矩阵的行数),构成新矩阵。
如,A1为一个10*100矩阵,A2为一个100*5矩阵,A1*A2为一个10*5矩阵
在生成A1*A2的过程中一共要使用乘法进行计算的次数为:10*100*5
注意:矩阵乘法只有结合律、分配律,没有交换律
矩阵拓展问题:给定n个矩阵{A1, A2,...,An},其中Ai与Ai+1可使用矩阵乘法(意味Ai列数等于Ai+1行数),求如何确定矩阵连乘的次序,使用连乘次数达到最小。
分析:假设A1维数为10*100,A2为100*5,A3为5*50
若(A1*A2)*A3 计算乘法数为:10*100*5 + 10*5*50 = 7500
若 A1*(A2*A3)计算乘法数为:100*5*50 + 10*100*50 = 75000
即不同的结合方式,会导致乘法数上的巨大差异
**动态规划求解:** 
1. 将矩阵连乘AiA(i+1)...Aj记为A[i:j]
2. 设以k(1<=k<=n)为界,计算A[1:k]和A[k+1:n]间的乘法量为:
	A[1:k]的计算量 + A[k+1:n]的计算量 + A[1:k]和A[k+1:n]相乘的计算量
3. 以此类推,求出每个子问题的最优解,子问题的最优解相乘便是原问题的最优解

构造二维数组m[n+1][n+1]来存放A[i:j]相乘的最小结果数(使用n+1牺牲0行0列,注意 1<=i <=j <=n)
使用数组p记录每个矩阵的行数和列数,由于矩阵Ai的列数和矩阵Ai+1的行数相同,所以只需要记录一遍,
如现在有三个矩阵分别为20*30,30*40,40*50,那么p中存入的是20,30,40,50
(即第i个矩阵的行数为p[i-1],列数为i --- 充分使用了数组0号元素空间)

设计算A[i:j]所需要的最小乘次数存放在m[i][j],则原问题的最优解存放在m[1][n]中
若i=j,则m[i][j]=0。即所有i=j,的最小乘次数均为0.故在二维数组m中对角线上(i=j)处存放的值均为0
当i j,不存在这种情况。矩阵不存在交换律A*B != B*A,所以真正有效的结果在矩阵中表现为对角线及其右上角区域

矩阵连乘代码求解:

		public class Matrix {
		
		    public static void main (String[] args) {
		        while (true) {
		            System.out.print("键入矩阵个数:");
		            Scanner sc = new Scanner(System.in);
		            int n = Integer.parseInt(sc.nextLine());
		            if (n <= 0) continue;
		
		            int[] p = new int[n+1];
		            System.out.print("键入矩阵行数、列数(以空格分割):");
		            String[] str = sc.nextLine().split("\\s+");
		            for (int i = 0; i < n+1; i++)
		                p[i] = Integer.parseInt(str[i]);
		
		            //构建存储最小计算量数组、标记数组
		            int[][] m = new int[n+1][n+1];
		            int[][] s = new int[n+1][n+1];
		            //构造查找表
		            matrixChain(m, s, p, n);
		
		            System.out.println("最小乘数:" + m[1][n]);
		            System.out.print("分割方案:");
		            printResult(s, 1, n);
		            System.out.println();
		            System.out.println("----------------------");
		        }
		
		    }
		
		    //自底向上,构建查找表(当然查找表只是记录了最小的计算次数,并未给出切分的细节)
		    public static void matrixChain (int[][] m, int[][] s, int[] p, int n){
		
		        //从对角线出发,逐渐向右上角靠拢
		        for (int t=2; t <= n; t++) {//对角线向右上角平移
		            //注意:为了计算简单,舍弃了第0行和第0列
		            //控制当前行、列处于当前平移线上
		            for (int i=1; i <= n+1-t; i++){//行控制
		                int j = t+i-1;//列控制
		
		                //开始寻找min{m[i][j]},切分线从k=i开始,到k=j-1结束,即将其分为A[i:k]和A[k+1:j]两部分
		                m[i][j] = m[i][i] + m[i+1][j] + p[i-1]*p[i]*p[j];//暂定初始值
		                s[i][j] = i;//暂定切分线为i
		                //开始寻求最小值
		                for (int k=i+1; k < j; k++) {
		                    int tmp = m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j];
		                    if (tmp < m[i][j]){
		                        m[i][j] = tmp;
		                        s[i][j] = k;
		                    }
		                }
		            }
		        }
		
		    }
		
		    //通过s[n+1][n+1]标记打印最终的结果
		    public static void printResult (int[][] s, int lf, int rt){
		
		        int tmp = s[lf][rt];
		        if (tmp == 0){
		            System.out.print("A" + lf);
		            return;
		        }
		
		        System.out.print("(");
		        printResult(s, lf, tmp);
		        System.out.print(")");
		
		        System.out.print("*(");
		        printResult(s, tmp+1, rt);
		        System.out.print(")");
		
		    }
		
		}

 测试结果:
 	键入矩阵个数:3
	键入矩阵行数、列数(以空格分割):2 3 4 5
	最小乘数:64
	分割方案:((A1)*(A2))*(A3)
	键入矩阵个数:

看完了矩阵连乘问题,再回到石子归并问题:在一条直线上有n堆石子,每堆有一定的数量,每次可以将两堆相邻的石子合并,合并后放在两堆的中间位置,合并的费用为两堆石子的总数。求把所有石子合并成一堆的最小花费。
是否发现,两道题神似,且石子归并问题应该相对矩阵连乘而言简单。
设dp[i][j]表示从第i堆石子到第j堆石子的归并最优解,sum[i][j]表示第i堆石子累加到第j堆石子的总数量。即状态转移公式为:
当 i=j 时,dp[i][j] = 0
当 i<=k

	import java.util.Scanner;

	public class Advance {
	
	    public static void main (String[] args) {
	
	        while (true) {
	
	            System.out.print("键入石堆数:");
	            Scanner sc = new Scanner(System.in);
	            int n = Integer.parseInt(sc.nextLine());
	
	            int[][] dp = new int[n+1][n+1];//记录最优解
	            int[] sp = new int[n+1];//存储每一堆石子原来的数量
	            System.out.print("键入每堆石子数(空格分割):");
	            String[] str = sc.nextLine().split("\\s+");
	            for (int i=1; i< n+1; i++)
	                sp[i] = Integer.parseInt(str[i-1]);
	
	            //求解
	            minStone(dp, sp, n);
	            for (int i = 1; i < n+1; i++){
	                for (int j=1; j < n+1; j++)
	                    System.out.print(dp[i][j] + " ");
	                System.out.println();
	            }
	
	            System.out.println("归并石子最低花费:" + dp[1][n]);
	            System.out.println("------------------------------");
	
	        }
	
	    }
	
	    //与矩阵连乘一致,同样是利用对角线向右上角平移的方法求解
	    public static void minStone (int[][] dp, int[] sp, int n){
	
	        for (int t=2; t <= n; t++) {// 控制对角线
	
	            for (int i=1; i <=n-t+1; i++){//控制行号
	
	                int j = i+t-1;//控制列号
	                int stones = countStone(sp, i,j);//本次需要累加的石子数
	
	                dp[i][j] = dp[i][i] + dp[i+1][j];//从i开始分割
	
	                for (int k=i+1; k 

问题深化: 如果把石子改为环形排列,又该怎么办?
分析:
假设共有n堆石子, stone[i]存储第i堆石子的重量,sum[i][j]记录从第i堆开始累加j堆得到的石头数
dp[i][j]记录从第i堆开始,合并后面j个堆(包括第i堆)得到的最小花费
则有,
当 j = 0,dp[i][j] = 0
当 0 <= k < j (这里解释一下 dp[(i+k)%n][j-k]:
以第1堆为起点,从第i堆后面第k+1堆起算,可能会超出总堆数n,所以进行取模操作,以表示环的概念。后面可能会出现 n%n=0的情况,需要做特殊处理,只对i+k > n的情况取模)

import java.util.Scanner;

public class Difficult {

    public static void main (String[] args){

        while (true){

            System.out.print("键入石堆数:");
            Scanner sc = new Scanner(System.in);
            int n = sc.nextInt();
            if (n <= 0) continue;

            int[] stone = new int[n+1];//记录每一堆的原始存储数
            System.out.print("键入每堆石子数(空格分割):");
            for (int i=1; i < n+1; i++)
                stone[i] = sc.nextInt();

            System.out.println("归并石子最低花费:" + countStone(n, stone));
            System.out.println("-----------------------");
        }

    }

    public static int countStone (int n, int[] stone) {

        int [][]dp = new int[n+1][n+1];//存储最优解(最小值)
        int [][] sum = new int[n+1][n+1];//存储从第i个到累加j个的石子数

        for (int i=1; i <= n; i++){
            //跳过j = 0 这种情况石子移动的花费都为0
            for (int j=1; j <= n; j++){//这里j要从1开始算,否则1位置的石子数丢失
                int t = i + (j-1);//注意:因为是从i算起的,所以要-1
                if (t > n) t= t%n;
                sum[i][j] = sum[i][j-1] + stone[t];
            }

        }

        for (int j=2; j <= n; j++){ // j为累加值,当j=0或1时,没有石子的累加,故跳过

            for (int i=1; i <= n; i++) {//i为起始石堆

                for (int k=1; k < j; k++){//k为切割位

                    int a = i+k;//a为范围数,注意i+k表示从i开始第k+1个数
                    if (a > n) a = a%n;

                    int tmp = dp[i][k] + dp[a][j-k] + sum[i][j];

                    if (dp[i][j] == 0 || dp[i][j] > tmp)
                        dp[i][j] = tmp;

                }
            }
        }

        int result = dp[1][n];
        for (int i=2; i <= n; i++){
            if (result > dp[i][n])
                result = dp[i][n];
        }

        return result;
    }


}

你可能感兴趣的:(算法)