您的位置:首页 > 其它

动态规划之最大子段和

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

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

(一)最大子段和问题

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]
,其子矩阵 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
;
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;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: