最大連續子序列——動態規劃經典問題
前幾天在牛客網上看到一道關於動態規劃的題目,完全不知如何著手。所以就去學習了一下動態規劃,參考網上的解析,跌跌撞撞把一道杭電上的最大連續子序列敲了出來。
題目來源: 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]);
}
}
}
參考部落格: