最大子序列和问题
- 对于最大子序列求和问题有很多种解法。其中比较简单的是使用3层嵌套for循环进行求解,这种方法非常简单但很暴力,需要 O ( N 3 ) O(N^{3}) O(N3)的时间花费。
int MaxSubsequenceSum( const int A[], int N)
{
int ThisSum, MaxSum, i, j, k;
MaxSum = 0;
for(i=0; i<N; i++)
for(j=i; j<N; j++)
{
ThisSum = 0;
for(k=i; k<j; k++)
ThisSum += A[k]; //计算从i到j之间的和
if(ThisSum>MaxSum)
MaxSum = ThisSum;
}
return MaxSum;
}
- 我们稍微优化一下可以变成一个2层嵌套for循环,但实质上并没有简化多少,时间复杂度 O ( N 2 ) O(N^{2}) O(N2)。
int MaxSubsequenceSum( const int A[], int N)
{
int ThisSum, MaxSum, i, j;
MaxSum = 0;
for(i=0; i<N; i++)
ThisSum = 0;
for(j=i; j<N; j++)
{
ThisSum += A[j]; //计算每个从i为起点的最大值
if(ThisSum>MaxSum)
MaxSum = ThisSum;
}
return MaxSum;
}
- 再高端一些,我们可以使用分治策略,递归的计算序列左半部、右半部的最大子序列和,比较序列左半部、右半部以及中间的最大和进行求解。
//递归函数
static int MaxSubSum( const int A[], int Left, int Right)
{
int MaxLeftSum, MaxRightSum;
int MaxLeftBorderSum, MaxRightBorderSum;
int LeftBorderSum ,RightBorderSum;
int Center ,i;
//递归停止条件
if(Left==Right)
if(A[Left]>0)
return A[Left];
else
return 0;
//计算中间位置
Center = ( Left + Right ) / 2;
//递归求解左半部和右半部最大和
MaxLeftSum = MaxSubSum(A, Left, Center);
MaxRightSum = MaxSubSum(A, Center+1, Right);
//计算左边界最大值
MaxLeftBorderSum = 0;
LeftBorderSum = 0;
for(i=Center; i>=Left; i--)
{
LeftBorderSum += A[i];
if(LeftBorderSum>MaxLeftBorderSum)
MaxLeftBorderSum = LeftBorderSum;
}
//计算右边界最大值
MaxRightBorderSum = 0;
RightBorderSum = 0;
for(i=Center; i<=Right; i++)
{
RightBorderSum += A[i];
if(RightBorderSum>MaxRightBorderSum)
MaxRightBorderSum = RightBorderSum;
}
//比较左半部、右半部以及中间边界的最大值
return Max3(MaxLeftSum, MaxRightSum,
MaxLeftBorderSum+MaxRightBorderSum);
}
//开始递归函数
int MaxSubsequenceSum(const int A[], int N)
{
return MaxSubSum(A, 0, N-1);
}
我们可以知道:
T
(
1
)
=
1
T(1)=1
T(1)=1
T
(
N
)
=
2
T
(
N
/
2
)
+
O
(
N
)
T(N)=2T(N/2)+O(N)
T(N)=2T(N/2)+O(N)
通过递推方法我们可以得到:
若
N
=
2
k
,
T
(
N
)
=
N
∗
(
k
+
1
)
+
N
=
N
l
o
g
N
+
N
若N=2^k,T(N)=N*(k+1)+N=NlogN+N
若N=2k,T(N)=N∗(k+1)+N=NlogN+N
因此该算法时间复杂度为
O
(
N
l
o
g
N
)
O(NlogN)
O(NlogN)。
- 上诉三种方法都比较好理解,下面介绍一种最方便的求解方法。
int MaxSubsequenceSum( const int A[], int N)
{
int ThisSum, MaxSum, i;
MaxSum = 0;
for(i=0; i<N; i++)
{
ThisSum += A[i];
if(ThisSum>MaxSum)
MaxSum = ThisSum;
else if(ThisSum<0)
ThisSum = 0;
}
return MaxSum;
}
这个方法看似很难理解,其实我们想清楚后是很明朗的。
首先代码表示的是如果当前数之前的序列求和大于MaxSum则交换,若求和小于零,那么本次求和归零,从下一个点从新开始求和。
可以这么想,假如从序列A第一个数开始加,加到第i个数时求和为负数,可以肯定的是A[i]一定为负数,那我们从A[i]开始反过去求和会发现求和即便在变大它也一定是负的,因此这段序列对之后的求和完全没有正作用,因此我们要清零,从新开始计算下一组序列。
该算法的时间复杂度为
O
(
N
)
O(N)
O(N),并且该算法只需要对A[]进行一次扫描,并且不需要被记忆。
其实该算法与动态规划相似,动态规划使用递推关系来计算:
d
p
[
i
]
=
m
a
x
(
0
,
d
p
[
i
−
1
]
)
+
A
[
i
]
dp[i]=max(0, dp[i-1])+A[i]
dp[i]=max(0,dp[i−1])+A[i]
其中
d
p
[
i
]
dp[i]
dp[i]表示以第i个数结尾的最大子序列和,同样的若前一个数的最大子序列和大于0则继续求和,否则从当前点重新开始求和。
本次算法都来自于数据结构与算法分析,c语言描述版。