问题描述:给定n个元素组成的序列:a0,a1,a2,...,an,求此序列中和最大的连续子序列。
如:序列为 1,3,-8,2,6,-2,4,-2,1 ,求得子序列为:2,6,-2,4 结果为10 。
额,自己很久都没写过动态规划方面的题目了,上一次校赛那个矩阵连乘问题拿到手上一看就知道是动态规划方面的问题,但就是不晓得怎么写。但面试题目中有很多动态规划方面的题目层出不穷,所以,偶打算从最简单的动态规划方面写起。
首先,还是写个暴力的呗。就是把序列的字串的和枚举出来。找出最大的一个即可。根据这个思路,代码很快出来了!
int maxSum(int a[], int n, int *begin, int *end)
{
int sum = -1000000, temp, i, j, k;
for(i=0; i<n; ++i)
{
for(j=i+1; j<n; ++j)
{
temp = 0;
for(k=i; k<=j; ++k)
temp += a[k];
if(temp > sum)
{
*begin = i;
*end = j;
sum = temp;
}
}
}
return sum;
}
int main()
{
int a[100], n, i;
while(scanf("%d", &n) != EOF)
{
int bestBegin, bestEnd;//最大子序列的下表的开始位置和结束位置。
for(i=0; i<n; ++i)
scanf("%d", &a[i]);
printf("bestBegin = %d, bestEnd = %d, The Subsequence MaxSum = %d./n", bestBegin, bestEnd, maxSum(a, n, &bestBegin, &bestEnd));
}
return 0;
}
额,话说我暴力也还是出了一点小小的问题。当我输入测试数据 1 10的时候,结果给我返回一个结果:
bestBegin = -858993460, bestEnd = -858993460, The Subsequence MaxSum = -1000000.偶一查maxSum函数,马上知道代码问题出哪里了,果断把第一个for循环前面j初始化为 j = i;OK,序列只有一个数也正常了。这个很幼稚的错误我特意列出来是提醒自己一下,无论觉得自己的Code多么正确,一定要用自己能想到的刁钻的测试数据去测试一下自己的代码。
OK,来分析一下这个代码。呵呵,优点是简单易懂,只需要O(1)的空间。可是时间复杂度为O(n3),显然,这么大的时间复杂度肯定是不行的,当n=1千万的时候,很难出结果了。那就慢慢优化呗~~~
OK,注意到sum(a0+a1+a2+……+aj-1+aj) = sum(a0+a1+a2+……+aj-1) +aj ,那么,我们可以把maxSum函数做个小小的优化。代码如下:
for(i=0; i<n; ++i)
{
temp = 0;
for(j=i; j<n; ++j)
{
temp += a[j];
if(temp > sum)
{
*begin = i;
*end = j;
sum = temp;
}
}
}
return sum;
}
这样,利用前面计算出来的结果,不重复计算,我们把时间复杂度从O(n3)降到了O(n2)。好了,接着再来优化吧。
额,接下来是用分治的算法来降低复杂度。
将a[1n]分成a[1n/2]和a[n/2+1n],则a[1n]的最大字段和有三种情况:
(1)a[1n]的最大子段和与a[1n/2]的最大子段和相同
(2)a[1n]的最大子段和与a[n/2n]的最大子段和相同
(3)a[1n]的最大子段和为ai++aj,1<=i<=n/2,n/2+1<=j<=n
相信这个思想大家都能看懂。偶就啥也不说了,根据这个思想,偶就把代码写出来了。
int maxSum(int a[],int left, int right)
{
int sum = -100000000;
if(left == right-1)
{
return a[left];
}
else
{
int middle = (left + right) / 2;
int leftSum = maxSum(a, left, middle);
int rightSum = maxSum(a, middle, right);
int i;
int temp = 0, tempLeftSum = -100000000;
for(i=middle-1; i>=left; --i)
{
temp += a[i];
if(temp > tempLeftSum)
{
tempLeftSum = temp;
}
}
temp = 0;
int tempRightSum = -100000000;
for(i=middle; i<right; ++i)
{
temp += a[i];
if(temp > tempRightSum)
{
tempRightSum = temp;
}
}
sum = tempLeftSum + tempRightSum;
if(sum < leftSum)
sum = leftSum;
if(sum < rightSum)
sum = rightSum;
}
return sum;
}
int main()
{
int a[100], n, i;
while(scanf("%d", &n) != EOF)
{
for(i=0; i<n; ++i)
scanf("%d", &a[i]);
printf("The Subsequence MaxSum = %d./n", maxSum(a, 0, n));
}
return 0;
}
终于把O(n3),O(n2)的复杂度降到了O(nlog2n)。呵呵,应该是一个不错的改进了。O(∩_∩)O哈哈~给自己鼓掌一下。还能不能降低时间复杂度呢?答案是肯定的!因为动态规划的思想还没有提到嘛。下面就用动态规划的思想把时间复杂度降到O(n)。
额,以下思路来自《编程之美》。
假设已经知道(a[1], a[2], ......a[n-1])中最大的一段数组之和为sum[1],并且你还知道(a[1], a[2], ......a[n-1])包含a[1]的最大和的一段数组为start[1],那么,毫无疑问,(a[0], a[1], a[2], ......a[n-1])所求的最大的一段数组和为max(a[0], a[0]+start[1], sum[1]);由于这个问题无后效性,故根据该思路可以写出如下代码:
int max(int a, int b)
{
return a>b ? a:b;
}
int maxSum(int a[],int n)
{
int *sum = new int[n], *start = new int[n], i, temp;
sum[n-1] = a[n-1];
start[n-1] = a[n-1];
for(i=n-2; i>=0; --i)
{
start[i] = max(a[i], a[i]+start[i+1]);
sum[i] = max(start[i], sum[i+1]);
}
temp = sum[0];
delete []sum;
delete []start;
return temp;
}
int main()
{
int a[100], n, i;
while(scanf("%d", &n) != EOF)
{
for(i=0; i<n; ++i)
scanf("%d", &a[i]);
printf("The Subsequence MaxSum = %d./n", maxSum(a, n));
}
return 0;
}
值得注意的是:maxSum函数的for循环里面不能写成max(a[i], a[i]+start[i+1], sum[i+1])(想想为什么?)。
不过美中不足的是,虽然这段代码终于把时间复杂度降到了O(n),但我们又申请了两个额外的两个数组start[n], sum[n]。能不能把空降复杂度降到O(1)呢?我们来观察maxSum函数里面的两个递推式:
start[i] = max(a[i], a[i]+start[i+1]);
sum[i] = max(start[i], sum[i+1]);
很显然,要是start[i+1]<0,那么start[i] = a[i], 而且,start[i+1]只有在计算start[i]时才使用,而sum[i+1]也只有在计算sum[i]的时候才使用。所以程序可做进一步改进,使空间复杂度将为O(1)。代码如下:
int max(int a, int b)
{
return a>b ? a:b;
}
int maxSum(int a[],int n)
{
int sum, start, i;
sum = a[n-1];
start = a[n-1];
for(i=n-2; i>=0; --i)
{
start = max(a[i], a[i]+start);
sum = max(start, sum);
}
return sum;
}
int main()
{
int a[100], n, i;
while(scanf("%d", &n) != EOF)
{
for(i=0; i<n; ++i)
scanf("%d", &a[i]);
printf("The Subsequence MaxSum = %d./n", maxSum(a, n));
}
return 0;
}
OK,该算法不仅改进了空间,而且代码也不长。基本上算是完美了~~~当然,上面的代码是逆推出来的,可不可以顺推呢?当然是可以的,请看下面的代码:
int maxSum(int a[],int n)
{
int sum = -10000000, temp = sum, i;
for(i=0; i<n; ++i)
{
if(temp>0)
temp += a[i];
else
temp = a[i];
sum = temp > sum ? temp : sum;
}
return sum;
}
int main()
{
int a[100], n, i;
while(scanf("%d", &n) != EOF)
{
for(i=0; i<n; ++i)
scanf("%d", &a[i]);
printf("The Subsequence MaxSum = %d./n", maxSum(a, n));
}
return 0;
}
好了,这段代码我觉得是所有代码里面最完美的了,不仅空间复杂度时间复杂度都为O(n),而且代码短小。最后留两个作业给自己和看过的同学思考:①如果要返回最大子数组的位置,算法该如何改变?(其实偶在暴力法里面写了)还能保持O(n)的时间复杂度么?②如果数组首位相连,代码该如何写?