分治:区间和问题

问题:

现有一个含n个整数(可含负数)的序列,求x,y使a[x]+a[x+1]+……+a[y]最大.


算法:


第一级:枚举

首先想到的就是暴力枚举:从大到小枚举x,y,计算从a[x]加到a[y]的和,与当前答案比较。代码如下:

#include <cstdio>

const int maxn=1000;

int main()
{
  int n,ans,a[maxn];
  scanf("%d",&n);

  for (int i=0;i<n;i++)
    scanf("%d",&a[i]);

  ans=a[0];

  for (int i=0;i<n;i++)
    for (int j=i;j<n;j++)
    {
        int sum=0;
        for (int k=i;k<=j;k++) sum+=a[k];

        ans=sum>ans?sum:ans;
    }

  printf("%d\n",ans);
  return 0;
}

时间复杂度:O(n^3),对于大一点的n,n>1000时就相当吃力了。当然还要继续优化。

第二级:枚举+优化:

我们发现:当计算 [2,7] 和 [1,6] 时,我们都把[1,6] 计算了两次。实际计算真是有大量这样的浪费。
由此不难得到OI中的一个常见优化:前缀和。我们用s[i]表示a[1]+a[2]+……a[i]的和,当计算[x,y]时,我们只需计算s[y]-s[x-1]就行了,在O(1)时间内得到了元素和。

#include <cstdio>

const int maxn=1000;

int main()
{
  int n,ans,a[maxn],s[maxn];
  scanf("%d",&n);

  s[0]=0;

  for (int i=1;i<=n;i++)
  {
    scanf("%d",&a[i]);
    s[i]=s[i-1]+a[i];
  }

  ans=a[0];

  for (int i=1;i<=n;i++)
    for (int j=i;j<=n;j++)
    {
        int sum=s[j]-s[i-1];
        ans=sum>ans?sum:ans;
    }

  printf("%d\n",ans);
  return 0;
}

时间复杂度:O(n^2),然而至此,我们还是可以再次优化。

第三级:分治算法:

首先一个序列,我们要求它的最大子序列和,只有3种情况:

(1):最大子序列全在左字串
(2):最大子序列全在右子串
(3):最大子序列头在左字串,尾在右子串

所以对于一个序列[x,y],我们先求出它左字串的最大子序列,再求出它的右子串的最大子序列,最后把结果与直接求出的(3)的子序列比较,最大值即[x,y]的最大子序列。注意:这个过程是递归的,所以求左右子串的最大子序列亦是如此,只不过当x=y时,直接返回a[x]即可(不能返回0,因为不能不选)。

#include <cstdio>
#include <cstdlib>
#include <algorithm>

using namespace std;

const int maxn=1000;

int max_sum(int *a,int x,int y)
{
  if (x==y) return a[x];

  int m=x+(y-x)/2; //中间下标
  int maxm=max(max_sum(a,x,m),max_sum(a,m+1,y)); //左右子串最大值

  int v=0,l=a[m],r=a[m+1];

  // 求出中间串最大值(贪心过程)

  for (int i=m;i>=x;i--)
    l=max(l,v+=a[i]);

  v=0;

  for (int i=m+1;i<=y;i++)
    r=max(r,v+=a[i]);

  return max(maxm,l+r); //比较
}

int main()
{
  int n,a[maxn];
  scanf("%d",&n);

  for (int i=0;i<n;i++)
    scanf("%d",&a[i]);

  printf("%d\n",max_sum(a,0,n-1));
  return 0;
}

时间复杂度?因为二分的存在,复杂度降至了O(nlogn)。

第四级:信息维护:

即便标题是分治,也有更好的算法:不难发现,最终的答案就是找到y>=x,使得s[y]-s[x]最大。

我们从前往后枚举y,远比博主机智的多的读者们已经发现了:上一次枚举的x,在这一次的枚举中不会更差。由此,我们可以维护一个”前趋最小s:last“,每次我们用当前的s[y]-last与答案比较,更大再采用,再判断当前的s[y]是否比last更小,维护即可:

#include <cstdio>
#include <algorithm>

using namespace std;

const int maxn=10000;

int main()
{
  int n,a[maxn],s[maxn];
  scanf("%d",&n);

  s[0]=0;
  for (int i=1;i<=n;i++)
  {
    scanf("%d",&a[i]);
    s[i]=s[i-1]+a[i];
  }

  int ans=a[1],min=s[1];

  for (int i=2;i<=n;i++)
  {
    ans=max(ans,s[i]-min); //维护答案
    if (s[i]<min) min=s[i]; //维护最小值
  }

  printf("%d\n",ans);
  return 0;
}

因为x,y单调递增,所以只遍历了一遍。自此,时间复杂度降到了O(n)。


小结:

即使存在O(n)算法,这个问题最重要的实际上还是分治的算法。分治的思想可广泛运用于其他算法,像归并排序,快速排序,二分查找之类,在子串的情况分析上也闪耀着线段树的思想。分治作为OI的基本算法之一,尽管难度不高,实用性却不容小觑。

(P.S:由于博主水平问题,本节代码有可能存在Bug或是写的不够优化的地方,读者若发现,可以通过留言告知,博主定会立即修改)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值