浅析连续子向量,子数组和(一维,二维)问题

浅析连续子向量,子数组和(一维,二维)问题

 9月 16 2014 更新日期:9月 16 2014

文章文件夹
  1. 1. 最大连续子向量和
    1. 1.0.1. 问题描写叙述:
  • 2. 子向量和接近于0
  • 3. 收费站问题
  • 4. 区间赋值问题
  • 5. 二维连续子数组和
  • 6. 參考资料:
  • 最大连续子向量和

    问题描写叙述:

    输入是具有n个浮点数的向量x,输出这个向量的不论什么连续子向量中的最大和。
    

    简单分析:子向量能够是一个空向量,空向量的和为0;假设向量的全部元素都是负数,最大子向量和就是0;

    1 简单分析后,对于这个问题,我们立刻能向想到的就是暴力算法,对全部0<=i<=j<n的整数对进行迭代。对每一个整数对(i,j),程序都要计算x[i…j]的总和,并推断其是否大于当前的最大总和。

    解法1:简单粗暴型
    int res=0; //答案
    for(int i= 0 ; i<n;i++)
        for(int j=i;j<n;j++)
        {
            sum=0
            for(int k=i;k<=j;k++) 
                sum+=x[k]
            res=max(res,sum)    
        }
    

    2 怎么看,上面的算法都是简单粗暴型,O(n^3)的时间复杂度实在不敢恭维,数据量一大,时间上实在不能容忍。那么有没有略微优雅一点的?我们发现后面的部分有反复计算的,那么我们怎样节省它~~一种就是从i開始往前加的时候,每次都记录下来。直接看代码:

    解法2int res=0;  //答案
    for(int i=0;i<n;i++)
    {
        int sum=0;  
        for(int j=i;j<n;j++)
        {
            sum+=x[j];  // sum 就是 x[i]至x[j]的和 
            res=max(res,sum);  
        }
    }
    

    3 上面的代码,尽管比简单粗暴型有一些改进,算法的时间复杂度降为O(n^2),另一种O(n^2)的算法,令sum(i)表示x[0…i]的总和,然后,x[i] = sum(i) - sum(i-1);

    解法3sum[-1]=0
    for i=[0,n)
        sum[i]=sum[i-1]+x[i]
    res=0  //储存答案
    for i=[0,n)
        for j=[i,n)
            tem=sum[j]-sum[i-1] 
            res=max(res,tem)
    

    4 O(n^2)的效率,我们还是认为不行,可不能够优化一下,好了,我们能够採用分治的思想。要解决规模为n的问题,可递归地解决两个规模近似为n/2的子问题,然后对两个结果进行合并以得到整个问题的答案。将x划分为两个近似相等的子向量ab,在a和b中分别找出总和最大的子向量ma和mb,然后找到跨越a和b边界的最大子向量mc,返回三个总和中的最大者。通过观察发现mc在a中的部分是a中包括右边界的最大子向量,mc在b中的部分是b中包括左边界的最大子向量。伪代码例如以下:

    解法4:
    float maxsum(l,u)
        if(l>u)  return 0  /* zero elements */
        if(l==u)  return max(0,x[1])  /* one element */
        m=(l+u)/2
        lmax=sum=0;
        for(i=m;i>=l;i--)  /* find max crossing to left */
            sum+=x[i]
            lmax=max(lmax,sum)
        rmax=sum=0
        for i=(m,u]  /* find max crossing to right */
            sum+=x[i]
            rmax=max(rmax,sum)
        return  max(lmax+rmax,maxsum(l,m),maxsum(m+1,u))
    

    5 能否够再优化一下?好了,事实上能够。使用扫描算法:我们採用从x[0]開始扫描,一起到最右端x[n-1],并记下所遇到的最大子向量总和(初始值设为0)。如果我们已攻克了x[0,i-1]的问题,怎样将其扩展到x[0…i]呢?前i个元素中,最大总和子数组要么在前i-1个元素中(用maxsofar存储),要么其结束位置为i(用maxendinghere存储)。

    maxsofar=0
    maxendinghere=0
    for i=[0,n)
        maxendinghere=max(maxendinghere+x[i],0) /* 计算前maxendinghere是结束位置为i-1的最大子向量的和 */
        maxsofar=max(maxsofar,maxendinghere)
    

    几个重要的算法设计技术:

    • 保存状态,避免反复计算:

    • 将信息预处理至数据结构:   

    • 分治算法:

    子向量和接近于0

    如果我们想要查找的是总和最接近0的子向量,而不是具有最大总和的子向量,该怎样设计算法?

    可初始化累加数组cum,使得cum[i]=x[0]+…+x[i]。假设cum[l-1]=cum[u],那么子向量x[l…u]之和就为0.因此能够通过定位cum中最接近的两个元素来找出和最接近0的子向量。这能够通过排序数组,在O(nlogn)时间内完毕。


    收费站问题

    问题描写叙述:

    收费公路由n个收费站之间的n-1段公路组成,每一段都和行驶费用挂钩,仅使用费用数组依照O(n)的时间,或者使用具有O(n^2)个项的表依照O(1)的时间描写叙述随意两站之间的费用是无意义的,请设计一个结构,它须要O(n)的空间,但它同意O(1)的时间复杂度求解。
    

    驶过两个收费站,就是一段公路,汽车在行驶时,仅仅能连续行驶,不会从这段公路跳到后面的公路。所以就符合连续子向量的求和问题。

    可初始化累加数组cum,使得cum[i]=x[0]+…+x[i],

    对于收费站i和j,cum[j] - cum[i-1]就表示在i和j内行驶的路段费用,而且仅仅占用 cum[n]的线性空间。


    区间赋值问题

    对数组array[0…n-1]初始化为全0后,运行n次运算:for i = [l,u] {x[i] += v;},当中l,u,v是每次运算的參数,0<=l<=u<=n-1。直接用这个伪码须要O(n2)的时间,请给出更快的算法。

    初始化y[0,…,n-1]为全0,对每一个操作令y[l]+=v和y[u+1]-=v。则结束时x[i]=sigma{k=0 to i}(y[k])。正确性:仅仅需证明每一次运行完操作之后该性质保持不变就可以。注意这里的y[i]表示的意义


    二维连续子数组和

    二维数组连续的二维子数组的和怎么求,肯定是一个矩形,我们要遍历吗???
    遍历的话预计复杂度扛不住吧。。怎样遍历也是一个问题。

    这个时候我们能够把每一行看成是一个元素,这样就变成了一个纵向的一维数组了。

    这样对一维数组的遍历是和刚才一样的。而对于每一行我们遍历也是和一维是一样的。编码试一试

     //求二维数组的连续子数组之和的最大值
      int MaxSum(int (*array)[N])
      {
          int i,j;
          int MaxSum=-INFINITY;//初始化
          int imin,imax,jmin,jmax;
          for(imin=1;imin<=N;imin++)
      {
              for(imax=imin;imax<=N;imax++)//当成是遍历纵向的一维
      {
                  for(jmin=1;jmin<=M;jmin++)
      {
                      for(jmax=jmin;jmax<=M;jmax++)//当成是遍历横向的一维
                              MaxSum=MaxNum(MaxSum,PartSum(imin,jmin,imax,jmax));
                  }
      }
      }            
          return MaxSum;
      }
      
    

    时间复杂度(N^2M^2O(PartSum)),怎样求部分和PartSum呢?假设这个还是要遍历求的话,复杂度真是不敢看了。。
    求一维的时候我们求Sum[i-j]非常好求,但是求二维的时候就变成了四个坐标了,不敢遍历求和了。我们能够先求部分和,把他当作已知的,这个时候遍历求的时候复杂度就是O(1)。
    怎样求?我们定义一个部分和数组PartSum,当中PartSum[i][[j]代表了下标(0,0),(0,j),(i,0),(i,j)包围的区间的和。

    而此时下标(imin,jmin),(imin,jmax),(imax,jmin),(imax,jmax)包围的区间和就等于

    PartSum[imax][[jmax]-PartSum[imin-1][[jmax]-PartSum[imax][[jmin-1]+PartSum[imin-1][[jmin-1]。
    

    这就是我们要求的PartSum(imin,jmin,imax,jmax),接下来就是求PartSum数组了。怎样求呢?
    对于每个PartSum[i][[j]都不是孤立的,都是和其它的有关系的。我们要找出这个关系式

    PartSum[i][[j]=PartSum[i-1][[j]+PartSum[i][[j-1]-PartSum[i-1][[j-1]+array[i][j]。
    

    这样求能够求出所有的PartSum[i][[j],但是我们不要忽略了一点,PartSum[0][[0]=?对于边界值我们要处理好,并且下标要从1開始。对于PartSum[i][[0]和PartSum[0][[j]都要初始化0,并且array[i][j]的下标也是要-1,由于数组的下标是从0開始的。这是一个办法,只是我们也能够单独求PartSum[i][[0]和PartSum[0][[j]的值,连续相加就可以,然后再求其它的也是能够的,空间复杂度也是一样。但是在4重遍历的时候对于PartSum[i][[0]和PartSum[0][[j]我们还是要另外处理,这就比較麻烦了。我们还是用预处理的方法来编码吧。。

        int PartSum[N+1][M+1];
          int i,j;
          for(i=0;i<=N;i++)
              PartSum[i][0]=0;
          for(j=0;j<=M;j++)
              PartSum[0][j]=0;
          for(i=1;i<=N;i++)
              for(j=1;j<=M;j++)
              PartSum[i][j]=PartSum[i-1][j]+PartSum[i][j-1]-PartSum[i-1][j-1]+array[i-1][j-1];
    

    OK,求得部分和之后我们就開始完好我们的编码了。记住一点,下标(imin,jmin),(imin,jmax),(imax,jmin),(imax,jmax)包围的区间和等于
    PartSum[imax][[jmax]-PartSum[imin-1][[jmax]-PartSum[imax][[jmin-1]+PartSum[imin-1][[jmin-1]。
    编码開始:

    //求二维数组的连续子数组之和的最大值
    int MaxSum(int (*array)[N])
    {
        int PartSum[N+1][M+1];
        int i,j;
        for(i=0;i<=N;i++)
            PartSum[i][0]=0;
        for(j=0;j<=M;j++)
            PartSum[0][j]=0;
        for(i=1;i<=N;i++)
            for(j=1;j<=M;j++)
                PartSum[i][j]=PartSum[i-1][j]+PartSum[i][j-1]-PartSum[i-1][j-1]+array[i-1][j-1];
        int MaxSum=-INFINITY;//初始化
        int imin,imax,jmin,jmax;
        for(imin=1;imin<=N;imin++)
            for(imax=imin;imax<=N;imax++)
                for(jmin=1;jmin<=M;jmin++)
                    for(jmax=jmin;jmax<=M;jmax++)
                            MaxSum=MaxNum(MaxSum,PartSum[imax][jmax]-PartSum[imin-1][jmax]-PartSum[imax][jmin-1]+PartSum[imin-1][jmin-1]);
    
        return MaxSum;
    }
    

    时间复杂度是O(N^2*M^2),有点坑啊。想一想一维的时候我们用DP来做,这个也能够吗?能够的。我们把每一列看成一个元素。这样对于遍历的行区间,我们就能够当成一维来做。

    对于imin和imax之间的每一列,就相当于一维的一个元素。

    如果这个一维数组是BC,则BC[j]=array[imin][j]+….+array[imax][j],问题就变成了求BC数组的连续子数组之和的最大值了。而依据刚才求的部分和,我们能够知道对于imin行和imax行之间的区间第j列的值是

    BC(PartSum,imin,imax,j)=PartSum[imax][j]-PartSum[imin-1][j]-PartSum[imax][j-1]+PartSum[imin-1][j-1].(此时BC是一个函数)
    OK,编码開始

    //求二维数组的连续子数组之和的最大值
    int MaxSum(int (*array)[N])
    {
        int PartSum[N+1][M+1];
        int i,j;
        for(i=0;i<=N;i++)
            PartSum[i][0]=0;
        for(j=0;j<=M;j++)
            PartSum[0][j]=0;
        for(i=1;i<=N;i++)
            for(j=1;j<=M;j++)
                PartSum[i][j]=PartSum[i-1][j]+PartSum[i][j-1]-PartSum[i-1][j-1]+array[i-1][j-1];
        int MaxSum=-INFINITY;
        int Start,All;
        int imin,imax;
        for(imin=1;imin<=N;imin++)
        {
            for(imax=imin;imax<=N;imax++)
            {
                Start=BC(PartSum,imin,imax,M);
                All=BC(PartSum,imin,imax,M);
                for(j=M-1;j>=1;j--)
                {
                    if(Start>0)
                        Start+=BC(PartSum,imin,imax,j);
                    else
                        Start=BC(PartSum,imin,imax,j);
                    if(Start>All)
                        All=Start;
                }
                if(All>MaxSum)
                    MaxSum=All;
            }
        }
        return MaxSum;
    }
    
    int BC(int (*PartSum)[N+1],int imin,int imax,int j) //imin--imax第j列的和
    {
        int value;
        value=PartSum[imax][j]-PartSum[imin-1][j]-PartSum[imax][j-1]+PartSum[imin-1][j-1];
        return value;
    }
    

    时间辅助度降到O(NMmin(M,N)),差点儿相同O(N^3)吧。


    參考资料:

    posted @ 2014-11-09 12:46  blfshiye  阅读(298)  评论(0编辑  收藏  举报