以空间换时间——动态规划算法及其应用:矩阵链相乘

动态规划算法是5大算法基础中最重要的一个,它专门用来解决平面世界下的应用,即会多次使用二维数组。
当然动态规划算法是空间换时间的算法,也就是说:我们可以利用空间资源来使某算法问题的时间复杂度降到最低。
当然算法题中,动态规划也是占了一大部分

说到以空间换时间,上一篇中的锦标赛算法就是一个以空间换时间的算法。锦标赛算法创建了一个2倍的原始数组的临时数组,是用来保存原始数组中的数,和相互比较的结果;然后再通过筛选被最大值打败的数中找到一个最大值,这个值就是第二大的值。这就是锦标赛算法。通过以空间换时间,我们可以从蛮力算法的2n-3,降到n+⌈logn⌉-2的量。

然而,锦标赛算法是对两个独立的子问题进行归结,所以充其量是一个分治算法。

那么什么是动态规划算法?它和分治算法有什么区别?

本篇主要介绍什么是动态规划算法,以及用动态规划算法进行分析问题,和它的一个经典问题:矩阵链相乘问题。

1、动态规划算法

什么是动态规划算法

动态规划算法与分治法基本相似的,它也是将问题划分成若干个子问题,对子问题进行求解,从这些子问题的解中得到原问题的解。然而动态规划算法是一个多阶段决策的算法。

动态规划算法也正是处理这种多决策问题,使问题的最后结果达到最优解的问题。就比如说最少或者是最多这一类似的问题,我们都可以考虑用动态规划算法来解决。

在不同阶段中,动态规划算法就是将某一阶段的算法达到最优,从而应用到所依赖的下一阶段中的算法中。也就是说,动态规划算法对子问题是要求依赖关系的。而对没有依赖关系的不能用动态规划算法。

什么是依赖关系

就是说,你在某一个阶段或者某一个子问题中,我们求出来一个最优的结果。这个最优的结果是可以拿来当做动态规划算法的解。如果这一个解只是满足于那一个小的子问题或者是小的阶段,而对于总问题中,这个解是不包含在总问题的最优解中,则这就具有依赖关系

动态规划算法的例子

有一个问题:
有5个起点,这5个起点都会经过不同长度的路分别到达5个终点
问:请找出一条从起点到终点的最短路

如图:

那么根据这张图如何去寻找一条最短路径。我们可以通过蛮力法来解,就是说,考察每一条从某个起点到某个终点的路径,计算长度,从中找到最短路径。

我们经过计算可以找到一条最短路:

结果我们发现,从S3->A3->B4->C4->A3->T4和从S5->A4->B4->C4->T4的长度都是最小的是:10。

去想啊,如果你用蛮力法这样的题,会很浪费时间,用蛮力法解这个题的时间复杂度会是一个指数级。所以我们考虑有没有一个比较好的方法来解这样的题。

这个比较好的方法就是上面提到的那个多决策算法的动态规划算法。

动态规划算法,就是将每步求解的问题是后面阶段求解问题的子问题。每步决策将依赖于以前步骤的决策结果。

我们用动态规划算法来解这个问题
首先从终点开始寻找最短路径。划分子问题,先将C标号的进入第一阶段,计算终点到C标号的最短路径

我们看到啊,从终点经过C1的有两条路,上面的长度是2,下面的长度是5,因为上面的长度短,所以我们上面的长度;对于从终点经过C2的路径,我们会选择下面的3的长度的路径;对于从终点经过C3的路径,T3->C3是7,T4->C3也是7。我们一般会选取上面的路径作为最短路径。同样的对于从终点经过C4的最短路径是上面的1的长度

接下来我们将B标号的进入第二阶段

我们会将在第一阶段选取的最短路径为基础,来选取从C标号到B标号的最短路径。例如B2,从C1经过B2是3的长度,再加上从终点经过C1的最短路径:2,则就是5的长度;从C2经过B2是6的长度,再加上从终点经过C2的最短路径:3,则就是9的长度。我们自然会选择从C1经过B2,因为路径最短(5<9)

一样的,然后将标号A的进入第三阶段,最后将入口的进入最后一个阶段

我们看到入口S3和S5的长度都是10,而且都是最短路径。然后我们追踪解,来追踪到达入口的最短路径,例如:

我们从S3开始追踪,看到啊,顶上的标号是d,10,那么S3则往下面走到A3(d是选取经过该标号的两条路中的下面的路为最短路径);接下来,A3顶上的标号是d,6,那么A3则往下面走是B4;一样的,B4顶上的标号是d,5,那么则往下面走是C4;最后C4顶上的标号是u,1(u是选取经过该标号的两条路中的上面的路为最短路径),那么C4则往上面走是T4。因此,我们选取了一条样的路径:S3->A3->B4->C4->T4。这一条路径就是10的长度,而且这一条路径就是其中1条最短路径。同样的,我们也能找到从S5出发的最短路径
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-loQ0wMRm-1586996979350)(/localImg/ArtImage/2020/03/2020031008394108.jpg)]

这两条最短路径就是问题的解。

我们看到动态规划算法,是一个有策略的,求出解的时间又是最短的算法。

我们看到动态规划算法讲究的是一个阶段性策略算法,同时又将最优解记录下来,以便在后边可以追踪解。从而得到原问题的最小(大)值和造成这个值的过程。

而且动态规划算法,是要求子问题之间要有依赖关系。如果没有依赖关系,那么原问题的解不包含子问题的最优解,则假如你在某一阶段求出来的解,然后下一个阶段再代入这个解,到了最后,求出来的解还不是原问题的最值。那求出来的又有何意义。因此动态规划算法一定是要满足依赖关系的。

那么如何设计动态规划算法

动态规划算法设计

1、构建问题规模,优化的目标函数是什么?约束条件是什么?
2、如何划分子问题
3、问题的优化函数值与子问题的优化函数值存在着什么依赖关系
4、是否满足优化原则
5、最小子问题怎样界定?

然后对于解的追踪,我们应该考虑,用空间记录解,然后自底向上追踪解。

下面我们动态规划的经典算法——矩阵链相乘

2、矩阵链相乘

什么是矩阵链相乘

比如说,A1,A2,…,An为一矩阵序列,Ai的列是Ai+1的行,确定矩阵的乘法顺序,使得元素相乘的总次数最少。
输入:P=<P0,P1,…Pn>,P0就是A1的行,P1既是A1的列也是A2的行。因此满足Ai就是Pi-1·Pi的阶的矩阵
输出:矩阵链乘法中加的括号的位置。

我们看一个实例:
例如:P=<10,100,5,50>
我们能根据这个输入,我们能写出矩阵序列:
A1:10·100,A2:100·5,A3:5·50
我们想求出A1·A2·A3的值,那么怎么才能让乘法次数最少,还能求出结果呢?
比如说:
我们知道A1,A2这一个矩阵链进行了P0·P1·P2的次数。
就是说A1是3·5的矩阵,而A2是5·7的矩阵。那么这两个矩阵相乘,会进行3·5·7=105次的乘法。

那么来解下原问题:
乘法次序:
(A1A2)A3:10·100·5+10·5·50=7500次
A1(A2A3):100·5·50+10·100·50=75000次

大家能看到啊,计算矩阵链乘积时,在结果相同下,加括号的不同位置导致了乘积次数的不同。(多个矩阵相乘不满足交换律,但满足结合律)

如何才能选择一种更好的加括号的位置呢?

用蛮力法设计呢。它的时间复杂度是一个指数级别。所以不采用蛮力法设计。

就得用动态规划来去设计算法

递归法解矩阵链问题

我们有输入向量<Pi-1,Pi,…,Pj>对应着Ai,Ai+1,…,Aj的行和列
最好划分的运算次数(乘积次数):m[i,j]

