Bootstrap

连续子串的最大值(经典的DP问题)

【问题描述】 
在长度为N的整形数组中,求连续子串的和的最大值,要求复杂度为O(N)。
例如:1 2 3 -1 -20 100 34,结果为134。


【分析】 
[思路一] 
自己最初的想法,利用回溯的思想,从下标为0的元素开始遍历,用former保存当前最大的连续子集的和,当sum小于0的时候开始回溯,即删除这个元素,删除到已遍历数组最右边的那个正整数为止(用下标j标识)。事后觉得这个算法比较复杂,因为要维护下标j的指向,以及还要单独求出数组中的最大值,并最后与former进行比较。因此这个算法只是实现了功能,此算法的效率和代码量都不好。

  1. #include <cstdio>  
  2. #include <cassert>  
  3. int MaxAdjVal(int a[], int len)  
  4. {  
  5.     int sum=0, maxval=a[0], former=0;  
  6.     assert(len);// check  
  7.     for (int i=0,j=0,i_tmp,flag=true; i<len; ++i)  
  8.     {  
  9.         maxval<a[i] ? maxval=a[i] : NULL;// save biggest value  
  10.         if (a[i]<0 && flag)  
  11.         {  
  12.             j=i-1;// index of last positive number  
  13.             flag=false;  
  14.         }  
  15.         a[i]>0 ? flag=true : NULL;  
  16.         sum+=a[i];  
  17.         if (sum<=0)  
  18.         {  
  19.             sum-=a[i];// get rid of this element  
  20.             i_tmp=i;  
  21.             while (--i_tmp>j && sum>0)  
  22.             {  
  23.                 sum-=a[i_tmp];  
  24.             }  
  25.             if (former<sum)  
  26.             {  
  27.                 former=sum;  
  28.             }                 
  29.             sum=0;// reset  
  30.         }  
  31.     }  
  32.     return maxval>0 ? (sum>former? sum:former) : maxval;  
  33. }  
  34. int main()  
  35. {  
  36.     int a[100]={0}, i=0, res=0;  
  37.     printf("Enter some numbers (Ctrl+z to end):/n");  
  38.     while((1==scanf("%d",&a[i])) && ++i<100);  
  39.     rewind(stdin);  
  40.     res=MaxAdjVal(a,i);  
  41.     printf("%d/n",res);  
  42.     return 0;  
  43. }  

 

[思路二] 
思路二对算法进行了简化,不采用回溯,只用maxsum保存当前最大的连续子集的和,如果数组全为负数,找一个最大值就可以了。注意当cursum小于0的时候,要将cursum置为0。
一个无序的数组,找出相邻的任意个元素,使得其和最大。要得O(n)的复杂度,顺序读取,任意步骤都是当前读取的序列判断最优解,此谓联机算法。(参考:http://blog.csdn.net/hairetz/archive/2009/10/15/4676994.aspx )
用vector可以描述如下:

  1. int maxsubsum(const vector<int>& vec)  
  2. {  
  3.     int maxsum = 0, cursum = 0;  
  4.     for (int i = 0; i < vec.size(); ++i)   
  5.     {  
  6.         cursum += vec[i];  
  7.         if (cursum > maxsum)  
  8.             maxsum = cursum;  
  9.         else if (cursum < 0)  
  10.             cursum = 0;  
  11.     }  
  12.     return maxsum;  
  13. }  

用C语言描述如下:

  1. #include <cstdio>  
  2. #include <cassert>  
  3. int MaxAdjVal(int a[], int len)  
  4. {  
  5.     assert(len);// check  
  6.     int sum=0, max=a[0];  
  7.     for(int i=0; i<len; ++i)  
  8.     {  
  9.         sum+=a[i];  
  10.         if (sum>max)  
  11.             max=sum;  
  12.         if (sum<0)  
  13.             sum=0;  
  14.     }  
  15.     return max;  
  16. }  
  17. int main()  
  18. {  
  19.     int a[100]={0}, i=0, res=0;  
  20.     printf("Enter some numbers (Ctrl+z to end):/n");  
  21.     while((1==scanf("%d",&a[i])) && ++i<100);  
  22.     rewind(stdin);  
  23.     res=MaxAdjVal(a,i);  
  24.     printf("%d/n",res);  
  25.     return 0;  
  26. }  

【问题扩展】 
1. 问这样的子串有多少个。
2. 如果是首尾相连的,那么最大子串和是多少,有多少个。
3. 如果是首尾相连的,取两个不相交子串,那么最大子串和是多少。
4. 若存在多个最大子串和,则输出第一个最大子串和的起始和终止下标。

【参考】 
(1) 经典的DP问题。
请参考这道ACM题:http://acm.hdu.edu.cn/showproblem.php?pid=1003

  1. #include <iostream>  
  2. using namespace std;  
  3. void MaxSub(int *arr, int count, int &first, int &last, int ∑);  
  4. int main()  
  5. {  
  6.     int num, n, i, j;  
  7.     int *arr, first, last, sum;  
  8.     bool top = true;  
  9.     cin >> num;  
  10.     for(j=0;j<num;j++)  
  11.     {  
  12.         cin >> n;  
  13.         arr = new int[n];  
  14.         for(i=0;i<n;i++)  
  15.             cin >> arr[i];  
  16.         MaxSub(arr, n, first, last, sum);  
  17.         if(top)  
  18.             top = false;  
  19.         else  
  20.             cout << endl;  
  21.         cout << "Case " << j+1 << ":" << endl  
  22.             << sum << " " << first+1 << " "  
  23.             << last+1 << endl;  
  24.         delete [] arr;  
  25.     }  
  26.     return 0;  
  27. }  
  28. //求最大子序列之和,arr是原来的数组,count是数组的元素个数,  
  29. //first,last是所求子序列的第一个元素,最后一个元素的下标,sum是所求子序列的元素之和  
  30. void MaxSub(int *arr, int count, int &first, int &last, int ∑)  
  31. {  
  32.     //tFirst,tSum为临时的最大子序列的第一个元素与和  
  33.     int tFirst, tSum, i;  
  34.     //初始化  
  35.     first = tFirst = last = 0;  
  36.     sum = tSum = arr[0];  
  37.     //遍历该数组  
  38.     for(i=1;i<count;i++)  
  39.     {  
  40.         //如果tSum大于,则将它与相加,否则,arr[i]为子序列的第一个元素  
  41.         if(tSum >= 0)  
  42.             tSum += arr[i];  
  43.         else  
  44.         {  
  45.             tSum = arr[i];  
  46.             tFirst = i;  
  47.         }  
  48.         //如果临时的最大值大于实际的最大值,则更新子序列的first, last, sum  
  49.         if(sum < tSum)  
  50.         {  
  51.             sum = tSum;  
  52.             first = tFirst;  
  53.             last = i;  
  54.         }  
  55.     }  
  56. }  

(2) 一网友的几种解决方法:

  1. #include<iostream>  
  2. using namespace std;  
  3. #define NUM 10  
  4. template<class ElemType>  
  5. //普通2层循环时间复杂度 O(n*n)  
  6. void MaxSum(ElemType*p,int& beg,int& end,int& sum)  
  7. {  
  8.     ElemType count=0;  
  9.     sum=count;  
  10.     for(int i=0;i<NUM-1;i++)  
  11.     {  
  12.         count=0;  
  13.         for(int j=i;j<=NUM-1;j++)  
  14.         {  
  15.             count+=p[j];  
  16.             if (count>sum)   
  17.             {  
  18.                 beg=i;  
  19.                 end=j;  
  20.                 sum=count;  
  21.             }  
  22.         }  
  23.     }  
  24. }  
  25. //利用分治算法时间复杂度O(n)  
  26. template<class ElemType>  
  27. void MaxSum1(ElemType*p,int low,int high,int& beg,int& end,ElemType& sum)  
  28. {  
  29.     if(low==high)  
  30.     {  
  31.         sum=(p[low]>0)?p[low]:0;  
  32.     }  
  33.     else  
  34.     {  
  35.         int i;  
  36.         int mid=(low+high)/2;  
  37.         int leftStart,leftEnd; //左边区间最大子段的起止位置  
  38.         int rightStart,rightEnd; //右边区间最大子段的起止位置  
  39.         int leftSum=0; //左边区间的最大和  
  40.         int rightSum=0; //右边区间的最大和  
  41.         MaxSum1(p,low,mid,leftStart,leftEnd,leftSum);  
  42.         MaxSum1(p,mid+1,high,rightStart,rightEnd,rightSum);  
  43.         ElemType sumL=0; //跨越左右区间的左边部分的最大子段和  
  44.         ElemType curSum=0; //当前跨越左右区间的左边区间部分的子段和  
  45.         for(i=mid;i>=low;i--)  
  46.         { //处理左边区间  
  47.             curSum+=p[i];  
  48.             if (sumL<curSum)  
  49.             { //修改最大字段和和开始位置  
  50.                 sumL=curSum;  
  51.                 beg=i;  
  52.             }  
  53.         }  
  54.         ElemType sumR=0; //跨越左右区间的右边部分的最大子段和  
  55.         ElemType curSum1=0; //当前跨越左右区间的右边区间部分的子段和  
  56.         for(i=mid+1;i<=high;i++)  
  57.         { //处理右边区间  
  58.             curSum1+=p[i];  
  59.             if (sumR<curSum1)  
  60.             { //修改最大字段和和开始位置  
  61.                 sumR=curSum1;  
  62.                 end=i;  
  63.             }  
  64.         }  
  65.         sum=sumL+sumR; //跨越左右区间的最大字段和  
  66.         if (sum<=leftSum)   
  67.         {  
  68.             sum=leftSum;  
  69.             beg=leftStart;  
  70.             end=leftEnd;  
  71.         }  
  72.         if (sum<=rightSum)   
  73.         {  
  74.             sum=rightSum;  
  75.             beg=rightStart;  
  76.             end=rightEnd;  
  77.         }  
  78.     }  
  79. }  
  80. //利用动态规划时间复杂度也为O(n)  
  81. template<class ElemType>  
  82. void MaxSum2(ElemType*p,int& beg,int& end,ElemType& sum)  
  83. {  
  84.     sum=0;  
  85.     ElemType b=0; //上界为j的最大字段和  
  86.     int bStart,bEnd; //上界为j的最大字符段和的起止位置  
  87.     for(int j=0;j<NUM;j++)  
  88.     { //循环求最大字段和  
  89.         if (b>0)   
  90.         {  
  91.             b+=p[j];  
  92.             bEnd=j;  
  93.         }  
  94.         else  
  95.         {  
  96.             b=p[j];  
  97.             bStart=j;  
  98.             bEnd=j;  
  99.         }  
  100.         if (b>sum) //判断当前子段和,之前最大子段和的大小  
  101.         {  
  102.             sum=b;  
  103.             beg=bStart;  
  104.             end=bEnd;  
  105.         }  
  106.     }  
  107. }  
  108. void main()  
  109. {  
  110.     int p[NUM]={-4,2,4,2,-2,8,9,-4,3,-2};  
  111.     int beg,end,sum;  
  112.     beg=end=sum=0;  
  113.     // MaxSum<int>(p,beg,end,sum);  
  114.     // MaxSum1<int>(p,0,NUM-1,beg,end,sum);  
  115.     MaxSum2<int>(p,beg,end,sum);  
  116.     cout<<"开始下标为:"<<beg<<endl;  
  117.     cout<<"结束下标为:"<<end<<endl;  
  118.     cout<<"最大字段和为:"<<sum<<endl;  
  119. }  

(3) 参考《编程之美》
(4) 关于此问题的讨论。
http://topic.csdn.net/u/20100929/09/b9c84138-b5d2-4cfa-bcd4-140ffedfbc11.html?seed=1620195128&r=68870831#r_68870831

;