• [算法]连续子数组最大和


    [问题描述]

    给定一个数组,求其中一段连续的子数组,使其和最大。

    [算法及其实现]

    据说这是一个面试的经典问题,照例咱们先来分析分析。如果所有数都是非负的,当然整个数组的和就是最大和;所以当数组中存在负数时,这个问题才有意思。(本题考虑整型数,且一切运算结果不超过int的情况)

    一切先从暴力开始思考。一个很朴素的思想是,枚举子数组的起始位置、终止位置,再进行加和。所以这是个O(n^3)的算法,实现如下(c++代码):

     1 #include <iostream>
     2 #include <cstdio>
     3 using namespace std;
     4 #define MaxN 120
     5 void Read(int *a, int &n)
     6 {
     7     int i;
     8     printf("请输入一个整数n,表示数组的元素个数。
    ");
     9     scanf("%d", &n);
    10     printf("请输入n个整数,用空白符隔开。
    ");
    11     for(i = 1; i <= n; i++)
    12         scanf("%d", &a[i]);
    13 }
    14 int Get_Max_Sum(int *a, int n) //求a[1...n]的连续子数组最大和 
    15 {
    16     int i, j, k;
    17     int Ret, Sum;
    18     Ret = a[1];
    19     for(i = 1; i <= n; i++)
    20     {
    21         for(j = i; j <= n; j++)
    22         {
    23             Sum = 0;
    24             for(k = i; k <= j; k++)
    25                 Sum +=a[k];
    26             Ret = max(Ret, Sum);
    27         }
    28     }
    29     return Ret;
    30 }
    31 int main()
    32 {
    33     int a[MaxN], n;
    34     int Ans;
    35     Read(a, n);
    36     Ans = Get_Max_Sum(a, n);
    37     printf("连续子数组最大和 = %d
    ", Ans);
    38     return 0;
    39 }
    View Code

    当然我们是不满意这样的复杂度的,所以思考优化。很容易能想到,当已知起始位置和终止位置之后,在进行加和计算是愚蠢而耗时的(因为重复计算太多),能不能把这个操作优化一下呢?答案是肯定的。有一个非常常用的技术——维护前缀和。对于原数组a[]来说,我们维护一个新的数组sum[],其中sum[i]=sigma{a[k]}_{k=1...i},那么a[i]+a[i+1]+……+a[j]=sum[j]-sum[i-1],这样一个O(n)的操作就变成O(1)的了。整体的复杂度降为O(n^2)。优化后的代码如下(c++代码):

     1 #include <iostream>
     2 #include <cstdio>
     3 using namespace std;
     4 #define MaxN 120
     5 void Read(int *a, int &n)
     6 {
     7     int i;
     8     printf("请输入一个整数n,表示数组的元素个数。
    ");
     9     scanf("%d", &n);
    10     printf("请输入n个整数,用空白符隔开。
    ");
    11     for(i = 1; i <= n; i++)
    12         scanf("%d", &a[i]);
    13 }
    14 int Get_Max_Sum(int *a, int n) //求a[1...n]的连续子数组最大和 
    15 {
    16     int i, j;
    17     int Ret;
    18     a[0] = 0;
    19     for(i = 2; i <= n; i++)
    20         a[i] += a[i - 1]; //这里a[]就变为了维护前缀和的sum[]
    21     Ret = a[1];
    22     for(i = 1; i <= n; i++)
    23         for(j = i; j <= n; j++)
    24             Ret = max(Ret, a[j] - a[i - 1]);
    25     return Ret;
    26 }
    27 int main()
    28 {
    29     int a[MaxN], n;
    30     int Ans;
    31     Read(a, n);
    32     Ans = Get_Max_Sum(a, n);
    33     printf("连续子数组最大和 = %d
    ", Ans);
    34     return 0;
    35 }
    View Code

    但从实际考虑,O(n^2)仍然是一个很高的复杂度,我们当然希望复杂度能够更低。从复杂度上来看,比O(n^2)低的一般来说有两个量级,O(n lg n)和O(n)。先思考O(n lg n)的算法,出现对数,基本上跟二分会扯上关系,那么这个问题能否用分治来实现呢?非常幸运,是可以的。

    从答案出发思考,最后得到的使和最大的一段连续的子数组,会出现在什么地方呢?将原数组从中间一分为二mid=(left+right)/2,要么答案出现在左侧,要么答案出现在右侧,要么答案横跨中间位置。前两种情况将问题转化为规模更小、本质没变的子问题了,分治的雏形大致明了了;对于后一种情况,我们很容易证明,最大和=mid向左延伸的最大值+(mid+1)向右延伸的最大值,复杂度为O(n)。所以整体复杂度为O(n lg n)。实现如下(c++代码):

     1 #include <iostream>
     2 #include <cstdio>
     3 using namespace std;
     4 #define MaxN 120
     5 void Read(int *a, int &n)
     6 {
     7     int i;
     8     printf("请输入一个整数n,表示数组的元素个数。
    ");
     9     scanf("%d", &n);
    10     printf("请输入n个整数,用空白符隔开。
    ");
    11     for(i = 1; i <= n; i++)
    12         scanf("%d", &a[i]);
    13 }
    14 int Get_Max_Sum(int *a, int L, int R)
    15 {
    16     int Ret, tmp, mid = L + (R - L) / 2;
    17     int Max1, Max2;
    18     int i;
    19     /*单个点的情况,直接返回*/
    20     if(L == R)
    21     {
    22         Ret = a[L];
    23         return Ret;
    24     }
    25     /*从左侧得到答案*/
    26     Ret = Get_Max_Sum(a, L, mid);
    27     /*从右侧得到答案*/
    28     tmp = Get_Max_Sum(a, mid + 1, R);
    29     Ret = max(Ret, tmp);
    30     /*横跨mid的情况,实际上是指至少包括a[mid]和a[mid+1]*/
    31     //左侧
    32     tmp = 0;
    33     Max1 = a[mid];
    34     for(i = mid; i >= L; i--)
    35     {
    36         tmp += a[i];
    37         Max1 = max(Max1, tmp);
    38     }
    39     //右侧
    40     tmp = 0;
    41     Max2 = a[mid + 1];
    42     for(i = mid + 1; i <= R; i++)
    43     {
    44         tmp += a[i];
    45         Max2 = max(Max2, tmp);
    46     }
    47     tmp = Max1 + Max2;
    48     Ret = max(Ret, tmp);
    49     return Ret;
    50 }
    51 int main()
    52 {
    53     int a[MaxN], n;
    54     int Ans;
    55     Read(a, n);
    56     Ans = Get_Max_Sum(a, 1, n);
    57     printf("连续子数组最大和 = %d
    ", Ans);
    58     return 0;
    59 }
    View Code

    到现在为止,这样的复杂度已经足够让人感到满意了。(如果说递归的实现让人担心速度或者爆栈,可以尝试手写栈等,这都是实现技术上的问题,这里笔者认为没有必要纠结)

    但是人都是贪心的,之前我们说了,还有一种更低的复杂度的量级O(n),如果能达到这个级别那多好啊。确实,真的有O(n)的算法。(zyy卖了好久的关子w(゚Д゚)w)

    在上一种算法中,有一种情况是当前区间的答案来自于横跨中间位置mid的子数组,其答案由mid向左延伸最大值和(mid+1)向右延伸最大值组成。由此得到启发,我们可以从某一个点向左延伸,当然一定会有一个最大值(相当于固定了子数组的末端位置)。乍一看还是一个平方复杂度的算法,实则不然。考虑当前按位置i,从位置i向前延伸得到的最大和保存在sum[i]中,而对于sum[i]的取值,要么将当前元素a[i]跟之前的加在一起为sum[i]=sum[i-1]+a[i],要么从i位置重新开始一个新的子串,即sum[i]=a[i]。sum[i]取何值与sum[i-1]有关,若sum[i-1]<0,显然应该丢掉前面的东西重新开始,否则选后者。所以最终答案是max{sum[1...n]}。

    具体实现时,我们发现sum[]的元素不会重复使用,故只用一个变量就可以了,另外用一个变量来记录出现过的最大值即可。实现如下(c++代码):

     1 #include <iostream>
     2 #include <cstdio>
     3 using namespace std;
     4 #define MaxN 120
     5 void Read(int *a, int &n)
     6 {
     7     int i;
     8     printf("请输入一个整数n,表示数组的元素个数。
    ");
     9     scanf("%d", &n);
    10     printf("请输入n个整数,用空白符隔开。
    ");
    11     for(i = 1; i <= n; i++)
    12         scanf("%d", &a[i]);
    13 }
    14 int Get_Max_Sum(int *a, int L, int R)
    15 {
    16     int Sum, Ret, i;
    17     Ret = Sum = a[L];
    18     for(i = L + 1; i <= R; i++)
    19     {
    20         if(Sum > 0)
    21             Sum += a[i];
    22         else
    23             Sum = a[i];
    24         Ret = max(Ret, Sum);
    25     }
    26     return Ret;
    27 }
    28 int main()
    29 {
    30     int a[MaxN], n;
    31     int Ans;
    32     Read(a, n);
    33     Ans = Get_Max_Sum(a, 1, n); //求数组a[1...n]的子数组最大和
    34     printf("%d
    ", Ans);
    35     return 0;
    36 }
    View Code

    这个算法确实应该算作DP,给出状态转移方程:

    sum[0] = 0

    sum[i] = max{sum[i - 1], sum[i - 1] + a[i]}

    Ans = max{sum[i]}_{i=1...n}

  • 相关阅读:
    MVAPICH
    sql server触发器的例子
    Sql Server 判断表或数据库是否存在
    JS验证用户真实姓名
    js实现瀑布流的一种简单方法实例分享
    C#实现登录窗口(不用隐藏)
    判断滚动条到底部的JS代码
    php 中文字符串首字母的获取函数
    C#获取当前页面的URL
    C#动态生成图书信息XML文件
  • 原文地址:https://www.cnblogs.com/CQBZOIer-zyy/p/5282802.html
Copyright © 2020-2023  润新知