然后我们去找它们的依赖关系:
我们假设最优划分最后一次相乘发生在矩阵k的位置,即:
Ai…j=Ai…k · Ak+1…j
那么Ai…j的最优次数依赖于Ai…k与Ak+1…j的最优依赖次数

也就是说,我们把Ai…j的划分成Ai…k和Ak+1…j以及组合这两个子问题解的乘积次数。因此我们要找到一个划分k,使乘积次数达到最小。

我们可以列出递推方程:

这里的Pi-1PkPj是Ai…k和Ak+1…j组合这两个子问题解的乘积次数。类似于A1A2的次数+A3A4的次数,但别忘了A2和A3也要乘起来。

这个递归方程我们能看到,m[i,j]的最优解是和m[i,k]和m[k+1,j]有关的,其中一个达到最小,那原问题的解肯定就会达到最小。进而满足依赖关系

如何去找到一个更好的划分k呢?
我们将会把k插入到A1的后面,A2和A3之间等等,计算最优乘法次数,然后用一个空间保存一下k,便于追踪解

追踪解:我们会进行比较,然后找到最少的乘法次数,然后把k保存在s[i,j](这里s[i,j]是在这个范围内最优划分k的位置)

下面我们进行代码分析:
我们根据上面的递推方程来编写代码。
因为是要从k=1遍历到k=n-1,因为计算Pi-1PkPj,所以说就是从1开始。这里你要知道我给出了n个输入,但是n-1个矩阵相乘。这个根据上面的定义就能清楚

遍历情况知道了之后,我们可以用递归了,那么总乘法次数=递归(原数组,递归初始i=1,划分k,存储解的数组)+递归(原数组,划分k+1,末位置j,存储解的数组) 然后就是判断,将值和k记录下来。这就是递归法写代码

我们再来看如何追踪解:
首先给一个表:

我们假设是n=5的时候的矩阵链问题,即A1,A2,A3,A4,A5。

首先要知道m[1,5]是原问题的解,所以追踪解也要从s[1,5]看。
这里s[1,5]=3,那说明k=3
我们可以以此划分
(A1A2A3)(A4A5)
然而后面是两个矩阵相乘,那不需要再分了
所以我们对前面进行划分
表中s[1,3]=1
我们再以此进行划分就是
(A1(A2A3))(A4A5)
这就是问题的解

那如何去编写代码?
我们也可以用递归来进行实现,递归(存储解的数组,初始下标i,s[i][j]),递归(存储解的数组,s[i][j]+1,末位置j)。两个递归的前面是’(’,两个递归的后面是")"。大家可以根据上面追踪解的分析来去编写代码
下面我们上代码:

public static void main(String[] args) {
		// TODO Auto-generated method stub
		int a[]=new int[] {10,1,50,50,20,5};
	    int s[][]=new int[a.length][a.length];
		int total=MatrixchainMutiply(a, 1, a.length-1, s);
	    System.out.println("矩阵链相乘最少执行次数:"+total);
	    System.out.print("矩阵链相乘最优解是:");
		MatrixoutPut(s, 1, a.length-1);
		
	}

//递归法
	public static int MatrixchainMutiply(int a[],int i,int j,int s[][]) {
		if(i==j)
			return 0;
		int r=0x3f3f3f3f,sum;
		for(int k=i;k<j;k++) {
			sum=MatrixchainMutiply(a, i, k, s)+MatrixchainMutiply(a, k+1, j, s)+a[i-1]*a[k]*a[j];
			if(sum<r) {
				r=sum;
				s[i][j]=k;
			}
		}
		return r;
	}


//输出矩阵链相乘的最优解
	public static void MatrixoutPut(int s[][],int i,int j) {
		if(i==j)
			System.out.print("a"+i);
		else {
			System.out.print("(");
			MatrixoutPut(s, i, s[i][j]);
			MatrixoutPut(s, s[i][j]+1, j);
			System.out.print(")");
		}
	}
对递归法进行分析

我们看到啊,递归式是k=1,k<n(这里的n是a.length-1。也就是矩阵链的个数),共有n-1个数。那么从k=1到k=n-1之间,(T(n)+T(n-k)+O(n)) (n>=2)
以此我们列出如下式子:


我们看到用递归法解矩阵链问题,还是指数级的量。

我们经过分析发现造成时间复杂度大的原因和用递归求斐波那契数列一样。造成子问题计算重复,使得时间浪费。比如说m[i,4],在某一处递归中已经算过一次。而在另外的递归中又遇到了这个m[i,4],又得去算一遍。又遇到一次又得算一遍。这样循环往复,造成子问题重复计算产生大量的时间浪费。

因为我们要对这个代码进行改进。我们可以采用空间换时间的策略,记录每个子问题首次计算结果,当后面再用时则直接取值,每个子问题只算一遍

迭代实现矩阵链问题

用迭代实现,首先明确最小子问题是什么,然后从这个最小子问题开始算起;然后考虑计算顺序,保证后面的值在前面已经计算好。我们用一个数组记录这些保存的值。而解的追踪就和上面的递归解法一样了。

我们发现啊,矩阵链乘法划分不同子问题会产生不同的长度:
长度1:只含1个矩阵,有n个子问题。类似于m[i,i],像这样的没有乘法不用计算,所以m[i,i]=0。我们不妨把这种不用计算的作为最小子问题,也就是作为初值。

长度2:含两个矩阵,有n-1个子问题。类似于A1A2,A2A3,A3A4…

长度3:含3个矩阵,有n-2个子问题。类似于A1A2A3,A2A3A4,A3A4A5…

长度n: 含n个矩阵的原始问题

那么长度由少变多,计算由易到难,因此可以根据这种长度顺序来计算子问题

说了这么多,可能还是会很懵逼。

下面进行迭代的算法分析。

我们令m数组为备忘录,用来存储子问题的最优解;用s数组存储k值。
首先,我们先把长度为1的那些矩阵的初值存到m数组里。即m[i][i]=0
然后开始通过长度2到长度n(n是矩阵链序列的个数),i从1到n+1-r开始寻找最优解
这里不妨设j是末位置。因为i是初位置,r是长度距离,因此有j-i+1=r,那么会有j=i+r-1
我们在长度为r的时候计算m[i][j],可以把m[i][i]拿过来,然后根据上边的递推法进行计算
m[i][j]=m[i][i]+m[i+1][j]+Pi-1·Pi·Pj。想想是不是可以这样写。再看看这个图想一想:
然后,再让s[i][j]=i。
下面让k=i+1,k<j。我们再让这个k在这之间进行计算。目的就是和递归那样的就一样了。
m[i][j]=m[i][k]+m[k+1][j]+Pi-1·Pk·Pj。

这里我们举个例子r=2的例子,走一下算法流程
i=1
j=2+1-1=2
m[1][2]=m[1][1]+m[2][2]+P0·P1·P2。然而m[1][1],m[2][2]是可以查的,因为在长度1的时候我们已经赋值了。
因为k>=j。所以不会经过m[i][j]=m[i][k]+m[k+1][j]+Pi-1·Pk·Pj。
当然还有一个原因就是长度为2的时候,两个矩阵的乘积次数是一个定值,大家可以想想就能想到。
就是这样m[2][3],m[3][4],m[4][5]都写进去了
当r=3时,i=1,j=3时,我们看到m[1][3]就可以写进去了
此时k=2,我们让k在P1,P2到P3之间插入进行测试,计算一个最优解。然后这个最优解的划分k就保存在了s数组里面。而且新的最少次数会覆盖m[1][3]的值
当然我们计算m[1][3]=m[1][k]+m[k+1][3]+P0·Pk·P3。实际上这个式子的m[1][2],m[2][3]是可以查的,所以不用计算。所以这个整个式子直接计算加法和组合解的乘法就可以了。两个m的值是已经知道的了。

这里计算了两次m[1][3],是为了找到合适划分和最优解。也就是说k的值是什么。然而大家应该能想到在k=i+1之前的是为了计算k在A1,A2之间的值;而随后的一次运算是为了计算k在A2,A3之间的值。我希望我把这些点说明白了。如果有不会的可以在底下评论。

ok,这就是迭代法的流程。而对于解的追踪,我们已经在递归法介绍了。

我们可以根据这个流程来编写代码:

public static void main(String[] args) {
		// TODO Auto-generated method stub
		int a[]=new int[] {10,1,50,50,20,5};
		int m[][]=new int[a.length][a.length];
		int s[][]=new int[a.length][a.length];
	    int total=MatrixchainMutiply2(a, m, s);
		System.out.println("矩阵链相乘最少执行次数:"+total);
	    System.out.print("矩阵链相乘最优解是:");
		MatrixoutPut(s, 1, a.length-1);
		
	}

       //输出矩阵链相乘的最优解
	public static void MatrixoutPut(int s[][],int i,int j) {
		if(i==j)
			System.out.print("a"+i);
		else {
			System.out.print("(");
			MatrixoutPut(s, i, s[i][j]);
			MatrixoutPut(s, s[i][j]+1, j);
			System.out.print(")");
		}
	}
	
	//动态规划算法
	public static int MatrixchainMutiply2(int a[],int m[][],int s[][]) {
		int sum=0x3f3f3f3f,j;
		for(int i=1;i<a.length;i++) 
			m[i][i]=0;  //将所有的m[i][i]初值设为0
		for(int r=2;r<a.length;r++)
			for(int i=1;i<=a.length-r;i++) {
				j=i+r-1;
				m[i][j]=m[i][i]+m[i+1][j]+a[i-1]*a[i]*a[j];
				s[i][j]=i;
				for(int k=i+1;k<j;k++) {
					sum=m[i][k]+m[k+1][j]+a[i-1]*a[k]*a[j];
					if(sum<m[i][j]) {
						m[i][j]=sum;
						s[i][j]=k;
					}
				}
			}
		return m[1][a.length-1];
	}
对迭代法进行分析

我们看到迭代里面是进行三个循环,所以时间复杂度就是O(n^3)。而追踪解的时间复杂度是O(n),这是因为有n个矩阵序列,所以要遍寻n次。
综上时间复杂度是O(n^3)

因此我们根据这两个方法分析出:

递归实现:时间复杂性高,空间较小

迭代实现:时间复杂性低,空间消耗多

原因:递归实现子问题多次重复计算,子问题计算次数呈指数增长,迭代实现每个子问题只计算一次

动态规划时间复杂度:

备忘录各项计算量之和+追踪解工作量

通常追踪解工作量不超过计算工作量,是问题规模的多项式函数

动态规划算法要素:

1、划分子问题,确定子问题边界,将问题求解转变成多步判断的过程

2、定义优化函数,以该函数极大(极小)值作为依据,确定是否满足优化原则

3、列优化函数的递推方程和边界条件

4、自底向上计算,设计备忘录(表格)

5、考虑是否需要设立标记函数

3、动态规划算法和分治算法的区别

1、 分治法通常利用递归求解。

2、 动态规划通常利用迭代法自底向上求解,但也能用具有记忆功能的递归法自顶向下求解。

3、 分治法将分解后的子问题看成相互独立的。

4、 动态规划将分解后的子问题理解为相互间有联系,有重叠部分。

        天地玄黄,宇宙洪荒。日月盈昃,辰宿列张。寒来暑往,秋收冬藏 《千字文》
发布了9 篇原创文章 · 获赞 0 · 访问量 14

猜你喜欢

转载自blog.csdn.net/qq_41926985/article/details/105549565
今日推荐