初阶问题:(贪心算法求解)
(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
以第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;
}
}