动态规划(dynamic programming)与分治方法类似,都是通过组合子问题的解来求解原问题。分治方法将问题划分为互不相交的子问题,递归的求解子问题,再将它们的解组合起来,求出原问题的解。与之相反,动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子子问题(子问题的求解是递归进行的,将其划分为更小的子子问题)。而动态规划算法对每个子子问题只求解一次,将其解保存在一个表格中,从而无需每次求解一个子子问题时都重新计算,避免了这种不必要的计算工作。
动态规划方法通常用来求解最优化问题。这类问题可以有很多可行解,每个解都有一个值,我们希望寻求具有最优值的解。我们称这样的解为问题的一个最优的解(an optimal solution),而不是最优解(the optimal solution),因为可能有多个解都达到最优解。
我们通常按如下4个步骤来设计一个动态规划算法:
1、刻画一个最优解的结构特征。
2、递归地定义最优解的值。
3、计算最优解的值,通常采用自底向上的方法。
4、利用计算出的信息构造一个最优解。
一、钢条切割
定义一段长度为i英寸的钢条的价格为pi(i=1,2,...)
长度i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
价格pi | 1 | 5 | 8 | 9 | 10 | 17 | 17 | 20 | 24 | 30 |
钢条切割问题:给定一段长度为n英寸的钢条和一个价格表pi,求切割钢条方案,使得销售收益rn最大。
长度为n英寸的钢条共有2n-1种不同的切割方案,因为在距离钢条左端i(i=1,2,3,...,n-1)英寸处,我们总是可以选择切割或不切割。我们用普通的加法符号表示切割方案,如果一个最优解将钢条切割为k(1≤k≤n)段,那么最优切割方案:n=i1+i2+...+ik 将钢条切割为长度分别为i1,i2,...,ik的小段,得到最大收益rn=pi1+pi2+...+pik
对于上述价格表,我们可以观察所有最优收益值ri(i=1,2,...,10)及对应的最优切割方案:
r1=1,切割方案1=1(无切割)
r2=5,切割方案2=2(无切割)
r3=8,切割方案3=3(无切割)
r4=10,切割方案4=2+2
r5=13,切割方案5=2+3
r6=17,切割方案6=6(无切割)
r7=18,切割方案7=1+6或7=2+2+3
r8=22,切割方案8=2+6
r9=25,切割方案9=3+6
r10=30,切割方案10=10(无切割)
更一般地,对于rn(n≥1),我们可以用更短的钢条的最优切割收益来描述它:rn=max(pn,r1+rn-1,r2+rn-2,...,rn-1+r1)
第一个参数pn对应不切割,直接出售长度为n英寸的钢条的方案。其他n-1个参数对应另外n-1中方案:对每个i=1,2,...,n-1,首先将钢条切割为长度i和n-i的两端,接着求解这两段的最优切割收益ri和rn-i(每种方案的最优收益为两段的最优收益之和)。由于无法预知哪种方案会获得最优收益,我们必须考察所有可能的i,选取其中收益最大者。如果直接出售原钢条会获得最大收益,我们当然可以选择不做任何切割。为了求解规模为n的原问题,我们先求解形式完全一样,但规模更小的子问题。即当完成首次切割后,我们将两段钢条看成两个独立的钢条切割问题实例。我们通过组合两个相关子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大者,构成原问题的最优解。我们称钢条切割问题满足最优子结构(optimal substructure)性质:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。
除了上述求解方法外,钢条切割问题还存在一种相似的但更为简单的递归求解方法:我们将钢条从左边切割下长度为i的一段,只对右边剩下的长度为n-i的一段继续进行切割(递归求解),对左边的一段则不再进行切割。即问题分解的方式为:将长度为n的钢条分解为左边开始一段,以及剩余部分继续分解的结果。这样,不做任何切割的方案就可以描述为:第一段的长度文n,收益为pn,剩余部分长度为0,对应的收益为r0=0.于是我们可以得到公式rn =max1≤i≤n(pi+rn-i)。
带备忘的自顶向下法(top-down with memoization)
此方法按自然的递归形式编写过程,但过程会保存每个子问题的解(通常保存在一个数组或散列表中)。当需要一个子问题的解时,过程首先检查是否已经保存过此解。如果是,则直接返回保存的值,从而节省了计算时间。我们称这个递归过程是带备忘的,因为它记住了之前已经计算出的结果。
public class Memoized_cut_rod_test
{
public static int memoized_cut_rod(int[] p,int n,int[] r,int[] s)
{
if(r[n]>=0)
return r[n];
if(n==0)
{
r[n] = 0;
return 0;
}
int q = Integer.MIN_VALUE;
for(int i=1;i<=n;i++)
{
int temp = p[i]+memoized_cut_rod(p, n-i, r, s);
if(q<temp)
{
q = temp;
s[n] = i;
}
}
r[n] = q;
return r[n];
}
public static void main(String[] args)
{
int n = 7;
int[] p={0,1,5,8,9,10,17,17,20,24,30};
int[] r = new int[n+1];
int[] s = new int[n+1];
for(int i=0;i<=n;i++)
r[i] = Integer.MIN_VALUE;
System.out.println(memoized_cut_rod(p,n,r,s)+" "+s[n]);
}
}
自底向上法:
这种方法一般需要恰当定义子问题规模的概念,使得任何子问题的求解都只依赖于"更小的"子问题的求解。因而我们可以将子问题按规模排序,按由小到大的顺序进行求解。当求解某个子问题时,它所依赖的那些更小的子问题都已求解完毕,结果已经保存。每个子问题只需求解一次,当我们求解它时,它的所有前提子问题都已求解完成。
两种方法得到的算法具有相同的渐进运行时间,仅有的差异是在某些特殊情况下,自顶向下方法并未真正递归地考察所有可能的子问题。由于没有频繁的递归函数调用的开销,自底向上方法的时间复杂度函数通常具有更小的系数。
public class Memoized_cut_rod_test2 {
/**
* n=4时
*
* 4
* / | |
* 3 2 1 0
* / | / |
* 2 1 0 1 0 0
* / | |
* 1 0 0 0
* |
* 0
*/
public static void memoized_cut_rod(int[] p,int n,int[] r,int[] s){
r[0] = 0;
for(int i=1;i<=n;i++){
int q = Integer.MIN_VALUE;
for(int j=1;j<=i;j++){
if(q < p[j]+r[i-j]){
q = p[j]+r[i-j];
s[i] = j;
}
}
r[i] = q;
}
}
public static void main(String[] args) {
int n = 8;
int[] p={0,1,5,8,9,10,17,17,20,24,30};
int[] r = new int[n+1];
int[] s = new int[n+1];
for(int i=0;i<=n;i++)
r[i] = Integer.MIN_VALUE;
memoized_cut_rod(p, n, r, s);
System.out.print(r[n]+" "+s[n]);
}
}
二、矩阵链乘法
给定一个n个矩阵的序列(矩阵链)<A1,A2,...An>,我们希望计算它们的乘积 A1 A2 ... An ,我们可以先用括号明确计算次序,然后利用标准的矩阵相乘算法进行计算。由于矩阵乘法满足结合率,因此任何加括号的方法都会得到相同的计算结果。
A1 A2 A3 的规模分别为10×100、100×5、5×50。如果按(A1 A2)A3 的计算顺序,为计算A1 A2 需要做10×100×5=5000次标量乘法,再与A3 相乘又需要做10×5×50=2500次标量乘法,共需7500次标量乘法。如果按A1(A2 A3)的顺序,计算A2 A3 需要100×5×50=25000次标量乘法,A1 再与之相乘又需10×100×50=50000次标量乘法,共需75000次标量乘法。因此,按第一种顺序计算矩阵链乘积要比第二种顺序快10倍。
矩阵链乘法问题(matrix-chain multiplication problem)可描述如下:给定n个矩阵的链<A1 A2,...,An >,矩阵A 的规模为pi-1 ×pi (1≤i≤n),求完全括号化方案,使得计算乘积A1 A2 ...An 所需的标量乘法次数最少。
在使用动态规划方法求解矩阵链乘法问题之前,我们先来说服自己,穷举所有可能的括号化的数量。当n=1时,由于只有一个矩阵,因此只有一种完全括号化方案。当n≥2时,完全括号化的矩阵乘积可描述为两个完全括号化的部分积相乘的形式,而两个部分积的划分点在k个矩阵和第k+1个矩阵之间,k为1,2,...,n-1中的任意一个值。因此,我们可以得到如下递归公式:
P(n) = 1 n=1
∑k-1n-1 P(K)P(n-k) n≥2
由于括号化方案的数量与n呈指数关系,通过暴力搜索穷尽所有可能的括号化方案来寻找最优方案是一个糟糕的策略。
对矩阵链乘法问题,我们可以将对所有1≤i≤j≤n确定Ai Ai+1 ...Aj 的最小代价括号化方案作为子问题。令m[i][j]表示计算矩阵Ai...j 所需标量乘法次数的最小值,那么,原问题的最优解,计算A1...n 所需的最低代价就是m[1][n]。
对于i=j,矩阵链只包含唯一的矩阵Ai...i=Ai ,因此不需要做任何标量乘法运算。所以,对所有i=1,2,...,n,m[i][i]=0。若i<j,假设AiAi+1Aj的最优括号化方案的分割点在矩阵A 和A 之间,其中i≤k<j。那么,m[i][j]就等于计算Ai...k和Ak+1...j的代价加上两者相乘的代价的最小值。由于矩阵A 的大小为pi-1×pi,易知Ai...k与Ak+1...j相乘的代价为pi-1pkpj次标量乘法运算。因此,m[i][j]=m[i][k]+m[k+1][j]+pi-1pkpj
此递归公式假定最优分割点k是已知的,但实际上我们是不知道的。不过,k只有j-i中可能的取值,即k=i,i+1,...,j-1。由于最优分割点比在其中,我们只需检查所有可能情况,找到最优者即可。
A= A1 A2 A3 A4 ... An-1 An
p0 p1 p2 p3 p4 pn-2 pn-1 pn
自顶向下递归算法:
public class Matrix_chain
{
/*
* 1.4
* / / | |
* 1.1 2.4 1.2 3.4 1.3 4.4
* / | | / / / | |
* 2.2 3.4 2.3 4.4 1.1 2.2 3.3 4.4 1.1 2.3 1.2 3.3
* / / / /
* 3.3 4.4 2.2 3.3 2.2 3.3 1.1 2.2
*
*/
public static int recursive_matrix_chain(int[] p,int i,int j,int[][] m,int[][] s){
if(m[i][j]<Integer.MAX_VALUE)
return m[i][j];
if(i==j){
m[i][j] = 0;
return m[i][j];
}
int q = Integer.MAX_VALUE;
for(int k=i;k<j;k++){
int temp = recursive_matrix_chain(p, i, k, m, s)+
recursive_matrix_chain(p, k+1, j, m, s)+p[i-1]*p[k]*p[j];
if(q>temp){
q = temp;
s[i][j] = k;
}
}
m[i][j] = q;
return m[i][j];
}
public static void main(String[] args) {
int[] p = {10,100,5,50,100,10,50,100};
int n = p.length-1;
int[][] m = new int[n+1][n+1];
//s[i][j]记录最优值m[i][j]对应的分割点k
int[][] s = new int[n+1][n+1];
for(int i = 1;i<=n;i++)
for(int j=1;j<=n;j++)
//m[i][0],m[0][j]没有意义
m[i][j] = Integer.MAX_VALUE;
System.out.println(recursive_matrix_chain(p, 1, n, m, s));
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
System.out.print("s["+i+"]["+j+"]="+s[i][j]+" ");
}
}
结果:
67500
s[1][1]=0 s[1][2]=1 s[1][3]=2 s[1][4]=2 s[1][5]=2 s[1][6]=2 s[1][7]=2 s[2][1]=0 s[2][2]=0 s[2][3]=2 s[2][4]=2 s[2][5]=2 s[2][6]=2 s[2][7]=2 s[3][1]=0 s[3][2]=0 s[3][3]=0 s[3][4]=3 s[3][5]=4 s[3][6]=5 s[3][7]=6 s[4][1]=0 s[4][2]=0 s[4][3]=0 s[4][4]=0 s[4][5]=4 s[4][6]=5 s[4][7]=5 s[5][1]=0 s[5][2]=0 s[5][3]=0 s[5][4]=0 s[5][5]=0 s[5][6]=5 s[5][7]=5 s[6][1]=0 s[6][2]=0 s[6][3]=0
自底向上:
public class Matrix_chain
{
public static void recursive_matrix_order(int[] p,int[][] m,int[][] s)
{
int n = p.length-1;
for(int i=1;i<=n;i++)
m[i][i] = 0;
//sv表示矩阵链的长度
for(int v=2;v<=n;v++)
//因为矩阵链的长度为v,最后一个矩阵为An,所以for循环最大的矩阵为An-v+1
for(int i=1;i<=n-v+1;i++)
{
//表示Aij为长度为v的矩阵链
int j = i+v-1;
for(int k=i;k<j;k++)
{
int temp = m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j];
if(m[i][j] > temp)
{
m[i][j] = temp;
s[i][j] = k;
}
}
}
}
public static void main(String[] args)
{
int[] p = {10,100,5,50,100,10,50,100};
int n = p.length-1;
int[][] m = new int[n+1][n+1];
//s[i][j]记录最优值m[i][j]对应的分割点k
int[][] s = new int[n+1][n+1];
for(int i = 1;i<=n;i++)
for(int j=1;j<=n;j++)
//m[i][0],m[0][j]没有意义
m[i][j] = Integer.MAX_VALUE;
recursive_matrix_order(p, m, s);
System.out.println(m[1][n]);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
System.out.print("s["+i+"]["+j+"]="+s[i][j]+" ");
}
}
三、最长公共子序列
最长公共子序列问题(longest-common-subsequence problem)给定两个序列X=<x1 ,x2 ,...,xm>和Y=<y1 ,y2 ,..yn>,求X和Y长度最长的公共子序列。即求存在一个严格递增的X的下标序列<i1 ,i2 ,...ik>,对所有j=1,2,...,k,满足xij = yj。例如,X=<A,B,C,B,D,A,B>,Y=<B,D,C,A,B,A>,那么序列<B,C,A>就是X和Y的公共子序列。但它不是X和Y的最长公共子序列(LCS),因为它长度为3,而<B,C,B,A>也是X和Y的公共子序列,其长度为4。<B,C,B,A>是X和Y的最长公共子序列,<B,D,A,B>也是,因为X和Y不存在长度大于等于5的公共子序列。
如果用暴力搜索方法求解LCS问题,就要穷举X的所有子序列,对每个子序列检查它是否也是Y的子序列,记录找到的最长子序列。X的每个子序列对应X的下标集合{1,2,...,m}的一个子集,所以X有2m(Cm0 +Cm1+....)个子序列,因此暴力方法的运行时间为指数阶,对较长的序列是不实用的。
LCS问题具有最优子结构性质:
令X=<x1,x2,...,xm>和Y=<y1,y2,..yn>,Z=<z1,z2,...,zk>为X和Y的任意LCS。
1、如果xm=yn ,则zk=xm=yn 且Zk-1是Xm-1和Yn-1的一个LCS。
2、如果xm≠yn ,那么zk≠xm 意味着Z是Xm-1 和Y 的一个LCS。
3、如果xm≠yn ,那么zk≠yn 意味着Z是X和Yn-1 的一个LCS。
与矩阵链乘法问题类似,设计LCS问题的递归算法首先要建立最优解的递归式。我们定义c[i][j]表示Xi 和Yj 的LCS长度。如果i=0或j=0,即一个序列长度为0,那么LCS的长度为0。
0 若i=0或j=0
c[i][j]=c[i-1][j-1]+1 若i,j>0且xi=yj
max(c[i][j-1],c[i-1][j]) 若i,j>0且xi≠yj
public class MaxLength_LCS {
public static int lcs_length(char[] x,char[] y,int i,int j,int[][] c,int[][] b){
if(i==0||j==0){
c[i][j] = 0;
return c[i][j];
}
//序列中第i个字符对应数组下标i-1
if(x[i-1]==y[j-1]){
b[i][j] =1;
c[i][j] = lcs_length(x,y,i-1,j-1,c,b)+1;
}
else{
int temp1 = lcs_length(x,y,i-1,j,c,b);
int temp2 = lcs_length(x,y,i,j-1,c,b);
if(c[i][j]<temp1){
c[i][j] = temp1;
b[i][j] =2;
}
if(c[i][j]<temp2){
c[i][j] = temp2;
b[i][j] =3;
}
}
return c[i][j];
}
public static void print_LCS(int[][] b,char[] x,int i,int j){
if(i==0||j==0)
return;
if(b[i][j]==1){
print_LCS(b,x,i-1,j-1);
System.out.print(x[i-1]);
}else if(b[i][j]==2)
print_LCS(b,x,i-1,j);
else if(b[i][j] ==3)
print_LCS(b,x,i,j-1);
}
public static void main(String[] args) {
char[] x ={'A','B','C','B','D','A','B'};
char[] y ={'B','D','C','A','B','A'};
int m = x.length;
int n = y.length;
int[][] c =new int[x.length+1][x.length+1];
int[][] b = new int[x.length+1][x.length+1];
System.out.println(lcs_length(x,y,m,n,c,b));
print_LCS(b, x, m,n);
}
}
结果:
4
BCBA
自底向上:
public class MaxLength_LCS {
public static void lcs_length(char[] x,char[] y,int m,int n,int[][] c,int[][] b){
for(int i=1;i<=m;i++)
c[i][0] = 0;
for(int j=1;j<=n;j++)
c[0][j] = 0;
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++){
if(x[i-1] == y[j-1]){
c[i][j] = c[i-1][j-1]+1;
b[i][j] = 1;
}else if(c[i-1][j]>=c[i][j-1]){
c[i][j] = c[i-1][j];
b[i][j] = 2;
}else if(c[i-1][j]<c[i][j-1]){
c[i][j] = c[i][j-1];
b[i][j] = 3;
}
}
}
public static void print_LCS(int[][] b,char[] x,int i,int j){
if(i==0||j==0)
return;
if(b[i][j]==1){
print_LCS(b,x,i-1,j-1);
System.out.print(x[i-1]);
}else if(b[i][j]==2)
print_LCS(b,x,i-1,j);
else if(b[i][j] ==3)
print_LCS(b,x,i,j-1);
}
public static void main(String[] args) {
char[] x ={'A','B','C','B','D','A','B'};
char[] y ={'B','D','C','A','B','A'};
int m = x.length;
int n = y.length;
int[][] c =new int[x.length+1][x.length+1];
//b[i][j]表示对应计算c[i][j]时所选择的子问题的最优解
int[][] b = new int[x.length+1][x.length+1];
lcs_length(x,y,m,n,c,b);
print_LCS(b, x, m,n);
}
}
结果:BCBA
四、最优二叉搜索树(optimal binary search tree)
给定一个n个不同关键字的已排序的序列K=<k1 ,k2 ,...,kn >(因此k1 <k2 <...<kn ),我们希望用这些关键字构造一棵二叉搜索树。对每个关键字ki ,都有一个概率pi 表示其搜索频率。有些要搜索的值可能不在K中,因此我们还有n+1个"伪关键字"d0 ,d1 ,d2 ,...,dn 表示不在K中的值。d0 表示所有小于k1 的值,dn 表示所有大于kn 的值,对i=1,2,...,n-1,伪关键字di 表示所有在ki 和ki+1 之间的值。对每个伪关键字di ,也都有一个概率pi 表示对应的搜索频率。
与哈夫曼数的区别:哈夫曼数只计算叶子节点,最优二查搜索树计算所有的节点。
对于一个给定的概率集合,我们希望构造一棵期望搜索代价最小的二叉搜索树,我们称之为最优二叉搜索树。最优二叉搜索树不一定是高度最矮的。而且,概率最高的关键字也不一定出现在二叉搜索树的根节点。
给定关键字序列ki ,...,kj ,其中某个关键字,比如说kr (i≤r≤j),是这些关键字的最优子树的根节点。那么k 的左子树就包含关键字ki ,...,kr-1 (和伪关键字di-1 ,...,dr-1 ),而右子树包含关键字kr+1 ,...,kj (和伪关键字dr ,...,dj )。只要我们检查所有可能的根节点kr (i≤r≤j),并对每种情况分别求解包含ki ,...,kr-1 及包含kr+1 ,...,kj 的最优二叉搜索树,即可保证找到原问题的最优解。
细节--"空子树"。假定对于包含关键字ki ,...,kj 的子问题,我们选定kr 为根节点。k 的左子树包含关键字ki ,...,ki-1 。我们将次序列解释为不包含任何关键字。但子树仍然包含伪关键字。我们认为包含关键字序列ki ,...ki-1 的子树不含任何实际关键字,但包含单一伪关键字di-1 。对称地,如果选择kj 为根节点,那么kj 的右子树包含关键字kj+1 ,...,kj ,次右子树不包含任何实际关键字,但包含伪关键字dj 。
求解包含关键字ki ,...,kj 的最优二叉搜索树,其中i≥1,j≤n且j≥i-1(当j=i-1时,子树不包含实际关键字,只包含伪关键字di-1 )。定义e[i][j]为在包含关键字ki ,...,kj 的最优二叉搜索树中进行一次搜索的期望代价。最终,我们希望计算出e[1][n]。
j=i-1的情况最为简单,由于子树只包含伪关键字di-1 ,期望搜索代价为e[i][i-1]=qi-1 。
当j≥i时,我们需要从ki ,...,ki 中选择一个根节点kr ,然后构造一棵包含关键字ki ,...,kr-1 的最优二叉搜索树作为其左子树,以及一棵包含关键字kr+1 ,...,kj 的二叉搜索树作为其右子树。当一棵子树成为一个节点时,期望搜索代价有何变化?由于每个节点的深度都增加了1,这棵子树的期望搜索代价的增加值应为所有概率之和。对于包含关键字ki ,...,kj 的子树,所有概率之和:w(i,j)=∑k=ij pk+∑k-1j qk
因此,若kr 为包含关键字ki ,...,kj 的最优二叉搜索树的根节点,我们有如下公式:e[i][j]=p +(e[i][r-1]+w(i,r-1))+(e[r+1][j]+w(r+1,j)),因为w(i,j)=w(i,r-1)+p +w(r+1,j),因此e[i][j]可重写为e[i][j]=e[i][r-1]+e[r+1][j]+w(i,j)
最终递归公式:e[i][j]=q 若j=i-1
mini≤r≤j{e[i][r-1]+e[r][j]+w(i,j)} 若i≤j
e[i][j]的值给出了最优二叉搜索树的期望搜索代价。为了记录最优二叉搜索树的结构,对于包含关键字ki ,..., ki (1≤i≤j≤n)的最优二叉搜索树,我们定义root[i][j]保存根节点k 的下标r。
我们用一个表e[1..n+1][0..n]来保存e[i][j]值。第一维下标上界为n+1而不是n,原因在于对于只包含伪关键字dn 的子树,我们需要计算并保存e[n+1][n]。第二维下标下界为0,是因为对于只包含伪关键字d 的子树,我们需要计算并保存e[1][0]。对于d ,使用e[k][k-1]来保存。
为了避免每次计算e[i][j]时都重新计算w(i,j),我们将这些值保存在表w[1..n+1][0..n]中,这样每次可节省©(j-i)次加法。对基本情况,令w[i][i-1]=q (1≤i≤n+1)。对j≥i的情况,可如下计算:w[i][j]=w[i][j-1]+p +q。这样,对©(n )个w[i][j],每个的计算时间为©(1)。
public class Optimal_BST {
public static int optimal_bst(int[] p,int[] q,int i,int j,int[][] e,int[][] w){
if(e[i][j]!=0)
return e[i][j];
if(j == i-1){
e[i][j] = q[i-1];
return q[i-1];
}
int t = Integer.MAX_VALUE;
for(int k=i;k<=j;k++){
int temp = optimal_bst(p, q, i, k-1, e, w)+optimal_bst(p, q, k+1, j, e, w)+w[i][j];
if(t > temp){
t = temp;
}
}
e[i][j] = t;
return e[i][j];
}
public static void main(String[] args) {
int[] p = {0,15,10,5,10,20};
int[] q = {5,10,5,5,5,10};
int m = p.length;
int n = q.length;
int[][] e = new int[m+1][n];
int[][] w = new int[m][n];
for(int i=1;i<m;i++)
for(int j=i-1;j<n;j++){
if(j == i-1)
w[i][j] = q[j];
else
w[i][j] = w[i][j-1]+p[j]+q[j];
}
for(int i=1;i<m;i++)
for(int j=i-1;j<n;j++)
System.out.print("w["+i+"]["+j+"]="+w[i][j]+" ");
System.out.println((float)optimal_bst(p, q, 1,m-1, e, w)/100);
for(int i=1;i<=m;i++)
for(int j=0;j<n;j++)
System.out.print("e["+i+"]["+j+"]="+e[i][j]+" ");
}
}
结果:
w[1][0]=5 w[1][1]=30 w[1][2]=45 w[1][3]=55 w[1][4]=70 w[1][5]=100
w[2][1]=10 w[2][2]=25 w[2][3]=35 w[2][4]=50 w[2][5]=80
w[3][2]=5 w[3][3]=15 w[3][4]=30 w[3][5]=60
w[4][3]=5 w[4][4]=20 w[4][5]=50
w[5][4]=5 w[5][5]=35
2.75
e[1][0]=5 e[1][1]=45 e[1][2]=90 e[1][3]=125 e[1][4]=175 e[1][5]=275
e[2][0]=0 e[2][1]=10 e[2][2]=40 e[2][3]=70 e[2][4]=120 e[2][5]=200
e[3][0]=0 e[3][1]=0 e[3][2]=5 e[3][3]=25 e[3][4]=60 e[3][5]=130
e[4][0]=0 e[4][1]=0 e[4][2]=0 e[4][3]=5 e[4][4]=30 e[4][5]=90
e[5][0]=0 e[5][1]=0 e[5][2]=0 e[5][3]=0 e[5][4]=5 e[5][5]=50
e[6][0]=0 e[6][1]=0 e[6][2]=0 e[6][3]=0 e[6][4]=0 e[6][5]=10
自底向上:
public class Optimal_BST {
public static void optimal_bst(int[] p,int[] q,int k,int[][] e,int[][] w){
for(int i=1;i<=k+1;i++)
e[i][i-1] = q[i-1];
for(int s=1;s<=k;s++)
for(int i=1;i<=k-s+1;i++){
int j=i+s-1;
e[i][j] = Integer.MAX_VALUE;
for(int r=i;r<=j;r++){
int t = e[i][r-1]+e[r+1][j]+w[i][j];
if(t<e[i][j]){
e[i][j] = t;
//root[i][j] = t;
}
}
}
}
public static void main(String[] args) {
int[] p = {0,15,10,5,10,20};
int[] q = {5,10,5,5,5,10};
int m = p.length;
int n = q.length;
int[][] e = new int[m+1][n];
int[][] w = new int[m][n];
for(int i=1;i<m;i++)
for(int j=i-1;j<n;j++){
if(j == i-1)
w[i][j] = q[j];
else
w[i][j] = w[i][j-1]+p[j]+q[j];
}
for(int i=1;i<m;i++)
for(int j=i-1;j<n;j++)
System.out.print("w["+i+"]["+j+"]="+w[i][j]+" ");
optimal_bst(p, q, m-1, e, w);
for(int i=1;i<=m;i++)
for(int j=0;j<n;j++)
System.out.print("e["+i+"]["+j+"]="+e[i][j]+" ");
}
}