1. 程式人生 > >最大連續子序列——動態規劃經典問題

最大連續子序列——動態規劃經典問題

前幾天在牛客網上看到一道關於動態規劃的題目,完全不知如何著手。所以就去學習了一下動態規劃,參考網上的解析,跌跌撞撞把一道杭電上的最大連續子序列敲了出來。

題目來源: http://acm.hdu.edu.cn/showproblem.php?pid=1231

給定K個整數的序列{ N1, N2, ..., NK },其任意連續子序列可表示為{ Ni, Ni+1, ..., Nj },其中 1 <= i <= j <= K。最大連續子序列是所有連續子序列中元素和最大的一個, 例如給定序列{ -2, 11, -4, 13, -5, -2 },其最大連續子序列為{ 11, -4, 13 },最大和 
為20。 在今年的資料結構考卷中,要求編寫程式得到最大和,現在增加一個要求,即還需要輸出該 子序列的第一個和最後一個元素。

 分析:

1.我第一次採用的是暴力迴圈的方法,利用兩層for迴圈,遍歷陣列以每一個元素開頭的所有子序列。雖然可以算出結果,但是超時了,時間複雜度為O(n^2)。

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        int n;
        Scanner scanner=new Scanner(System.in);
        while ((n=scanner.nextInt())!=0){
            int[] a=new int[n];
            for(int i=0;i<n;i++){
                a[i]=scanner.nextInt();
            }
            int x=0,y=0,sum,max=Integer.MIN_VALUE;
            for (int i=0;i<n;i++){
                sum=0;
                for (int j=i;j<n;j++){
                    sum+=a[j];
                    if (sum>max){
                        x=i;
                        y=j;
                        max=sum;
                    }
                }
            }
            if (max<0){
                x=0;
                y=n-1;
                max=0;
            }
            System.out.println(max+" "+a[x]+" "+a[y]);
        }
    }
}

2.第二次採用的是分治法,結果還是超時了。

演算法的主要思想如下:

首先,我們可以把整個序列平均分成左右兩部分,答案則會在以下三種情況中:
1、所求序列完全包含在左半部分的序列中。
2、所求序列完全包含在右半部分的序列中。
3、所求序列剛好橫跨分割點,即左右序列各佔一部分。

import java.util.Scanner;

public class Main {
    static int x,y,sum=0,max=Integer.MIN_VALUE;
    public static void main(String[] args) {
        int n;
        Scanner scanner=new Scanner(System.in);
        while ((n=scanner.nextInt())!=0){
            x=y=0;
            int[] a=new int[n];
            for(int i=0;i<n;i++){
                a[i]=scanner.nextInt();
            }
            max=a[0];
            System.out.println(findMax(a,0,n-1)+" "+a[x]+" "+a[y]);
        }
    }

    //遞迴方法
    public static int findMax(int a[],int left,int right){
        //遞迴邊界值
        if(left==right){
            int sum=a[left];
            if (sum>max){
                x=y=left;
                max=sum;
            }else if(sum==max&&left<x){
                    x=y=left;
            }
            return sum;
        }


        int mid=(left+right)/2;
        //找出左邊部分的最大值
        int leftMax=findMax(a,left,mid);
        //找出右邊部分的最大值
        int rightMax=findMax(a,mid+1,right);

        //找出中間部分的最大值,不需要遞迴
        int mLeftMax=Integer.MAX_VALUE;int mLeftSum=0;
        int start=mid;
        for(int i=mid;i>=0;i--){
            mLeftSum+=a[i];
            if(mLeftSum>mLeftMax){
                mLeftMax=mLeftSum;
                start=i;
            }
        }
        int mRightMax=Integer.MAX_VALUE;int mRightSum=0;
        int end=mid+1;
        for(int i=mid+1;i<=right;i++){
            mRightSum+=a[i];
            if(mRightSum>mRightMax){
                mRightMax=mRightSum;
                end=i;
            }
        }

        int mMax=mLeftMax+mRightMax;
        if (mMax>max){
            max=mMax;
            x=start;
            y=end;
        }else if(mMax==max&&start<x){
                x=start;
                y=end;
        }
        if(max<0){
            x=0;y=a.length-1;
            max=0;
        }

        return max;


    }
}

這裡又牽扯到遞迴了,遞迴一時半會兒我也沒法弄的很清晰。可以參考一文讀懂遞迴演算法,這篇部落格還挺簡潔通俗的。

回到上面的分治法,它的時間複雜度是多少呢?它的效率有比第一種方法高麼?

假設資料數量為n,它的時間複雜度假設為T(n),O(n)為遍歷中間部分的時間複雜度,那麼:

T(n)=2T(n/2)+O(n)

      =4T(n/4)+2O(n)

      =8T(n/8)+3O(n)

      ...

      =nT(n/n)+log(n)O(n)

      =n+O(nlog(n))

省略n,最終T(n)=O(nlog(n))。所以,它的時間效率稍微高於第一種方法。

3.第三次採用的是動態規劃,時間複雜度為O(n)。

演算法主要思想:遍歷該序列每一個元素,計算以它結尾的子序列的最大值,然後找出最大的那個最大值。

import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        Scanner scanner=new Scanner(System.in);
        int n;
        while ((n = scanner.nextInt())!=0) {
            int[] a=new int[n];
          
            int s=0,L=0,R=0,l=0;int max=Integer.MIN_VALUE;
            for(int i=0;i<n;i++){
                a[i]=scanner.nextInt();
                //以a[i]元素結尾的子序列必須包含a[i]
                s+=a[i];
                if(s>max){
                    max=s;L=l;R=i;
                }
                if(s<0){
                    //在這一輪迴圈中,l表示的是以a[i+1]開始的子序列的左下標
                    s=0;l=i+1;
                }
            }
            if(max<0){max=0;L=0;R=n-1;}
            System.out.println(max+" "+a[L]+" "+a[R]);
        }
    }
}

參考部落格:

經典演算法問題 - 最大連續子數列和

幾個經典的動態規劃問題