基本思想
子问题:动态规划同样是把大问题拆分为小问题,小问题继续拆,直到没得拆(极其容易获得结果)。但这些拆开的小问题之间不是互相独立的,也就是同一层次的子问题之间存在结果的传递利用。
- 同一层次:即由同一个大问题拆分得到的多个小问题;
- 互相独立:排序如[2,1,4,3,6,5]拆分为A:[2,1,4]和B:[3,6,5],子问题A和B的排序互不影响,各排各的;
- 结果传递:如0-1背包,容量C里判断放入(a,b,c,d,e)中的哪些,拆成两部分(a)和(b,c,d,e),拿第二部分来看,选择放入谁必须先知道第一部分(a)是否放入了,因为它的结果直接影响背包所剩容量。
记录思想:上面的分析知道,子问题之间存在信息传递,所以我们要准备一个表,把遇到的子问题结果都记录下来,当别的问题用到其它子问题结果时,直接查表就可以了。
重要性质
最优子结构:某问题的最优解包含了其子问题的最优解。即大问题的最优解,可以通过所有子问题的最优解“合并”来获取。
重叠子问题:即某个子问题的继续拆分过程中,存在拆出来的子问题不是新问题(在其他的子问题上已经拆出来过),这样就存在了重复计算。也是为什么要用表记录的原因。
算法步骤
- 找出最优解的性质,并刻画其结构特征;——>什么样的解是最优解
- 递归地定义最优值;——>注意这里是递归的“定义”
- 以自底向上的方式计算出最优值;——>循环的方式从最小的子问题开始逐渐合并为所求是大问题
- 根据计算最优值时的信息,构造最优解;——>根据题目需要判断是否进行该步骤
例:矩阵连乘
问题描述:给定n个连乘矩阵(A1*A2*A3*...*An),求怎样加括号组合才能使整个的数乘次数最少。
分析:数乘次数只跟每个矩阵的维数有关,我们不关心具体数值,如A1:10×100、A2:100×5、A3:5×50。采用方式((A1A2)A3)得10*100*5+10*5*50=7500次,而方式(A1(A2A3))得10*5*50+10*100*50=75000次,10倍之差。
最优解结构:另A[i:j]表示连乘式,假设把该1~n的连乘矩阵链从某一处k切开,分别得到1~k子链和(k+1)~n子链,那么A[1:n]的计算量=A[1:k]的计算量+A[k+1:n]的计算量+两个子链结果相乘的计算量。利用反证法可以证明矩阵连乘具有最优子结构性质。A[1:n]的最优次序一定包含A[1:k]和A[k+1:n]的最优次序。
递归定义:以m[i][j]表示A[i:j]链的最少数乘次数,原问题就是求m[1][n]
自底向上的循环求解:见函数matrixChain
根据第三步中的信息构造最优解,也即得到最佳的加括号方式:见函数traceback
以A1:30×35、A2:35×15、A3:15×5、A4:5×10、A5:10×20、A6:20×25为例:
import java.util.Arrays;
/**
* @author sun
* 创建时间:2018年8月2日上午11:28:36
*/
public class 矩阵连成 {
public static void main(String[] args){
int[][] a = {{1,2},{3,4}};
int[][] b = {{1,1},{1,1}};
int[][] c = matrixMultiply(a,b);
System.out.println(Arrays.toString(c[0])+"\n"+Arrays.toString(c[1]));
int[] p = {30,35,15,5,10,20,25};
int[][] m = new int[7][7];//为了对应字母A1,A2...舍去第0行,第0列
int[][] s = new int[7][7];
matrixChain(p,m,s);
traceback(s,1,6);
}
//2个矩阵的乘积算法
public static int[][] matrixMultiply(int[][] a,int[][] b){
int ra = a.length,ca = a[0].length;
int rb = b.length,cb = b[0].length;
int[][] c = new int[ra][cb];//c用来存放乘积结果
if(ca!=rb)//A×B要求矩阵A的列数等于矩阵B的行数
throw new IllegalArgumentException("矩阵不可乘");
for(int i=0;i<ra;i++){
for(int j=0;j<cb;j++){
int sum = a[i][0]*b[0][j];
for(int k=1;k<ca;k++){
sum+=a[i][k]*b[k][j];
}
c[i][j] = sum;
}
}
return c;
}
//递归求解
public static void matrixChain(int []p,int[][] m,int[][] s){
//p记录所有矩阵的列数,第一个包括行,m记录当前分段的最少乘次数,s记录断开位置
int n=p.length-1;
for(int i=0;i<n;i++) m[i][i]=0;//对角线为0,表示分段只有一个矩阵,无需计算
for(int r=2;r<=n;r++){//分段的长度从2到总长n
for(int i=1;i<=n-r+1;i++){//起始位置从1
int j=i+r-1;
m[i][j]=m[i+1][j]+p[i-1]*p[i]*p[j];//这里是分为第一个和后面的两部分
s[i][j]=i;
for(int k=i+1;k<j;k++){
int t=m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j];
if(t<m[i][j]){
m[i][j]=t;
s[i][j]=k;
}
}
}
}
}
//构造最优解
public static void traceback(int[][] s,int i,int j){
if(i==j)return;
traceback(s,i,s[i][j]);
traceback(s,s[i][j]+1,j);
System.out.println("Multiply A"+i+","+s[i][j]+"and A"+(s[i][j]+1)+","+j);
}
}
参考
[1] 算法设计与分析(第2版) 王晓东