(DP)最大子段和问题分析和总结(…

来源:互联网 发布:手机怎样看淘宝的积分 编辑:程序博客网 时间:2024/05/01 16:45

转载自http://www.cnblogs.com/shihao/archive/2012/01/18/2325562.html

最大子段和问题分析和总结

最大子段和问题(Maximum IntervalSum) 经典的动态规划问题,几乎所有的算法教材都会提到.本文将分析最大子段和问题的几种不同效率的解法,以及最大子段和问题的扩展和运用.

一.问题描述

给定长度为n的整数序列,a[1...n], 求[1,n]某个子区间[i,j]使得a[i]+…+a[j]和最大.或者求出最大的这个和.例如(-2,11,-4,13,-5,2)的最大子段和为20,所求子区间为[2,4].

二. 问题分析

1.穷举法

穷举应当是每个人都要学会的一种方式,这里实际上是要穷举所有的[1,n]之间的区间,所以我们用两重循环,可以很轻易地做到遍历所有子区间,一个表示起始位置,一个表示终点位置.代码如下:

?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
int start =0;//起始位置
int end =0; //结束位置
int max =0;
for(int i = 1;i <= n; ++i)
{
for(int j = i;j <= n;++j)
{
int sum =0;
for(int k = i;k <=j; ++k)
sum += a[k];
if(sum> max)
{
start = i;
end = j;
max = sum;
}
}
}

这个算法是几乎所有人都能想到的,它所需要的计算时间是O(n^3).当然,这个代码还可以做点优化,实际上我们并不需要每次都重新从起始位置求和加到终点位置.可以充分利用之前的计算结果.

或者我们换一种穷举思路,对于起点i,我们遍历所有长度为1,2,…,n-i+1的子区间和,以求得和最大的一个.这样也遍历了所有的起点的不同长度的子区间,同时,对于相同起点的不同长度的子区间,可以利用前面的计算结果来计算后面的.

比如,i为起点长度为2的子区间和就等于长度为1的子区间的和+a[i+1]即可,这样就省掉了一个循环,计算时间复杂度减少到了O(n^2).代码如下:

?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
int start =0;//起始位置
int end =0;//结束位置
int max =0;
for(int i = 1;i <= n; ++i)
{
int sum =0;
for(int j = i;j <= n;++j)
{
sum += a[j];
if(sum> max)
{
start = i;
end = j;
max = sum;
}
}
}

2.分治法

求子区间及最大和,从结构上是非常适合分治法的,因为所有子区间[start, end]只可能有以下三种可能性:

  • 在[1, n/2]这个区域内
  • 在[n/2+1, n]这个区域内
  • 起点位于[1,n/2],终点位于[n/2+1,n]内

以上三种情形的最大者,即为所求. 前两种情形符合子问题递归特性,所以递归可以求出. 对于第三种情形,则需要单独处理.第三种情形必然包括了n/2和n/2+1两个位置,这样就可以利用第二种穷举的思路求出:

  • 以n/2为终点,往左移动扩张,求出和最大的一个left_max
  • 以n/2+1为起点,往右移动扩张,求出和最大的一个right_max
  • left_max+right_max是第三种情况可能的最大值

参考代码如下:

?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
intmaxInterval(int*a, int left,int right)
{
if(right==left)
returna[left]>0?a[left]:0;
int center =(left+right)/2;
//左边区间的最大子段和
intleftMaxInterval = maxInterval(a,left,center);
//右边区间的最大子段和
intrightMaxInterval= maxInterval(a,center+1,right);
//以下求端点分别位于不同部分的最大子段和
//center开始向左移动
int sum =0;
intleft_max = 0;
for(int i =center; i >= left; –i)
{
sum += a[i];
if(sum> left_max)
left_max = sum;
}
//center+1开始向右移动
sum = 0;
intright_max = 0;
for(int i =center+1; i <= right; ++i)
{
sum += a[i];
if(sum> right_max)
right_max = sum;
}
int ret =left_max+right_max;
if(ret< leftMaxInterval)
ret =leftMaxInterval;
if(ret< rightMaxInterval)
ret =rightMaxInterval;
returnret;
}

分治法的难点在于第三种情形的理解,这里应该抓住第三种情形的特点,也就是中间有两个定点,然后分别往两个方向扩张,以遍历所有属于第三种情形的子区间,求的最大的一个,如果要求得具体的区间,稍微对上述代码做点修改即可.分治法的计算时间复杂度为O(nlogn).

3.动态规划法

动态规划的基本原理这里不再赘述,主要讨论这个问题的建模过程和子问题结构.时刻记住一个前提,这里是连续的区间

  • 令b[j]表示以位置 j 为终点的所有子区间中和最大的一个
  • 子问题:如j为终点的最大子区间包含了位置j-1,则以j-1为终点的最大子区间必然包括在其中
  • 如果b[j-1] >0, 那么显然b[j] = b[j-1] +a[j],用之前最大的一个加上a[j]即可,因为a[j]必须包含
  • 如果b[j-1]<=0,那么b[j] = a[j],因为既然最大,前面的负数必然不能使你更大

对于这种子问题结构和最优化问题的证明,可以参考算法导论上的“剪切法”,即如果不包括子问题的最优解,把你假设的解粘帖上去,会得出子问题的最优化矛盾.证明如下

  • 令a[x,y]表示a[x]+…+a[y] , y>=x
  • 假设以j为终点的最大子区间 [s, j] 包含了j-1这个位置,以j-1为终点的最大子区间[ r,j-1]并不包含其中
  • 即假设[r,j-1]不是[s,j]的子区间
  • 存在s使得a[s, j-1]+a[j]为以j为终点的最大子段和,这里的 r !=s
  • 由于[r, j -1]是最优解,所以a[s,j-1]<a[r,j-1],所以a[s,j-1]+a[j]<a[r, j-1]+a[j]
  • 与[s,j]为最优解矛盾.

参考代码如下:

?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
int max =0;
intb[n+1];
int start =0;
int end =0;
memset(b,0,n+1);
for(int i = 1;i <= n; ++i)
{
if(b[i-1]>0)
{
b[i] = b[i-1]+a[i];
}else{
b[i] = a[i];
}
if(b[i]>max)
max = b[i];
}

动态规划法的计算时间复杂度为O(n),是最优的解,这里推荐练习一下UVA507来加深理解. 我以前的题解:

http://www.stackpop.org/blog/html/y2007/371_uva_507.html


我们总结一下二维最大子段和问题,以及最大m段和问题.

二维最大子段和问题

二维最大子段和问题又称为最大子矩阵问题,给定一个m行n列的整数矩阵A,试求A的一子矩阵,使其各元素之和最大

问题分析

子矩阵的概念这里不再赘述,不了解的可以去复习一下线性代数.如下图所示的

首先明确一件事情,可以通过知道对角线上的两个元素来确定一个子矩阵,在二维最大子段和问题中,我们要求的是这样一个子矩阵,如图中红框所示,其中0<= i <= j <=n-1 , 0<= p <= q <=n-1。因而容易得到一个O(n^4)的枚举算法.

动态规划法

动态规划法其实就是把二维最大子段和转化为一维最大子段和问题.
转化方法:

  • 我们把这个矩阵划分成n个“条”,条的长度为1到m,通过两个for遍历所有长度的条
  • 然后,若干个连续的条,就是一个子矩阵了,这样问题就轻易地转化为一维最大子段和问题了
  • 通过求所有这种条,起点为i,长度为1到m-i+1的“条”的最大子段和,就可以求出整个矩阵的最大子矩阵了
  • 具体枚举长条的时候,同一起点的长度,由于“条”的不同长度间可以利用之前的结果
  • 比如令b[k][i][j]表示第k个长“条”区间从i到j的和,那么b[k][i][j+1] =b[k][i][j]+a[j][k]
  • 当然,实际编程的时候,由于之前的结果求完一维最大子段和后,便不需要保存,所以只需要一维数组b即可

参考代码如下:

?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
//标准的一维最大子段和
intmaxSubInterval(int*data, intn)
{
int max =0;
int b =0;
for(int i = 0;i != n; ++i)
{
if(b> 0)
{
b = b+data[i];
}else{
b = data[i];
}
if(b>max)
max = b;
}
returnmax;
}
intmaxSubMatrix(int(*a)[10], int m,int n)
{
int max =0;
//b[k]记录第k个“条”的和
int *b= new int[n+1];
for(int i = 0;i != m; ++i)
{
//“条”的和先置为0
for(int k = 0;k != n; ++k)
b[k] = 0;
//起点为i,长度为j-i+1的条
//相同起点,长度为k的“条”的和,等于长度为k-1的“条”的和加上当前元素a[j][k]
for(int j = i;j != m; ++j)
{
for(int k = 0;k != n; ++k)
b[k] += a[j][k];
int sum =maxSubInterval(b,n);
if(sum> max)
max = sum;
}
}
free(b);
returnmax;
原创粉丝点击