【笔记】《算法设计与分析(第三版)》-王晓东著-第3章-动态规划

第3章 动态规划

动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。

与分治法不同的是,适合于用动态规划法求解的问题,经分解得到的子问题往往不是互相独立的。若用分治法解这些问题,则分解得到的子问题数目太多,以至于最后解决原问题需要耗费指数时间。然而,不同子问题的数目常常只有多项式量级。在用分治法求解时,有些子问题被重复计算了许多次。如果能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,就可以避免大量重复计算,从而得到多项式时间算法。

为了达到这个目的,可以用一个表来记录所有已解决的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思想。具体的动态规划算法是多种多样的,但它们具有相同的填表样式。

动态规划算法适用于解最优化问题。通常可以按一下步骤设计动态规划算法:

(1)找出最优解的性质,并刻画其结构特征;

(2)递归地定义最优值;

(3)以自底向上的方式计算出最优值;

(4)根据计算最优值时得到的信息,构造最优解。

步骤(1)~(3)是动态规划算法的基本步骤。在只需要求出最优值的情形,步骤(4)可以省去。若需要求问题的最优解,则必须执行步骤(4)。此时,在步骤(3)中计算最优值时,通常需记录更多的信息,以便在步骤(4)中,根据所记录的信息,快速构造出最优解。

3.1 矩阵连乘问题

矩阵A和B可乘的条件是矩阵A的列数等于矩阵B的行数。若A是一个p*q矩阵,B是一个q*r矩阵,则其乘积C=AB是一个p*r矩阵。在上述计算C的标准算法中,主要计算量是3重循环,总共需要pqr次数乘。

矩阵连乘积的最优计算次序问题:对于给定的相继n个矩阵{A1,A2,…,An}(其中矩阵Ai的维数为p(i-1)*pi,i=1,2,…,n),如何确定计算矩阵连乘积A1A2…An的计算次序(完全加括号方式),使得依此次序计算矩阵连乘积需要的数乘次数最少。

穷举搜索法是最容易想到的方法。也就是列举出所有可能的计算次序,并计算出每一种计算次序相应需要的数乘次数,从中找出一种数乘次数最少的计算次序。这样做计算量太大。事实上,对于n个矩阵的连乘积,设其不同的计算次序为P(n)。由于可以先在第k个和第k+1个矩阵之间将原矩阵序列分为2个矩阵子序列,k=1,2,…,n-1;然后分别对这2个矩阵子序列完全加括号;最后对所得的结果加括号,得到原矩阵序列的一种完全加括号方式。由此,可以得到关于P(n)的递推式如下:

P(n) = 1 n=1
	(k=1~n-1)∑P(k)P(n-k) n>1

解此递归方程可得,P(n)实际上是Catalan数,即P(n)=C(n-1),其中,C(n)=1/(n+1)\*二项分布系数(2n n)=Ω(4^n/n^(3/2))

也就是说,P(n)是随n的增长呈指数增长的。因此,穷举搜索法不是一个有效的算法。

下面考虑用动态规划法解矩阵连乘积的最优计算次序问题。

1.分析最优解的结构

这个问题的一个关键特征设:计算A[1:n]的最优次序所包含的计算矩阵子链A[1:k]和A[k+1:n]的次序也是最优的。事实上,若有一个计算A[1:k]的次序需要的计算量更少,则用此次序替换原来计算A[1:k]的次序,得到的计算A[1:n]的计算量将比按最优次序计算所需计算量更少,这是个矛盾。(反证法)同理可知子链A[k+1:n]也满足

因此,矩阵连乘积计算次序问题的最优解包含着其子问题的最优解。这种性质称为最优子结构性质。问题的最优子结构性质是该问题可用动态规划算法求解的显著特征。

2.建立递归关系

设计动态规划算法的第二步是递归地定义最优值。对于矩阵连乘积的最优计算次序问题,设计算A[i:j],1<=i<=j<=n,所需的最少数乘次数为m[i][j],则原问题的最优值为m[1][n]。

当i=j时,可利用最优子结构性质计算m[i][j].事实上,若计算A[i:j]的最优次序在Ak和A(k+1)之间断开,i<=k<j,则m[i][j]+m[k+1][j]+p(i-1)*pk*pj。由于在计算时并不知道断开点k的位置,所以k还未定。不过k的位置只有j-i种可能,即k∈{i,i+1,…,j-1}。因此,k是这j-i个位置中使计算量达到最小的那个位置。

若将对应于m[i][j]的断开位置k记为s[i][j] ,在计算出最优值m[i][j]后,可递归地由s[i][j]构造出相应的最优解。

3. 计算最优值

public static void matrixChain(int []p, int [][]m, int [][]s)
{
	int n=p.length-1;
	for(int i=1;i<=n;i++) m[i][i]=0;
	for(int r=2;r<=n;r++)
		for(int i=1;i<=n-r+1;i++){
			int j=i+r-1;
			m[i][j]=m[i+1][j]+p[i-1]*p[k]*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;
				}
			}
		}
}

算法matrixChain的主要计算量取决于算法中对r,i和k的3重循环。循环体内的计算量为O(1),而3重循环的总次数为O(n3)。因此该算法的计算时间上界为O(n3)。算法所占用的空间显然为O(n2)。由此可见,动态规划法比穷举搜索法有效得多。

4. 构造最优解

动态规划算法的第四部是构造问题的最优解。算法matrixChain只是计算出了最优值,并未给出最优解。也就是说,通过算法matrixChain的计算,只知道最少乘次数,还不知道具体应按什么次序做矩阵乘法才能达到最少的数乘次数。

事实上,算法matrixChain已记录了构造最优解所需要的全部信息。

下面的算法traceback按算法matrixChain计算出的断点矩阵s指示的加括号方式输出计算A[i:j]的最优计算次序。

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);
}

3.2 动态规划算法的基本要素

从计算矩阵连乘积最优计算次序的动态规划算法可以看出,该算法的有效性依赖于问题本身所具有的两个重要性质:最优子结构性质和子问题重叠性质。从一般的意义上讲,问题所具有的这两个重要性质是该问题可用动态规划算法求解的基本要素。

下面着重研究动态规划算法的这两个基本要素以及动态规划法的变形——备忘录方法。

1. 最优子结构

设计动态规划算法的第一步通常是刻画最优解的结构。当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。问题的最优子结构性质提供了该问题可用动态规划算法求解的重要线索。

在动态规划算法中,利用问题的最优子结构性质,以自底向上的方式递归地从子问题的最优解逐步构造出整个问题的最优解。算法考查的子问题空间规模较小。

2. 重叠子问题

可用动态规划算法求解的问题应该具备的另一个基本要素是子问题的重叠性质。也就是说,在用递归算法自顶向下求解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。动态规划算法正式李永乐这种子问题的重叠性质,对每一个子问题都只解一次,而后将其解保存在一个表格中,当再次需要解此子问题时,只是简单地用常数时间查看一下结果。通常,不同的子问题个数随问题的大小呈多项式增长。因此,用动态规划算法通常只需要多项式时间,从而获得较高的解题效率。

3. 备忘录方法

备忘录方法是动态规划算法的变形。与动态规划算法一样,备忘录方法用表格保存已解决的子问题的答案,在下次需要解此子问题时,只要简单地查看该子问题的解答,而不必重新计算。

与动态规划算法不同的是,备忘录方法的递归方式是自顶向下的,而动态规划算法是自底向上递归的。因此,备忘录方法的控制结构与直接递归方法的控制结构相同,区别在于备忘录方法为每个解过的子问题建立了备忘录以备需要时查看,避免了相同子问题的重复求解。

备忘录方法为每个子问题建立一个记录项,初始化时,该记录项存入一个特殊值,表示该子问题尚未求解。在求解过程中,对每个待求子问题,首先查看其相应的记录项。若记录项中存储的是初始化时存入的特殊值,则表示该子问题是第一次遇到,此时计算出该子问题的解,并保存在其相应的记录项中,以备以后查看。若记录项中存储的已不是初始化时存入的特殊值,则表示该子问题已被计算过,其相应的记录项中存储的是该子问题的解答。此时,只要从记录项中取出该子问题的解答即可,而不必重新计算。

下面算法memorizedmatrixChain是解矩阵连乘积最优计算次序问题的备忘录方法:

public static int memorizedmatrixChain(int n)
{
	for(int i=1;i<=n;i++)
		for(int j=i;j<=n;j++)
			m[i][j]=0;
	return lookupChain(1,n);
}
private static int lookupChain(int i, int j)
{
	if(m[i][j]>0)return m[i][j];
	if(i==j)return 0;
	int u=lookupChain(i+1,j)+p[i-1]*p[i]*p[j];
	s[i][j]=i;
	for(int k=i+1;k<j;k++){
		int t=lookupChain(i,k)+lookupChain(k+1,j)+p[i-1]*p[k]*p[j];
		if(t<u){
			u=t;
			s[i][j]=k;}
	}
	m[i][j]=u;
	return u;
}

一般来讲,当一个问题的所有子问题都至少要解一次时,用动态规划算法比用备忘录方法好。此时,动态规划算法没有任何多余的计算。同时,对于许多问题,常可利用其规则的表格存取方式,减少动态规划算法的计算时间和空间需求。当子问题空间中的部分子问题可不必求解时,用备忘录方法则较有利,因为从其控制结构可以看出,该方法只解那些确实需要求解的子问题。

3.3 最长公共子序列

一个给定序列的子序列是在该序列中删去若干元素后得到的序列。确切地说,若给定序列X={x1,x2,…,xm},则另一序列Z={z1,z2,…,zk},X的子序列是指存在一个严格递增的下表序列{i1,i2,…,ik}使得对于所有j=1,2,…,k有zj=x(ij)。例如,序列Z={B,C,D,B}是序列X={A,B,C,B,D,A,B}的子序列,相应的递增下标序列为{2,3,5,7}。

给定两个序列X和Y,当另一序列Z既是X的子序列又是Y的子序列时,称Z是序列X和Y的公共子序列

最长公共子序列问题:给定两个序列X和Y,找出X和Y的最长公共子序列。

1. 最长公共子序列的结构

穷举搜索法是最容易想到的算法。对X的所有子序列,检查它是否也是Y的子序列,从而确定它是否为X和Y的公共子序列。而且在检查过程中记录最长的公共子序列,X的所有子序列都检查过后即可求出X和Y的最长公共子序列。X的每个子序列相应于下标集{1,2,…,m}的一个子集。因此,共有2^m个不同子序列,从而穷举搜索法需要指数时间。

事实上,最长公共子序列问题具有最优子结构性质。

设序列X={x1,x2,…,xm}和Y={y1,y2,…,yn}的最长公共子序列为Z={z1,z2,…,zk},则

(1)若xm=yn,则zk=xm=yn,且Zk-1是Xm-1和Yn-1的最长公共子序列;

(2)若xm≠yn且zk≠xm,则Z是Xm-1和Y的最长公共子序列;

(3)若xm≠yn且zk≠yn,则Z是X和Yn-1的最长公共子序列。

其中,Xm-1={x1,x2,…,xm-1};Yn-1={y1,y2,…,yn-1};Zk-1={z1,z2,…,zk-1}。

由此可见,两个序列的最长公共子序列包含了这两个序列的前缀的最长公共子序列。因此,最长公共子序列问题具有最优子结构性质。

2. 子问题的递归结构

由最长公共子序列问题的最优子结构性质可知,要找出X和Y的最长公共子序列,可按以下方式递归计算:

当xm=yn时,找出Xm-1和Yn-1的最长公共子序列,然后在其尾部加上xm(=yn)即可得X和Y的最长公共子序列。

当xm≠yn时,必须解两个子问题,即找出Xm-1和Y的最长公共子序列及X和Yn-1的一个最长公共子序列。这两个公共子序列较长者即为X和Y的最长公共子序列。

由此递归结构容易看到最长公共子序列问题具有子问题重叠性质。例如在计算X和Y的最长公共子序列时,可能要计算X和Yn-1及Xm-1和Y的最长公共子序列。而这两个子问题都包含一个公共子问题,即计算Xm-1和Yn-1的最长公共子序列。

首先建立子问题最优值的递归关系。用c[i][j]记录序列Xi和Yj的最大公共子序列的长度。当i=0或j=0时,空序列是Xi和Yj的最长公共子序列,故此时c[i][j]=0.在其他情况下,由最优子结构性质可建立递归关系如下:

c[i][j] = 0 i=0,j=0
	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

3. 计算最优值

直接利用递归式容易写出计算c[i][j]的递归算法,但其计算时间是随输入长度指数增长的。由于在所考虑的子问题空间中,总共有θ(mn)个不同的子问题,因此,用动态规划算法自底向上地计算最优值能提高算法的效率。

计算最长公共子序列长度的动态规划算法lcsLength以X和Y作为输入。输出两个数组c和b。其中c[i][j]存储Xi和Yj的最长公共子序列的长度,b[i][j]记录c[i][j]的值是由哪一个子问题的解得到的,这在构造最长公共子序列时要用到。问题的最优值,即X和Y的最长公共子序列的长度记录于c[m][n]中。

public static int lcsLength(char []x, char[]y, int [][]b)
{
	int m=x.length-1;
	int n=y.length-1;
	int [][]c=new int [m+1][n+1];
	for(int i=1;i<=m;i++)c[i][0]=0;
	for(int i=1;i<=n;i++)c[0][i]=0;
	for(int i=1;i<=m;i++)
		for(int j=1;j<=n;j++){
			if(x[i]==y[j]){
				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{
				c[i][j]=c[i][j-1];
				b[i][j]=3;
			}
		}
	return c[m][n];
}

由于每个数组单元的计算耗费O(1)时间,算法lcsLength耗时O(mn)。

4. 构造最长公共子序列

由算法lcsLength计算得到的数组b可用于快速构造序列X和Y的最长公共子序列。首先从b[m][n]开始,依其值在数组b中搜索。当b[i][j]=1时,表示Xi和Yj的最长公共子序列是由Xi-1和Yj-1的最长公共子序列在尾部加上xi所得到的子序列;当b[i][j]=2时,表示Xi和Yj的最长公共子序列与Xi-1和Yj的最长公共子序列相同;当b[i][j]=3时,表示Xi和Yj的最长公共子序列与Xi和Yj-1的最长公共子序列相同。

public static void lcs(int i, int j, char []x, int [][]b)
{
	if(i==0||j==0)return;
	if(b[i][j]==1){
		lcs(i-1,j-1,x,b);
		System.out.print(x[i]);
	}
	else if(b[i][j]==2)lcs(i-1,j,x,b);
		else lcs(i,j-1,x,b);
}

算法lcs中,每一次递归调用使i或j减1,因此算法的计算时间为O(m+n)。

5. 算法的改进

只需要计算最长公共子序列的长度,则算法的空间需求可大大减少。事实上,在计算c[i][j]时,只用到数组c的第i行和第i-1行。因此,用两行的数组空间就可以计算出最长公共子序列的长度。进一步的分析还可以将空间需求减至O(min{m,n})。

3.4 凸多边形最优三角剖分

凸多边形最优三角剖分的问题:给定凸多边形P={v0,v1,…,vn-1},以及定义在由多边形的边和弦组成的三角形上的权函数w。要求确定该凸多边形的三角剖分,使得该三角剖分所对应的权,即该三角剖分中诸三角形上权之和为最小。

可以定义三角形上各种各样的权函数w。例如,w(vi vj vk)=| vi vj | + | vj vk | + | vk vi |

其中,|vivj|是点vi到vj的欧氏距离。相应于此权函数的最优三角剖分即为最小弦长三角剖分。

1. 三角剖分的结构及其相关问题

凸多边形的三角剖分和表达式的完全加括号方式之间具有非常紧密的联系。正如所看到的,矩阵连乘积的最优计算次序问题等价于矩阵链的最优完全加括号方式。这些问题之间的相关性可从它们所对应的完全二叉树的同构性看出。

一个表达式的完全加括号方式相应于一棵完全二叉树,称为表达式的语法树。例如,完全加括号的矩阵连乘积(A1(A2A3)(A4(A5A6)))相应的语法树如图3-4(a)所示。

1555141709433

语法树每一个叶节点表示表达式中的一个原子。在语法树中,若一结点有一个表示表达式El的左子树,以及一个表示表达式Er的右子树,则以该结点为根的子树表示表达式(ElEr)。因此,有n个原子的完全加括号表达式对应于唯一的一棵有n个叶节点的语法树,反之亦然。

凸多边形{v0,v1,…,vn-1}的三角剖分也可以用语法树表示。例如,图3-4(a)中凸多边形的三角剖分可用3-4(b)所示语法树表示。该语法树的根结点为边v0v6。三角剖分中的弦组成其余的内结点。多边形中除v0v6边外的各边都是语法树的一个叶节点。树根v0v6是三角形v0v3v6的一条边。该三角形将原多边形分为三个部分:三角形v0v3v6,凸多边形{v0, v1, …, v3}和凸多边形{v3, v4, …, v6}。三角形v0v3v6的另外两条边,即弦v0v3和v3v6为根的两个儿子。以它们为根的子树表示凸多边形{v0, v1, …, v3}和凸多边形{v3, v4, …, v6}的三角剖分。

在一般情况下,凸n边形的三角剖分对应于一棵有n-1个叶结点的语法树产生相应的凸n边形的三角剖分。也就是说,凸n边形的三角剖分与有n-1个叶节点的语法树之间存在一一对应的关系。同样,n个矩阵的完全加括号乘积也与n个叶节点的语法树之间存在一一对应关系,所以,n个矩阵的完全加括号乘积和凸(n+1)边形的三角剖分之间也存在一一对应关系。矩阵连乘积A1A2…An中的每个矩阵Ai都对应凸(n+1)边形中的一条边vi-1vi。三角剖分中的一条弦vivj, i<j, 对应于矩阵连乘积A[i+1:j]。

事实上,矩阵连乘积的最优计算次序是凸多边形最优三角剖分问题的特殊情形。对于给定的矩阵链A1A2…An,定义与之相应的凸(n+1)边形P={v0,v1,…,vn-1},使得矩阵Ai与凸多边形中的边vi-1vi一一对应。若矩阵Ai的维数pi-1*pi,i=1,2,…,n, 则定义三角形vivjvk上的权函数值为:w(vivjvk)=pipjpk。依此权函数的定义,凸多边形P的最优三角剖分所对应的语法树给出矩阵链A1A2…An的最优完全加括号方式。

2. 最优子结构性质

凸多边形的最优三角剖分问题有最优子结构性质。

事实上,若凸(n+1)边形P={v0,v1,…,vn-1}的最优三角剖分T包含三角形v0vkvn,1<=k<=n-1,则T的权为三个部分的和:三角形v0vkvn的权,还有子多边形{v0, v1, …, vk}和{vk, vk+1, …, vn}的权之和。可以断言,由T所确定的这两个子多边形的三角剖分也是最优的。因为若有{v0, v1, …, vk}和{vk, vk+1, …, vn}的更小权的三角剖分将导致T不是最优三角剖分的矛盾。

3. 最优三角剖分的递归结构

首先,定义t[i][j],1<= i < j <=n为凸子多边形{vi, vi+1, …, vj}的最优三角剖分所对应的权函数值,即其最优值。为方便起见,设退化的多边形{vi-1, vi}具有权值0.据此定义,要计算的凸(n+1)边形P的最优权值为t[1][n]。

t[i][j]的值可以利用最优子结构性质递归地计算。由于退化的2顶点多边形的权值为0,所以t[i][i]=0,i=1, 2, …, n。当j-1>=1时,凸子多边形{vi-1, vi, …, vj}至少有3个顶点。由最优子结构性质,t[i][j]应为t[i][k]的值加上t[k+1][j]的值,再加上三角形vi-1vkvj的权值,其中i<=k<=j-1。由于计算时还不知道k的确切位置,而k的所有可能位置只有j-i个,因此,可以在这个j-i个位置中选出使t[i][j]值达到最小的位置,因此t[i][j]可以递归地定义为

t[i][j]=0 i=j
	min{t[i][k]+t[k+1][j]+w(vi vj vk)} i<j

4. 计算最优值

与矩阵连乘积中计算m[i][j]的递归式进行比较,容易看出,除了权函数的定义外,t[i][j]与m[i][j]的递归式完全一样。因此,只要对计算m[i][j]的算法matrixChain进行很小的修改就完全适用于计算t[i][j]。

下面描述的计算凸(n+1)边形P={v0,v1,…,vn}的最优三角剖分的动态规划算法minWeightTriangulation以凸多边形P={v0,v1,…,vn}的定义在三角形上的权函数w作为输入。

public static void minWeightTriangulation(int n, int[][] t, int[][] s)
{
    for(int i=1;i<=n;i++) t[i][i]=0;
    for(int r=2;r<=n;r++)
        for(int i=1;i<=n-r+1;i++){
            int j=i+r-1;
            t[i][j]=t[i+1][j]+w(i-1,i,j);
            s[i][j]=i;
            for(int k=i+1;k<i+r-1;k++){
                int u=t[i][k]+t[k+1][j]+w(i-1,k,j);
                if(u<t[i][j]){
                    t[i][j]=u;
                    s[i][j]=k;
                }
            }
        }
}

与算法matrixChain一样,算法minWeightTriangulation占用O(n2)空间,耗时O(n3)。

3.5 多边形游戏

多边形游戏是一个单人玩的游戏,开始时有一个由n个顶点构成的多边形。每个顶点被赋予一个整数值,每条边被赋予一个运算符+或*。所有边依次用整数从1到n编号。

游戏第一步,将一条边删除。随后n-1步按以下方式操作:

(1)选择一条边E以及由E连接着的两个顶点V1和V2

(2)用一个新的顶点取代边E以及由E连接着的两个顶点V1和V2。将由顶点V1和V2的整数值通过边E上的运算得到的结果赋予新顶点。

最后所有边都被删除,游戏结束。游戏的得分就是所剩顶点上的整数值。

问题:对于给定的多边形,计算最高得分。

该问题与上一节中讨论过的凸多边形最优三角剖分问题类似,但两者的最优子结构性质不同。多边形游戏问题的最优子结构性质更具有一般性。

1. 最优子结构性质

设所给的多边形的顶点和边的顺时针序列为op[1], v[1], op[2], v[2], …, op[n], v[n] 其中,op[i]表示第i条边所对应的运算符,v[i]表示第i个顶点上的数值,i=1~n。

在所给的多边形中,从顶点i(1<=i<=n)开始,长度为j(链中有j个顶点)的顺时针链p(i,j)可表示为 v[i], op[i+1], …, v[i+j-1]

如果这条链的最后一次合并运算在op[i+s]处发生(1<=s<=j-1),则可在op[i+s]处将链分割为两个子链p(i,s)和p(i+s,j-s)。

设m1是对子链p(i,s)的任意一种合并方式得到的值,而a和b分别是在所有可能的合并中得到的最小值和最大值。m2是p(i+s,j-s)的任意一种合并方式得到的值,而c和d分别是在所有可能的合并中得到的最小值和最大值。依此定义有a<=m1<=b,c<=m2<=d

由于子链p(i,s)和p(i+s,j-s)的合并方式决定了p(i,j)在op[i+s]处断开后的合并方式,在op[i+s]处合并后其值为m=(m1)op[i+s](m2)

(1)当op[i+s]=’+'时,显然有a+c<=m<=b+d

换句话说,由链p(i,j)合并的最优性可推出子链p(i,s)和p(i+s,j-s)的最优性,且最大值对应于子链的最大值,最小值对应于子链的最小值。

(2)当op[i+s]=’*'时,情况有所不同。由于v[i]可取负整数,子链的最大值相乘未必能得到主链的最大值。但是注意到最大值一定在边界点达到,即min{ac, ad, bc, bd}<= m <= max{ac,ad,bc,bd}

换句话说,主链的最大值和最小值可由子链的最大值和最小值得到。例如,当m=ac时,最大主链由它的两条最小子链组成;同理当m=bd时,最大主链由它的两条最大子链组成。无论哪种情形发生,由主链的最优性均可推出子链的最优性。

综上可知多边形游戏问题满足最优子结构性质。

2. 递归求解

由前面的分析可知,为了求链合并的最大值,必须同时求子链合并的最大值和最小值。因此,在整个计算过程中,应同时计算最大值和最小值。

设m[i,j,0]是链p(i,j)合并的最小值,而m[i,j,1]是最大值。若最优合并在op[i+s]处将p(i,j)分为两个长度小于j的子链p(i,i+s)和p(i+s,j-s),且从顶点i开始的长度小于j的子链的最大值和最小值均已计算出。为叙述方便,记

a=m[i,i+s,0]

b=m[i,i+s,1]

c=m[i+s,j-s,0]

d=m[i+s,j-s,1]

(1)当op[i+s]=’+'时,

m[i,j,0]=a+c

m[i,j,1]=b+d

(2)当op[i+s]=’*'时,

m[i,j,0]=min{ac,ad,bc,bd}

m[i,j,1]=max{ac,ad,bc,bd}

综合(1)和(2),将p(i,j)在op[i+s]处断开的最大值记为maxf(i,j,s),最小值记为minf(i,j,s),则

minf(i,j,s) = a+c op[i+s]=’+’

​ min{ac,ad,bc,bd} op[i+s]=’*’

maxf(i,j,s) = b+d op[i+s]=’+’

​ max{ac,ad,bc,bd} op[i+s]=‘*’

由于最优断开位置s有1<=s<=j-1的j-1种情况,由此可知

m[i,j,0]=min(1<=s<j){minf(i,j,s)} 1<=i,j<=n

m[i,j,1]=max(1<=s<j){maxf(i,j,s)} 1<=i,j<=n

初始边界值显然为

m[i,1,0] = v[i] 1<=i<=n

m[i,1,1] = v[i] 1<=i<=n

由于多边形是封闭的,在上面的计算中,当i+s>n时,顶点i+s实际编号为(i+s)mod n。按上述递推式计算出的m[i,n,1]即为游戏首次删去的第i条边后得到的最大得分。

3. 算法描述

基于以上讨论可设计解多边形游戏问题的动态规划算法如下:

private static void minMax(int i, int s, int j){
    int[] e = new int [5];
    int a = m[i][s][0],
    b=m[i][s][1],
    r=(i+s-1)%n+1,
    c=m[r][j-s][0],
    d=m[r][j-s][1];
    if(op[r]=='r'){
        minf=a+c;
        maxf=b+d;
    }
    else{
        e[1]=a*c;
        e[2]=a*d;
        e[3]=b*c;
        e[4]=b*d;
        minf=e[1];
        maxf=e[1];
        for(int k=2;k<5;k++){
            if(minf>e[k])minf=e[k];
            if(maxf<e[k])maxf=e[k];
        }
    }
}
public static int polyMax(){
    for(int j=2;j<=n;j++)
        for(i=1;i<=n;i++)
            for(int s=1;s<j;s++){
                minMax(i,s,j);
                if(m[i][j][0]>minf)m[i][j][0]=minf;
                if(m[i][j][1]<maxf)m[i][j][1]=maxf;
            }
    int temp=m[1][n][1];
    for(int i=2;i<=n;i++)
        if(temp<m[i][n][1])temp=m[i][n][1];
    return temp;
}

4. 计算复杂性分析

与凸多边形最优三角剖分问题类似,上述算法需要O(n3)的计算时间。

3.6 图像压缩

在计算机中常用像素点灰度值序列{p1, p2, …, pn}表示图像。其中,整数pi(1<=i<=n)表示像素点i的灰度值。通常灰度值的范围是0~255。因此,需要用8位表示一个像素。

图像的变位压缩格式将所给的像素点序列{p1, p2, …, pn}分割成m个连续段S1, S2, …, Sm。第i个像素段Si中(1<=i<=m),有l[i]个像素,且该段中每个像素都只用b[i]位表示。设t[i]=(k=1~i-1)∑l[k], l<=i<=m, 则第i个像素段Si

Si = {pt[i]+1, …, pt[i]+l[i]} l<=i<=m

设hi=⌈log(max(pk)+1)⌉ (t[i]+1<=k<=t[i]+l[i]), 则hi<=b[i]<=8。因此需要用3位表示b[i],l<=i<=m。如果限制l<=l[i]<=255,则需要用8位表示l[i],1<=i<=m。因此,第i个像素段所需的存储空间为l[i]*b[i]+11位.按此格式存储像素序列{p1, p2, …, pn},需要(i=1~m)∑l[i]*b[i]+11m位的存储空间。

图像压缩问题要求确定像素序列{p1, p2, …, pn}的最优分段,使得依此分段所需的存储空间最少。其中,0<=pi<=256,1<=i<=n.每个分段的长度不超过256位。

1. 最优子结构性质

设l[i],b[i],1<=i<=m是{p1, p2, …, pn}的最优分段。显而易见,l[1],b[1]是{p1, p2, …, pl[1]}的最优分段,且l[i],b[i],2<=i<=m是{pl[1]+1,…, pn}的最优分段。即图像压缩问题满足最优子结构性质。

2. 递归计算最优值

设s[i],1<=i<=n是像素序列{p1,…, ps}的最优分段所需的存储位数。由最优子结构性质易知

s[i] = min{s[i-k]+k*bmax(i-k+1,i)}+11 (1<=k<=min(i,256))

其中bmax(i,j)=⌈log(max{pk}+1)⌉ (i<=k<=j)。

据此可设计解图像压缩问题的动态规划算法如下:

static final int lmax = 256;
static final int header = 11;
static int m;

public static void compress(int p[], int s[], int l[], int b[]){
    int n=p.length-1;
    s[0]=0;
    for(int i=1;i<=n;i++){
        b[i]=length(p[i]);
        int bmax = b[i];
        s[i]=s[i-1]+bmax;
        l[i]=1;
        for(int j=2;j<=i&&j<=lmax;j++){
            if(bmax<b[i-j+1])bmax=b[i-j+1];
            if(s[i]>s[i-j]+j*bmax){
                s[i]=s[i-j]+j*bmax;
                l[i]=j;
            }
        }
        s[i]+=header;
    }
}

private static int length(int i){
    int k=1;
    i=i/2;
    while(i>0){
        k++;
        i=i/2;
    }
    return k;
}

3. 构造最优解

算法compress中用l[i]和b[i]记录了最优分段所需的信息。最优分段长度和像素位数分别存储于l[n]和b[n]中。其前一段的段长度和像素位数存储于l[n-l[n]]和b[n-l[n]]中。依次类推,由算法计算出的l和b可在O(n)时间内构造出相应的最优解。具体算法可实现如下:

private static void traceback(int n, int s[], int l[]){
    if(n==0)return;
    traceback(n-l[n],s,l);
    s[m++]=n-l[n];
}

public static void output(int s[], int l[], int b[]){
    int n=s.length - 1;
    System.out.println("The optimal value is "+s[n]);
    m=0;
    traceback(n,s,l);
    s[m]=n;
    System.out.println("Decomposed into"+m+"segments");
    for(int j=1;j<=m;j++){
        l[j]=l[s[j]];
        b[j]=b[s[j]];
    }
    for(int j=1;j<=m;j++)
        System.out.println(l[j]+"."+b[j]);
}

4. 计算复杂性

算法compress显然只需O(n)空间。由于算法compress中对j的循环次数不超过256,故对每一个确定的i,可在O(1)时间内完成min{s[i-j]+j*bmax(i-j+1,i)} (1<=j<=min(i,256))的计算。因此,整个算法所需的计算时间为O(n)。

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值