經典算法--求最大子序列和

本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/yaoxy/archive/2009/06/23/4289254.aspx

比較經典的算法問題,能夠很好的體現動態規劃的實現,以一點“畫龍點睛” 大大精簡了算法複雜度,且實現簡單。本文中實現了4種:

一般 maxSubSequenceSum0  O(n^3)

簡單優化過的算法 maxSubSequenceSum1  O(n^2)

分治法優化的算法 maxSubSequenceSum2  O(n*log(n))

動態規劃的算法 maxSubSequenceSum3  O(n)

#include <math.h>

#include "mymath.h"

/*
 * 計算序列的某段子序列的和,maxSubSequenceSum0使用
 */
static int subSequenceSum(int a[], int left, int right)
{
    int i, sum = 0;
    for (i = left; i <= right; i++)
    {
        sum = sum + a[i];
    }
    return sum;
}

/*
 * 三層遍歷求子序列和的最大值,算法複雜度O(n^3)
 */
int maxSubSequenceSum0(int a[], int len)
{
    int i, j;
    int curSum; /* 當前序列和 */
    int maxSum; /* 最大序列和 */

    /* 初始化最大子序列和爲序列第一個元素 */
    maxSum = a[0];

    /* 第一層循環定義子序列起始位置 */
    for (i = 0; i < len; i++)
    {
        /* 起始位置爲i,初始化當前和爲0 */
        curSum = 0;

        /* 第二層循環定義子序列結束位置 */
        for (j = i; j < len; j++)
        {
            /* 第三層循環在函數sumSubseqence中,計算子序列和 */
            curSum = subSequenceSum(a, i, j);

            /* 與最大子序列和比較,更新最大子序列和 */
            if (curSum > maxSum)
            {
                maxSum = curSum;
            }
        }
    }
    return maxSum;
}

/*
 * 雙層遍歷求子序列和的最大值,算法複雜度O(n^2)
 */
int maxSubSequenceSum1(int a[], int len)
{
    int i, j;
    int curSum; /* 當前序列和 */
    int maxSum; /* 最大序列和 */

    /* 初始化最大子序列和爲序列第一個元素 */
    maxSum = a[0];

    /* 外層循環定義子序列起始位置 */
    for (i = 0; i < len; i++)
    {
        /* 起始位置爲i,初始化當前和爲0 */
        curSum = 0;

        /* 內層循環定義子序列結束位置 */
        for (j = i; j < len; j++)
        {
            /* 計算子序列和,並與最大子序列和比較,更新最大子序列和 */
            curSum = curSum + a[j];

            /* 與最大子序列和比較,更新最大子序列和 */
            if (curSum > maxSum)
            {
                maxSum = curSum;
            }
        }
    }
    return maxSum;
}

/*
 * 某段字序列中,含左邊界元素的字序列和中的最大值,_maxSubSequenceSum2中使用
 */
static int _maxLeftBoderSubSequenceSum(int a[], int left, int right)
{
    int i;
    int sum = 0;
    int maxSum = a[left];
    for (i = left; i <= right; i++)
    {
        sum += a[i];
        if (sum > maxSum)
        {
            maxSum = sum;
        }
    }
    return maxSum;
}

/*
 * 某段字序列中,含右邊界元素的字序列和中的最大值,_maxSubSequenceSum2中使用
 */
static int _maxRightBoderSubSequenceSum(int a[], int left, int right)
{
    int i;
    int sum = 0;
    int maxSum = a[right];
    for (i = right; i >= left; i--)
    {
        sum += a[i];
        if (sum > maxSum)
        {
            maxSum = sum;
        }
    }
    return maxSum;
}

/*
 * 求序列某段子序列中子序列和最大值
 */
static int _maxSubSequenceSum2(int a[], int left, int right)
{
    int center;
    int leftMaxSum;
    int rightMaxSum;
    int maxLeftBorderSum;
    int maxRightBorderSum;

    /* 遞歸終止條件 */
    if (left == right)
    {
        return a[left];
    }

    /* 分治法遞歸開始,取中點二分處理 */
    center = (left + right) >> 1; /* center = (left + right) / 2; */

    /* 遞歸求左右子序列段中最大子序列和 */
    leftMaxSum = _maxSubSequenceSum2(a, left, center);
    rightMaxSum = _maxSubSequenceSum2(a, center + 1, right);

    maxLeftBorderSum = _maxRightBoderSubSequenceSum(a, left, center);
    maxRightBorderSum = _maxLeftBoderSubSequenceSum(a, center + 1, right);

    /*
     * 二分後的最大值有三個:
     *    1、leftMaxSum,左段最大子序列和
     *    2、rightMaxSum,右段最大子序列和
     *    3、maxLeftBorderSum+maxRightBorderSum,左段最大含右邊界子序列和最大值和右段最大含左邊界子序列和最大值,二者之和
     * 這三者中的最大值即爲分段前的最大子序列和
     * 
     * 分治算法核心部分,解決分治後結果歸併問題,具體分析:
     *    這是對分段後的子序列的一種劃分,有三種,只需分別求出各種的最大值然後在三者之間取一個最大值即可:
     *       1、子序列全在左段,最大子序列和爲leftMaxSum
     *       2、子序列全在右段,最大子序列和爲rightMaxSum
     *       3、子序列跨左右段,最大字序列和爲maxLeftBorderSum+maxRightBorderSum
     */
    return tmax(leftMaxSum, rightMaxSum, maxLeftBorderSum+maxRightBorderSum);
}

/*
 * 分治法實現,算法複雜度O(n*log(n))
 * 分:使用二分法進行分段
 * 治:詳細算法見_maxSubSequenceSum2內描述,簡述爲:
 *    全段最大子序列爲以下三者中的最大值
 *       左段最大子序列和
 *       右段最大子序列和
 *       左段最大含右邊界子序列和最大值和右段最大含左邊界子序列和最大值之和
 */
int maxSubSequenceSum2(int a[], int len)
{
    return _maxSubSequenceSum2(a, 0, len - 1);
}

/*
 * 動態規劃實現,算法複雜度O(n)
 */
int maxSubSequenceSum3(int a[], int len)
{
    int i;
    int curSum; /* 當前序列和 */
    int maxSum; /* 最大序列和 */

    /* 初始化當前序列和爲0 */
    curSum = 0;

    /* 初始化最大子序列和爲序列第一個元素 */
    maxSum = a[0];

    /* 開始循環求子序列和 */
    for (i = 0; i < len; i++)
    {
        curSum = curSum + a[i];

        /* 與最大子序列和比較,更新最大子序列和 */
        if (curSum > maxSum)
        {
            maxSum = curSum;
        }

        /* 動態規劃部分,捨棄當前和爲負的子序列 */
        if (curSum < 0)
        {
            curSum = 0;
        }
    }
    return maxSum;
}

 

PS:這是最近的一次面試中的一道分析題目,給出了本文中第二種算法,要求進行優化;由於時間段,且對本問題在先前並沒多少了解,首先想到的是分治,很遺憾,歸併條件想的不是很充分。

功    能:   在一個整數序列中求一個子序列,該子序列的和最大  
 輸入說明:   首先輸入整數序列的長度 length        
       接着輸入該整數序列           
 輸出說明:   輸出子序列的起點和終點,並輸出該子序列的和       
 事    例:   (建議用管道測試)          
              輸入文件 data.txt  內容如下:       
              8
       12 -13 1 2 23 -14 55 -2
              
       輸出:
       The subsequence from 2 to 6,max sum is 67 (序列從1開始算)
 評 價:  這個題可以有幾種算法:o(n^3),o(n^2),o(nlogn),o(n)         
       本程序使用o(n),這種求法幾乎完美 !    
 語    言:   非標準 C  編譯環境:VC ++ 6.0             
 Author  :   江南孤峯  Time :2006--11--9     

#include <stdio.h>
#include <stdlib.h>

#include <limits.h>
#include <malloc.h>

int main(){
 int *ip;
 int j,length,max,sum;
 int start1 = 0 ,start2 = 0;
 
 printf("Please enter the array's length:");
 scanf("%d",&length);
 if((ip = (int*)malloc(length*sizeof(int)))==NULL){
  fprintf(stderr,"Malloc memory failed !");
  exit(1);
 }
 printf("Enter eath element:");
 for(j = 0; j < length ; j ++)
  scanf("%d",ip+j);

 max = INT_MIN;
 for(sum = j = 0; j < length; j ++){
  sum += *(ip+j);
  if(max < sum){
   start1 = start2;
   max = sum;
  }
  if(sum < 0){
   start2 = j+1;
   sum = 0;
  }
 }
 for(j = start1,sum = 0; sum != max; j ++)
  sum += *(ip+j);
 printf(""nThe subsequence from %d to %d,max sum is %d"n",start1,j-1,max);
 return 0;
}

// 下面是網友的題目,估計也是ACM題,和上面的題差不多,所以貼這裏算了

現的問題是如果求環形整數串的最大連續和子串呢? 

輸入數據

本題有多組輸入數據,你必須處理到EOF爲止

每組數據的第一行有一個整數n, (1<=n<=1000000).第2行有n個整數,每個整數都在[-100,100]的範圍內

輸出數據

每組數據輸出一個整數,表示環形整數串最大連續子串和。

輸入樣例


-2 3 0 1 -48 80 

1 3 

10 -2 -3 1 1


輸出樣例

82 

12

分析:環行的 數組其實可以看做將該數組寫兩次,不過爲了節約空間,在達到數組長度時我們可以繞到數組的起始,下面的代碼就是這樣。ACM的題目都強調速度,如果下面的程序不能通過,可以嘗試後面那個,空間換時間。這裏時間複雜度都是O(n^2)。

代碼一:

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>

#include <limits.h>

int main(){
 int *ip;
 int i,j,n,max,maxSave,sum;
 
 while(scanf("%d",&n) != EOF){
  if((ip = (int*)malloc(n*sizeof(int)))==NULL){
   fprintf(stderr,"Malloc memory failed !");
   exit(1);
  }
  for(j = 0; j < n ; j++)
   scanf("%d",ip+j);
  maxSave = INT_MIN;

  for(i = 0; i < n; i++){
   for(max = sum = *(ip+i),j = i+1;j != i; j++){
    if(j == n){
     j = -1;
     continue;
    }
    sum += *(ip+j);
    if(max < sum)
     max = sum;
    if(sum < 0)
     sum = 0;
   }
   if(maxSave < max)
    maxSave = max;
  }
  printf("%d"n",maxSave);
  free(ip);
 }
 return 0;
}

代碼二:

// 注意TC開不了這麼大的數組
#include <stdio.h>

#include <limits.h>

int a[2000000];
int main(){
 int i,j,n,max,maxSave,sum;
 
 while(scanf("%d",&n) != EOF){
  for(j = 0; j < n ; j++){
   scanf("%d",&a[j]);
   a[j+n] = a[j];
  }

 maxSave = INT_MIN;
  for(i = 0; i < n; i++){
   for(max = sum = 0,j = i;j < n+i; j++){
    sum += a[j];
    if(max < sum)
     max = sum;
    if(sum < 0)
     sum = 0;
   }
   if(maxSave < max)
    maxSave = max;
  }
  printf("%d"n",maxSave);
 }
 return 0;
}

 2007 ---- 8 ----- 28   更新[來自湖大ACM]

[解題報告]: 
作者:wation 
解題思路: 
首先考慮是否所有數據都不大於0,這種情況直接輸出最大數(不證明), 
然後,分別求出非環串的最大子段和和最小子段和以及和, 
結果等於max(最大子段和,和-最小子段和); 
證明: 
設串s="s1,s2,...,sn" 
構成最大子段和的串smax="si,si+1,...sk" 
構成最小子段和的串smin="sj,sj+1,...sf" 
首先證明smax和smin不存在公共子串。 
證1:假設存在ssub="st,...sp"即屬於smax,也屬於smin; 
首先假設ssub在smax中,必然存在smax-ssub<smax(smax-ssub表示smax中除掉ssub所構成的子串), 
推出ssub>0(ssub的所有元素之和大於0) 
同理假設ssub在smin中推出ssub<0; 
從而推出矛盾,所以smax和smin不存在公共子串。 
然後證明smax和smin要麼相鄰要麼中間存在一個和爲0的子串。 
證明2:要證上述結論只要證得在smax和smin不相鄰的情況下 
假設存在nsub="sk+1,...,sf-1",使得nsub=0; 
假設nsub>0,那麼smax+nsub>smax,根據求解方法,smax必將擴充到sf-1;所以nsub<=0 
同理推出nsub不能小於0; 
所以推出nsub=0; 
最後證明環串的最大子段和csmax=max(smax,s-smin); 
證3:假設存在一個子串vsmax>csmax; 
顯然vsmax是兩端兩個子串的合併,否則必然有vsmax<=smax 
設 vsmax="s1,...,sz"+"sx,...,sn" 
那麼sx一定>sf,因爲如果假設sx<sf,即smin和vsmax存在公共子串,1已經證明不存在了; 
再者sz一定小於si,如果sz>=si,根據求解方法vsmax必將繼續擴展直到sz=sk, 
因爲"sf,...,sx"=0(2已經證得),所以有vsmax=s-smin;與假設矛盾。 
a、假設csmax=smax,那麼vsmax>smax,即"s1,...,sz"+"sx,...,sn">"si,si+1,...sk", 
即s-smin-"sz+1,...,si-1"-smax>"si,si+1,...sk", 
即s-smin-"sz+1,...,si-1"-smax>smax既-"sz+1,...,si-1"-smax>smax-(s-smin)>0,因爲那麼根據求解思路smin應該 
="sz+1,...,sf",所以矛盾 
b、假設csmax=s-smin,那麼vsmax>s-smin,既s-smin-"sz+1,...,si-1"-smax>s-smin,即-"sz+1,...,si-1"-smax>0, 
同理得出矛盾。 
從證1,證2,證3得證結論環串最優解等於max(最大子段和,和-最小子段和); 
至於求非環串最大最小子段和可以用dp算法。

下面是我根據上面思路寫的代碼:

#include <iostream>
using namespace std;

int main(){
    int n,i,amax,amin,submax,submin,sum,t;
    for(;cin>>n;){
        cin>>t;
        sum = submin = amin = submax = amax = t;
        for(i=1; i<n; i++){
            cin>>t;
            sum += t;
            amax = amax>0 ? amax+t : t;
            if(amax > submax)
                submax = amax;
            amin = amin<0 ? amin+t : t;
            if(amin < submin)
                submin = amin;
        }
        if(sum-submin > submax && sum!=submin)
            cout<<(sum-submin)<<endl;
        else
            cout<<submax<<endl;
    }
    return 0; 
}
Language : GNU C++ , Judge Result: Time Limit Exceeded

#include <stdio.h>

int main(){
    int n,i,amax,amin,submax,submin,sum,t;
    for(;scanf("%d",&n)!=EOF;){
        scanf("%d",&t);
        sum = submin = amin = submax = amax = t;
        for(i=1; i<n; i++){
            scanf("%d",&t);
            sum += t;
            amax = amax>0 ? amax+t : t;
            if(amax > submax)
                submax = amax;
            amin = amin<0 ? amin+t : t;
            if(amin < submin)
                submin = amin;
        }
        if(sum-submin > submax && sum!=submin)
            printf("%d"n",sum-submin);
        else
            printf("%d"n",submax);
    }
    return 0; 
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章