动态规划之最大子段和
2014-04-16 19:45
274 查看
去年有想过将研究过的动态规划总结一下,不过那时没有写博客习惯,后来就不了了之了。这次不作大而无望的总结了,有一点说一点。
以下部分代码和分析出自《计算机算法设计与分析》(王晓东 编著)。
设所要求的序列为
, 记:
也就是说:
那么原问题转化为:
而数组b是可以递归表达的,如下:
或者表示为:
这样就很明显的显示出了最优子结构性质。
具体算法就不给了,上篇博客已经给出了。
等于左边部分的最大子段和
等于右边部分的最大子段和
最大子段横跨左右两个部分
前两种情况是递归求解的基础,对于第三种情况,首先以中间元素为基准,向左求出以中间元素为尾的最大子段和,向右求出以中间元素(或其紧挨着的右边的下一个元素)为首的最大子段和,两部分相加即横跨左右两部分的最大子段的和,跟前两种情况进行比较,取三者中最大的作为返回值,即得。不做具体分析了,给出代码如下:
算法的时间复杂度是
。
,
以及一个正整数m,求该序列中m个不相交的子段,使其总和最大。显然m<n。
表示数组 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)的值只与它左边的及上一行左边的元素有关,即图中划横线的元素。因此,显然可以通过记录一张这样的表格,然后从左上角往右下角计算,最终得到结果。典型的动态规划问题。
下面是书上给的参考代码,便于理解这个算法。
该算法的时间复杂度是
, 除了数组a所占用空间之外,额外的空间复杂度是
。
先来看看该算法的计算流程,一来加深对算法的理解,二来进行空间上的优化。
如图,是一个长度为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 (牛逼的数字)。
,时间复杂度
.
给定一个二维数组 a[m]
,其子矩阵 a[ i1, i2, j1, j2] 是指位于行 i1, j1 和列 i2, j2 之间的所有元素, 其和为该区域的所有元素的和。求最大子矩阵和即求所有这样的子矩阵中,元素和最大的一个。(注意,这里面跟最长公共子序列问题不同的地方在于,要求子矩阵中的所有元素在原矩阵中也是连续的。)
显然一个矩阵的子矩阵有
个, 枚举所有的子矩阵是不现实的做法。由于我们已经有一位数组的最大子段和的解决办法,其时间复杂度是
,故而可以考虑将二维数组降维成
个一位数组,然后对每个一维数组执行最大子段和算法,最后,在所有的结果中选取和最大的一个,这样就能得到原问题的解,而且算法的时间复杂度为:
。 这个相较原来的穷举法已经降了一个幂数。
举个例子,如原始矩阵为:
对其进行降维操作,得到 10 个一维的数组,如下:
其中, aj 表示原数组的第j行。针对上面每一行数组求最大子序列和,取最大值即得所求。由于每次只需要求一行数组,所以并不需要二维数组来存储中间结果,故而算法最终的时间复杂度是
,空间复杂度是
(不包括原始矩阵的大小)。
下面是HDU 1081 该问题的AC代码:
以下部分代码和分析出自《计算机算法设计与分析》(王晓东 编著)。
(一)最大子段和问题
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; }
相关文章推荐
- 动态规划之最大子段和问题
- 动态规划一:最大子段和
- 动态规划----最大子段和
- 最大子段和问题的动态规划解法
- 动态规划:最大子段和
- 动态规划入门 P1115 最大子段和(链状)
- 动态规划 - 最大子段和
- HDU 1003 求最大子段和的动态规划
- 动态规划 - 最大子段和
- 最大子段和O(n)算法;不是动态规划;思路挺独特;
- 动态规划入门 TYVJ 1305 最大子段和(环状)
- 动态规划求最大子段和
- 动态规划入门之最大M子段和
- 动态规划1:最大子段和问题到最大子矩阵问题(一):最大子段和问题详谈
- 动态规划求最大子段和
- 阿里云笔试题:最大子段和问题的动态规划解法
- 动态规划-循环数组最大子段和
- js算法:动态规划-最大公共子串与最大子段和
- 动态规划(DP),递推,最大子段和,POJ(2479,2593)
- 动态规划3:最大子段和问题到最大子矩阵问题(三):初探最大子矩阵之和问题