动态规划之最大子段和

去年有想过将研究过的动态规划总结一下,不过那时没有写博客习惯,后来就不了了之了。这次不作大而无望的总结了,有一点说一点。

以下部分代码和分析出自《计算机算法设计与分析》(王晓东 编著)。

(一)最大子段和问题

1、一般理论

最大子段和问题复杂度为O(n)的解法,在上篇博客最大连续子序列中已经谈过了。这里在稍微提及一下,主要是更形式化的说明为什么是用动态规划。然后谈一下这个问题的分治解法。

设所要求的序列为 , 记:

                                          

也就是说:


那么原问题转化为:


而数组b是可以递归表达的,如下:



或者表示为:



这样就很明显的显示出了最优子结构性质。

具体算法就不给了,上篇博客已经给出了。


2、分治法

分治法的思路是将原数组等分成两个数组,分别在两个数组中求最大子段和。则最终的最大子段和有三种情况:

  • 等于左边部分的最大子段和
  • 等于右边部分的最大子段和
  • 最大子段横跨左右两个部分
前两种情况是递归求解的基础,对于第三种情况,首先以中间元素为基准,向左求出以中间元素为尾的最大子段和,向右求出以中间元素(或其紧挨着的右边的下一个元素)为首的最大子段和,两部分相加即横跨左右两部分的最大子段的和,跟前两种情况进行比较,取三者中最大的作为返回值,即得。不做具体分析了,给出代码如下:

int MaxSubSum(int *a, int left, int right)
{
	int sum = 0;
	if(left == right) sum = a[left]>0?a[left]:0;
	else
	{
		int center = (left+right)/2;
		int leftsum = MaxSubSum(a, left, center);
		int rightsum = MaxSubSum(a, center+1, right);
		int s1 = 0;
		int lefts = 0;
		for(int i = center ; i >= left ; i--)
		{
			lefts+=a[i];
			if(lefts > s1) s1 = lefts;
		}
		int s2 =0, rights =0;
		for(int i = center + 1 ; i <= right; i++)
		{
			rights += a[i];
			if(rights > s2) s2 = rights;
		}
		sum = s1+s2;
		sum = Max(sum, leftsum, rightsum);
	
	}
	return sum;
}

算法的时间复杂度是


(二)最大M子段和

最大M子段和是最大子段和问题的推广,给定n个正数(可能为负,因此结果也可能是负的,这点与上篇当结果为负时输出0还是有区别的)组成的序列  , 以及一个正整数m,求该序列中m个不相交的子段,使其总和最大。显然m<n。

1、分析

首先要定义转移方程。设  表示数组 a 的前 j 项中 i 个子段和的最大值, 且第 i 个子段包含 . 那么所求的最优值就变成了  (注意 j 的取值范围)。

接下来就要获得b的递归形式了。按照组合数学中经常用来分析序列的方式,将 的第 i 个子段的形成分成两种情况:一种是只包含  的,一种是不只包含  的。 对于后者而言, 是  与  的和,因为只有这样才能保证其第 i 子段以  结尾 而不仅仅只有一个 , 同时,  已经确定了 i 个以 a[j-1] 结尾的子段,所以不需要增加子段的个数,只需要将 加到已有子段上即可。 对于前者, 已经确定了  的 i 个子段中的 第 i 个子段,需要在前 j-1 个元素中再确定 i-1个子段,这 i-1 个子段可以是以任何元素(下标小于j)结尾的,但要求是其中和最大的。综上,可以得b的递归形式如下:



(注意i,j的取值范围,1 <= i <= m, i <= j <= n)

用数组显示如下:


b(i,j)的值只与它左边的及上一行左边的元素有关,即图中划横线的元素。因此,显然可以通过记录一张这样的表格,然后从左上角往右下角计算,最终得到结果。典型的动态规划问题。
下面是书上给的参考代码,便于理解这个算法。
int a[MaxN], b[MaxM][MaxN];
int MaxSum(int m, int n )
{
	for(int i = 0 ; i <= m ; i++) b[i][0] = 0;
	for(int j = 1 ; j <= n ; j++) b[0][j] = 0;
	for(int i = 1 ; i <= m ; i++)
	{
		for(int j = i ;  j <= n - m + i ; j++)
		{
			if(j>i)//取其上一行从i-1到j-1的最大元素与a[j]相加
			{
				.....//省略
			}
			else
			{
				b[i][j] = b[i-1][j-1]+a[j];
			}
		}
	
	}
	int sum = 0;
	for(int j = m ; j <= n ; j++)
		if(sum < b[m][j]) sum = b[m][j];
	return sum;
}

该算法的时间复杂度是 , 除了数组a所占用空间之外,额外的空间复杂度是

2、优化

上例b数组占用空间巨大,其实是可以优化的,因为每次计算一行的时候,只需要上一行参与运算就可以了,所以事实上只需要保留数组b的一行。同时因为需要额外的跟数组b长度相同的一个数组来保留每一行从1到 j 的最大值,所以需要两个一维数组。书上给出的例子就是这样实现的,但其实在空间上还是可以再优化的。
先来看看该算法的计算流程,一来加深对算法的理解,二来进行空间上的优化。




如图,是一个长度为6的序列的最大2子段和。观察颜色较重的元素2(3类似,不过3后面没有元素,故不用来讨论),可以发现元素2对于计算下面一行的任何一个数字均没有帮助,因为元素2在序列1到3之间位于当前序列最大元素后面。但元素2又是不能不保留的,因为它对于计算该元素后面的5是有帮助的。而这样的元素每回合只有一个,也就是说,可以将元素2这样的元素替换成当前数组里的最大值,然后只用一个额外的变量来保留元素2这样的元素,以便于参与下一个元素的计算。为了便于理解,我们可以将上图中每一行的元素都移动到最左边,如下:




还是上面的数列,不过数组b的定义只有 n-m+1个元素,然后第一遍循环的时候,数组b的每个元素都是从 1 到当前位置的最大值。这样就不需要另外开辟一个一维数组来保存数组b的最大值了,而且数组b的大小也降了下来。第四行的+表示加上相应的a[i],i值怎么确定,见代码。
下面是我的AC的代码,题目编号是 1024 (牛逼的数字)。
//max m sum
//dynamic programming
//hdj 1024
//625MS	1852K	1052 B
#include <iostream>
#define MAXN 1000001
using namespace std;
typedef long long ULONG;
ULONG a[MAXN];

ULONG MaxSum(long n, long m)
{
    ULONG* b = new ULONG[n-m+2];
    long i ;
    for( i = 1 ; i <= n-m+1 ; i++) b[i] = 0;
    for( i = 1 ; i <= m ; i++ )
    {
       b[1] = b[1] + a[i];
       ULONG max = b[1];
       long j;
       ULONG bleft = b[1];
       for( j = 2 ; j <= n-m+1; j++ )
       {
           ULONG tmp1 = b[j] + a[i+j-1];//上一排至此为止的最大元素 
           ULONG tmp2 = bleft + a[i+j-1];//该元素左边的元素 
           bleft = tmp1>tmp2?tmp1:tmp2; 
           if( bleft > max )   max = bleft;
           b[j] = max;
       }     
    }   
    ULONG rt =   b[n-m+1];
    delete [] b;
    return rt;   
}

int main()
{
    long n,m;
    while( cin>>m>>n )
    {
        long i;
        for( i = 1 ; i <= n ; i++ ) cin>>a[i];
        cout<<MaxSum(n, m)<<endl;       
    }
    return 0;
}
除了数组a之外,算法的空间复杂度是 ,时间复杂度 .

(三)最大子矩阵和

最大子矩阵和是最大子段和问题的另一个扩展。先来看看问题描述:
       给定一个二维数组 a[m][n],其子矩阵 a[ i1, i2, j1, j2] 是指位于行 i1, j1 和列 i2, j2 之间的所有元素, 其和为该区域的所有元素的和。求最大子矩阵和即求所有这样的子矩阵中,元素和最大的一个。(注意,这里面跟最长公共子序列问题不同的地方在于,要求子矩阵中的所有元素在原矩阵中也是连续的。)
显然一个矩阵的子矩阵有 个, 枚举所有的子矩阵是不现实的做法。由于我们已经有一位数组的最大子段和的解决办法,其时间复杂度是,故而可以考虑将二维数组降维成   个一位数组,然后对每个一维数组执行最大子段和算法,最后,在所有的结果中选取和最大的一个,这样就能得到原问题的解,而且算法的时间复杂度为:。 这个相较原来的穷举法已经降了一个幂数。
       举个例子,如原始矩阵为:
            

对其进行降维操作,得到  10 个一维的数组,如下:

其中, aj 表示原数组的第j行。针对上面每一行数组求最大子序列和,取最大值即得所求。由于每次只需要求一行数组,所以并不需要二维数组来存储中间结果,故而算法最终的时间复杂度是,空间复杂度是  (不包括原始矩阵的大小)。

下面是HDU 1081 该问题的AC代码:
//The Max Sub Matrix Sum
//dynamic programming
//hdu 1081
//0MS	336K	1055 B
#include <iostream>
#include <string>
#define MAXN 101

using namespace std;

int a[MAXN][MAXN];

long MaxSumSubArray(int* aa, int n)
{
   long sum = 0, b = 0;
   for(int i = 0; i < n ; i++)
   {
        if(b>0) b+=aa[i];
        else b=aa[i];
        if(b>sum) sum = b;
   }
   return sum;          
}

long MaxSumSubMatrix(int m, int n)
{
    long sum = 0;
    int *b = new int[n];
    for(int i = 0 ; i < m ; i++)
    {
            for(int k = 0 ; k < n ; k++) b[k] = 0;
            for(int j = i ; j < m ; j++)
            {
                    for(int k = 0 ; k < n; k++) b[k]+=a[j][k];
                    long max = MaxSumSubArray(b, n);
                    if(max > sum) sum = max;
            }
    }   
    return sum;
}

int main()
{
    int n;
    while(cin>>n)
    {
       for(int i = 0 ; i < n ; i++)
         for(int j = 0 ; j < n ; j++)
           cin>>a[i][j];
       cout<<MaxSumSubMatrix(n, n)<<endl;                 
    }
    return 0;
}



阅读更多

没有更多推荐了,返回首页