前几天在牛客网上看到一道关于动态规划的题目,完全不知如何着手。所以就去学习了一下动态规划,参考网上的解析,跌跌撞撞把一道杭电上的最大连续子序列敲了出来。
题目来源: 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]);
}
}
}
参考博客: