动态规划算法下的序列问题:最长公共子序列问题和最大子段和问题

本篇主要介绍最长公共子序列问题和最大子段和问题

1、最长公共子序列问题

什么是最长公共子序列

给定一个序列X=<x1,x2,x3,x4…,xm>,另一个序列Z=<z1,z2,z3,z4…,zk>,若存在一个严格递增的X的下标序列<i1,i2,i3,…,ik>对所有的1,2,3,…,k,都满足x(ik)=zk,则称Z是X的子序列

比如说:比如Z=<B,C,D,B>是X=<A,B,C,B,D,A,B>的子序列

如果Z既是X的子序列,又是Y的子序列,则称Z为X和Y的公共子序列

比如说
X:A B C B D A B
Y:B D C A B A

形如这样的就是最长公共子序列问题

那我们接下来用动态规划算法解最长公共子序列问题

用动态规划算法解最长公共子序列问题


我们可以把x1,x2,x3,…,xi和y1,y2,y3,…,yi共同组合一个子问题

然后我们看一下子问题间的依赖关系
假设大长方形框是一个原问题,左边是x序列,上边是y序列
若xm=yn,我们可以把原问题归结成xm-1和yn-1组合的子问题。

若xm!=yn,我们可以把原问题归结成xm-1和yn组合的子问题

或者归结成xm和yn-1组合的子问题

我们看到啊,在xi和yi上,如果能找到一个最长的公共子序列,xi+1和yi+1就可以在它的基础上继续寻找。对于原问题而言,子问题的最优解是包含在了原问题当中。因此这种算法是满足依赖关系。

接下来我们来看下递推方程
令C[i,j]:xi与yi的公共子序列长度
如果xi=yi,那么C[i,j]=C[i-1,j-1]+1
如果x1!=y1,那么我们就在C[i,j-1]和C[i-1,j]中找到最大值赋给C[i,j]
当然若i=0或j=0,C[i,j]=0。因为此时有一个序列没有长度,所以就没有公共子序列

下面来看一下追踪解:
首先我们先考虑标记函数
设B[i,j]是一个标记函数
我们需要从c[0][0]开始填表,填到c[m-1][n-1],所得到的c[m-1][n-1]就是LCS的长度
在对应字符相等的时候,用↖标记

在p1 < p2的时候,用←标记

在p1 >= p2的时候,用↑标记

我们看一个例子:
比如说求ABCBDAB和BDCABA的最长公共子序列长度:

我们分析下。
首先C[0,j]和C[i,0]=0
xi和yj进行比较,如果xi=yj。那么C[i,j]=C[i-1,j-1]+1,我们看一下C[1,4]=C[0,3]+1,然后标记斜向上的箭头
x1和y1进行比较,因为C[i-1,j]>=C[i,j-1],则让C[i,j]=C[i-1,j],然后标记向上的箭头
x1和y5进行比较,因为C[i,j-1]>C[i-1,j],则让C[i,j]=C[i,j-1]=1,此时标记向左的箭头
循环往复
i=0,i<字符串1的长度,i++
j=0,j<字符串2的长度,j++
在这个循环遵照上边的规则把表填出来就可以了

这个标记函数的指向就是怎么推出来的这个值

对于追踪解的过程,就是从最后一个值自底向上开始追踪,按照箭头的走向开始求解。如果遇到斜向上的箭头,那就说明找到了一个公共子序列。一直到0为止,图中画圆圈的就是问题的解即:BCBA。当然我们也要知道,这个解不是唯一的,我们能看到BCAB也是一个解。所以我们只需要找到一个最长子序列即可。

对于求解过程啊,因为原问题是要划成子问题来解的。那么原问题是需要依赖子问题的解,也就是说通过子问题的解和递推方程可以计算出原问题的解。所以解的箭头指向就是指向上一个子问题的最优解。因此这个方法是有效的。

通过分析,我们直接上代码:

public static void main(String[] args) {
		// TODO Auto-generated method stub
		String str1="ABCBDAB";
		String str2="BDCABA";
		int B[][]=new int[str1.length()+1][str2.length()+1];
		int temp[][]=LcsLength(str1, str2, B);
		System.out.println("最长公共子序列的个数是:"+temp[str1.length()][str2.length()]);
		
		System.out.print("最长公共子序列是:");
		Tracingsolution(str1.length(), str2.length(), B, str1);
		
	}

	
	public static int[][] LcsLength(String str1,String str2,int B[][]) {
		int [][]C=new int[str1.length()+1][str2.length()+1];
		for(int i=0;i<str1.length();i++) {
			for(int j=0;j<str2.length();j++) {
				if(str1.charAt(i)==str2.charAt(j)) {
					C[i+1][j+1]=C[i][j]+1;
					B[i+1][j+1]=0;  //0表示斜向上
				}
				
				else { 
					if(C[i][j+1]>=C[i+1][j]) {
					C[i+1][j+1]=C[i][j+1];  //将最大的值存放在C中
					B[i+1][j+1]=1;  //1表示竖直方向
				}
					
					else {
						C[i+1][j+1]=C[i+1][j];
						B[i+1][j+1]=-1;  //-1表示向左
					}
			}
			}
		}
		
		return C;
	}
	
	//递归追踪解
	public static void Tracingsolution(int a,int b,int B[][],String str1) {
		if(a==0||b==0) 
			return;
		
		//箭头斜向上
		if(B[a][b]==0) {
			Tracingsolution(a-1, b-1, B, str1);
			System.out.print(str1.charAt(a-1));
		}
		
		//箭头向上
		else if(B[a][b]==1)
			Tracingsolution(a-1, b, B, str1);
		
		//箭头向左
		else
			Tracingsolution(a, b-1, B, str1);
		
	}

下面,我们来进行时间复杂度分析:

动态规划算法来解最长公共子序列问题分析

我们看到动态规划算法里面是两层for循环的
那么时间复杂度就是O(mn)
追踪解的代码是逐步缩小的过程,要么是m-1,要么是n-1或者是m-1和n-1同时执行。那么在最坏情况下的时间复杂度就是O(m+n)。最后我们能得出时间复杂度就是O(mn)

而空间复杂度是一个备忘录的表格。所以是O(mn)

这就是动态规划解最长公共子序列问题的内容

2、最大子段和问题

什么是最大子段和问题

给定n个数(可以是负数)的序列(a1,a2,…,an)
求在这个序列中,某一连续段中的最大和

例如:(-2,11,-4,13,-5,-2)
则最大子段和就是a2+a3+a4=20

我们可以考虑用蛮力法来解

蛮力法解最大子段和问题

我们可以求这个序列的子集(连续段),然后对这个子集进行求和。直至找到最大值
譬如说:a1,a1+a2,a1+a2+a3,a2,a3…

我们对蛮力法进行代码分析:
用锁定范围来求解
让i=0,i<n;i++
j=i,j<n,j++
我们令k在i和j之间游走。即,k=i,k<=j,k++
遍寻max(sum+a[k])的值。如果我们找到了最大值,我们就用m,n记录下。m=i,n=j。然后
最大值,m,n就是问题的解
好,我们来上代码:

    public static int m;  //记最大子段和的初始位置
	public static int n;  //记最大子段和的末尾位置
    public static void main(String[] args) {
		// TODO Auto-generated method stub
		int a[]=new int[] {-2,11,-4,13,-5,-2};
		int maxsum=MaxsubsegmentsMax(a);
		Tracingsolution(a, maxsum);
		
		System.out.print("最大子段和是:");
		for(int i=m;i<=n;i++)
		{
			if(i!=n)
			System.out.print("a"+i+"+");
			else 
				System.out.print("a"+i);
		}
		System.out.print("="+maxsum);
	}
	
	//蛮力法解最大子段和
	public static int MaxsubsegmentsMax(int a[]) {
		int maxsum=-0x3f3f3f3f,tempsum=0;
		for(int i=0;i<a.length;i++)  //子序列初始位置
			for(int j=i;j<a.length;j++) {  //子序列末尾位置
				tempsum=0;
				for(int k=i;k<=j;k++) 
					tempsum+=a[k];
				if(tempsum>maxsum)
				{
					maxsum=tempsum;
					m=i+1;
					n=j+1;
				}
			}
		return maxsum;
	}
蛮力法解最大子段和问题的分析

我们看到,这是有三层循环的,所以蛮力法的时间复杂度就是O(n^3)

当然我们也可以用分治法来解最大子段和问题

分治法解最大子段和问题

我们可以在中间点将序列一分为二,通过递归将左右序列求解。对于中间部分。一定记住是从中间往两遍算。因为是要以中间为基准向两遍计算。从而得到跨边界的最大值。不然就会变成零零散散的区域,再合成就不是中间跨边界的值了。最后将左半部分的和,右半部分的和,跨边界的和,三者进行比较找到最大的和。

对于解的追踪,我们是要分开考虑的,跨边界的问题中记下最大和的时候左右的下标,而在左半部分和右半部分中,left和right的跨度就是最大和的左右下标。
然后我们要在三者之间进行比较。将对应的情况赋给m和n。

具体代码如下:

public static int m;  //记最大子段和的初始位置
	public static int n;  //记最大子段和的末尾位置
	public static int max=-0x3f3f3f3f;  //递归法求解最大值
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int a[]=new int[] {-2,11,-4,13,-5,-2};
		int maxsum=MaxsubsegmentsMax2(a, 0, a.length-1);
		Tracingsolution(a, maxsum);
		
		System.out.print("最大子段和是:");
		for(int i=m;i<=n;i++)
		{
			if(i!=n)
			System.out.print("a"+i+"+");
			else 
				System.out.print("a"+i);
		}
		System.out.print("="+maxsum);
	}

//递归解最大子段和
	public static int MaxsubsegmentsMax2(int a[],int left,int right) {
		int x = 0,y = 0;
		if(left==right)
			return a[left];
		int mid=(left+right)>>1;
		int leftMaxSum=MaxsubsegmentsMax2(a,left,mid);  //划成左半部部分
		int rightMaxSum=MaxsubsegmentsMax2(a, mid+1, right);  //划成右半部部分
		
		//处理跨边界问题
		int leftmax=-0x3f3f3f3f,rightmax=-0x3f3f3f3f,tempmax=0;
		//计算中间向左部分的最大值
		//一定记住是从中间往两遍算。因为是要以中间为基准向两遍计算。从而得到跨边界的最大值。不然就会变成零零散散的区域,再合成就不是中间跨边界的值了
		for(int i=mid;i>=left;i--) {
			tempmax=a[i]+tempmax;
			if(tempmax>leftmax) {
				leftmax=tempmax;
				x=i;
			}
		}
		tempmax=0;
		//计算中间向右部分的最大值
		for(int i=mid+1;i<=right;i++) {
			tempmax=a[i]+tempmax;
			if(tempmax>rightmax) {
				rightmax=tempmax;
				y=i;
			}
		}
		
		//将跨边界的左右值加起来
		int midMaxSum=leftmax+rightmax;
		//将左半部分,中间边界,右半部分中挑选一个最大值
		if(leftMaxSum>Math.max(midMaxSum, rightMaxSum)) {
			max=leftMaxSum;
			m=left+1;
			n=right+1;
		}
		else if (midMaxSum > Math.max(leftMaxSum, rightMaxSum)) {
	        max = midMaxSum;
	        m=x+1;
	        n=y+1;
	    }
		else {
	        max = rightMaxSum;
	        m=left+1;
			n=right+1;
	    }
	    return max;
	}

下面我们来分析时间复杂度

分治法解最大子段和问题的分析

我们想一下,从中间往左算最大和的时候,是不是A[k],A[k]+A[k-1],A[k]+A[k-1]+A[k-2],…,A[k]+A[k-1]+A[k-2]+…+A[1]
因此我们就能分析出,时间复杂度就是左半部分的时间复杂度就是O(n),同理右半部分的时间复杂度也是O(n)。所以左右一加还是O(n)
因此我们列出递推方程:
T(n)=2T(n/2)+O(n)
根据主定理得出
T(n)=O(nlogn)
分析得出分治法比蛮力法降到了O(nlogn)量级的时间复杂度

下面考虑动态规划解最大子段和问题

动态规划解最大子段和问题



我们这么分析,动态规划算法可以进行试探算法。
我们可以i=0,i<n;i++
tempsum+a[i]如果大于此时的a[i]。那我们就加,否则就tempsum=a[i]。一轮循环之后让tempsum和sum比较。

这个是将前i个最优解和第i个值进行比较。如果C[i-1]+A[i]大,就进行计算,否则就是A[i]的值。这样总是能获得一个最大的C[i]。然后再用最优解去和下一个子问题进行计算。因此是满足依赖关系的。

至于追踪解,我们通过比较是只能获取最大和的那个末位置的边界c。要自底向上的去求初位置的边界。怎么求呢?
将计算好的最大值max,以此减去A[c],得到的新max然后判断是否为0且max!=A[- -c]。如果是这样就结束循环,这个c就是初位置下标。否则继续计算直到条件满足。最后就得到初位置下标

好,下面直接上代码

    public static int m;  //记最大子段和的初始位置
	public static int n;  //记最大子段和的末尾位置
    public static void main(String[] args) {
		// TODO Auto-generated method stub
		int a[]=new int[] {-2,11,-4,13,-5,-2};
		int maxsum=MaxsubsegmentsMax3(a);
		Tracingsolution(a, maxsum);
		
		System.out.print("最大子段和是:");
		for(int i=m;i<=n;i++)
		{
			if(i!=n)
			System.out.print("a"+i+"+");
			else 
				System.out.print("a"+i);
		}
		System.out.print("="+maxsum);
	}

//用动态规划解最大子段和
	public static int MaxsubsegmentsMax3(int a[]) {
		int tempsum=0,maxsum=-0x3f3f3f3f;
		for(int i=0;i<a.length;i++) {
			if(tempsum+a[i]>a[i])
				tempsum=tempsum+a[i];
			else
				tempsum=a[i];
			
			if(tempsum>maxsum) {
				maxsum=tempsum;
				n=i+1;
			}
		}
		
		return maxsum;
	}
	
	//追踪解
	public static void Tracingsolution(int a[],int maxsum) {
		int k=n-1;
		while(maxsum!=0||maxsum==a[k]) {
		maxsum=maxsum-a[k];
		k--;
		}
		m=k+2;
	}

下面我们进行时间复杂度分析

动态规划算法来解最大子段和问题分析

我们看到动态规划算法里面只有一层循环,因此是O(n)
而对于追踪解,最坏时间复杂度也是O(n)
所以动态规划算法来解最大子段和问题的时间复杂度就是O(n)

而空间复杂度我们自始至终就只用一个数组,因此空间复杂度就是O(n)

以上就是最大子段和问题的内容

######            物情无巨细,自适固其常。 (杜甫《夏夜叹》)

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
最长公共子序列问题最大子段和问题都是动态规划问题。 1. 最长公共子序列问题 最长公共子序列问题(Longest Common Subsequence, LCS)是指给定两个序列X和Y,找出它们的最长公共子序列动态规划算法实现: 设X={x1,x2,…,xm}和Y={y1,y2,…,yn},令c[i][j]表示X的前i个字符和Y的前j个字符的LCS长度,b[i][j]记录c[i][j]是由哪一个子问题的解得到的。 则有以下的状态转移方程: c[i][j] = 0, i=0 或 j=0 c[i][j] = c[i-1][j-1]+1, i,j>0 且 xi=yj c[i][j] = max(c[i-1][j], c[i][j-1]), i,j>0 且 xi≠yj 实现代码如下: ```python def LCS(X, Y): m, n = len(X), len(Y) c = [[0]*(n+1) for i in range(m+1)] b = [[0]*(n+1) for i in range(m+1)] for i in range(1, m+1): for j in range(1, n+1): if X[i-1] == Y[j-1]: c[i][j] = c[i-1][j-1] + 1 b[i][j] = '↖' elif c[i-1][j] >= c[i][j-1]: c[i][j] = c[i-1][j] b[i][j] = '↑' else: c[i][j] = c[i][j-1] b[i][j] = '←' return c, b def printLCS(b, X, i, j): if i == 0 or j == 0: return if b[i][j] == '↖': printLCS(b, X, i-1, j-1) print(X[i-1], end='') elif b[i][j] == '↑': printLCS(b, X, i-1, j) else: printLCS(b, X, i, j-1) ``` 效率分析: 设X和Y的长度分别为m和n,时间复杂度为O(mn),空间复杂度也为O(mn)。 2. 最大子段和问题 最大子段和问题(Maximum Subarray Sum)是指在一个数列中找到一个连续的子序列,使得该子序列的和最大。 动态规划算法实现: 设a[1..n]为输入的数组,sum[i]表示以a[i]为结尾的最大子段和,则有以下的状态转移方程: sum[1] = a[1] sum[i] = max(sum[i-1]+a[i], a[i]), i>1 实现代码如下: ```python def maxSubarray(a): n = len(a) dp = [0] * n dp[0] = a[0] for i in range(1, n): dp[i] = max(dp[i-1] + a[i], a[i]) return max(dp) ``` 效率分析: 设a的长度为n,时间复杂度为O(n),空间复杂度为O(n)。 综上所述,最长公共子序列问题最大子段和问题动态规划算法都是时间复杂度为O(n^2),空间复杂度为O(n^2)的算法,但是两者的状态转移方程和实现方式不同。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值