最大连续子序和
最大连续子序列算是一个很经典的问题,它有多种算法可以实现,现在学习一下
算法一(暴力)O(N^3)
就是遍历完所有的子串,求出其中的最大值并保存。复杂度为O(N^3),臃肿且容易超时
代码如下:
1 #include <iostream> 2 3 using namespace std; 4 int a[1000]; 5 int len; 6 7 int site(int a[],int len)//复杂度为O(N^3)遍历完每个子序列 8 { 9 int maxs=0; 10 for(int i=0;i<len;i++)//子序列起始处 11 { 12 for(int j=i;j<len;j++)//子序列结束处 13 { 14 int sum=0; 15 for(int k=i;k<j;k++)//遍历子序列 16 { 17 sum+=a[k]; 18 } 19 if(sum>maxs) 20 { 21 maxs=sum; 22 } 23 } 24 } 25 return maxs; 26 } 27 28 void showa(int a[],int len)//用于查看数组内元素 29 { 30 for(int i=0;i<len;i++) 31 { 32 cout<<a[i]<<' '; 33 } 34 cout<<endl; 35 } 36 37 int main() 38 { 39 while(cin>>len&&len)//首先输入一个M代表有M个数据 40 { 41 for(int i=0;i<len;i++) 42 { 43 cin>>a[i]; 44 } 45 showa(a,len); 46 cout<<site(a,len)<<endl; 47 } 48 return 0; 49 }
其中site函数为计算的关键,我们将数组假设为一条线长,其计算顺序为:
总长: ------------------------------
计算中:
第一次: i----j 以i为0时,以j为终点,k为起点,逼近j
第二次: k--j
…………: k-j
即设立k为起点,j为终点,k不断逼近 即得每个子序列长度,再判断,但该算法中间重复计算了很多次相同的子序列长,因而我们可以优化一下
算法二(略微优化) 0(N^2)
代码如下:
#include <iostream> #include <algorithm> using namespace std; int a[1000]; int len; int site2(int a[],int len) { int maxs=0; for(int i=0;i<len;i++)//起点 { int sum=0; for(int j=i;j<len;j++)//终点 { sum+=a[j]; if(maxs<sum) { maxs=sum; } } } return maxs; } void showa(int a[],int len) { for(int i=0;i<len;i++) { cout<<a[i]<<' '; } cout<<endl; } int main() { while(cin>>len&&len) { for(int i=0;i<len;i++) { cin>>a[i]; } showa(a,len); cout<<site2(a,len)<<endl; } return 0; }
上述算法则去除了一些重复计算的长度,下面我们再看一下它是如何计算子序列长度的
总长: ------------------------------
计算中:
第一次: j- j=i不断变化j的初始位置
第二次: j-- 而且每次都与最长子序和比较,更新
…………: j------------------------
该算法虽然优化了重复计算的序列长,但其仍然复杂度仍为0(N^2),因而我们考虑其他算法进行计算
算法三(分治)O(N*log(N))
代码如下:
1 #include <iostream> 2 #include <algorithm> 3 using namespace std; 4 int a[1000]; 5 int len; 6 int site3(int a[],int left,int right) 7 { 8 if(left==right)//初始到每一个点 9 { 10 return a[left]; 11 /* 12 if(a[left]>0) 13 { 14 return a[left]; 15 } 16 else return 0; 17 */ 18 } 19 int center=(left+right)/2; 20 int maxlefts=site3(a,left,center); 21 int maxrights=site3(a,center+1,right);//递归到每一个点,开始执行下面的计算 point1 22 23 int leftsum=0,maxls=0; 24 for(int i=center;i>=left;i--) 25 { 26 leftsum+=a[i]; 27 if(leftsum>maxls) 28 maxls=leftsum; 29 } 30 31 int rightsum=0,maxrs=0; 32 for(int i=center+1;i<=right;i++) 33 { 34 rightsum+=a[i]; 35 if(rightsum>maxrs) 36 maxrs=rightsum; 37 } 38 39 int maxssum=maxls+maxrs;//计算中子序最大值 40 41 int maxs=0; 42 maxs=max(maxlefts,maxrights); 43 maxs=max(maxs,maxssum);//比较得到三个子序列中最大子序和 44 45 return maxs; 46 } 47 48 void showa(int a[],int len) 49 { 50 for(int i=0;i<len;i++) 51 { 52 cout<<a[i]<<' '; 53 } 54 cout<<endl; 55 } 56 57 int main() 58 { 59 while(cin>>len&&len) 60 { 61 for(int i=0;i<len;i++) 62 { 63 cin>>a[i]; 64 } 65 showa(a,len); 66 67 cout<<site3(a,0,len-1)<<endl; 68 69 } 70 return 0; 71 }
分治法 每次计算三个序列最大子序和,进行比较,取其中最大值,即得到分段前的最大子序和复杂度为O(N*log(N))
= =分治法的主要思想就是将大问题分解成同一解法的小问题得到解,再不断合并解,从而得到最终的答案。
首先是如上面point1处,先分解到每个点
例:10
1 1 1 -5 1 2 -5 2 2 -5
我们不断分解将得到一棵形似树的数列:
1: (1 1 1 -5 1 2 -5 2 2 -5)
2: (1 1 1 -5 1) (2 -5 2 2 -5)
3: (1 1 1) (-5 1) (2 -5 2) (2 -5)
4: (1 1) (1) (-5) (1) (2 -5) (2) (2) (-5)
5: (1) (1) (2) (-5)
再求每个子数列的最大子序和,合并,得到父数列的最大子序和,不断合并,得到整个数列的最大子序和
下面还有一种更优的算法-即动态规划,但现在没时间了,下机了(╯‵□′)╯︵┻━┻回去再弄,好的现在我弄完了yeah
算法四 动态规划O(N)
版本一:
1 #include <iostream> 2 #include <algorithm> 3 using namespace std; 4 int a[1000]; 5 int len; 6 7 int site4(int a[],int len) 8 { 9 int f[10000]={0}; 10 for(int i=0;i<len;i++) 11 { 12 if(f[i-1]>0) 13 f[i]=f[i-1]+a[i]; 14 else 15 f[i]=a[i]; 16 } 17 int maxs=0; 18 for(int i=0;i<len;i++) 19 { 20 if(maxs<f[i]) 21 maxs=f[i]; 22 } 23 return maxs; 24 } 25 26 void showa(int a[],int len) 27 { 28 for(int i=0;i<len;i++) 29 { 30 cout<<a[i]<<' '; 31 } 32 cout<<endl; 33 } 34 35 int main() 36 { 37 while(cin>>len&&len) 38 { 39 for(int i=0;i<len;i++) 40 { 41 cin>>a[i]; 42 } 43 showa(a,len); 44 //cout<<site(a,len)<<endl; 45 cout<<site4(a,len)<<endl; 46 //cout<<g_site(a,len)<<endl; 47 } 48 return 0; 49 }
我们将每次加法分为阶段性的,那么有:
当前阶段的子段和=上一阶段的子段和+当前元素
即:s[i]=s[i-1]+a[i]
而大家也都应知道这么一个道理:一个数加上一个负数,那么和必然小于原来这个数,
因而我们摒弃掉负数和,从当前点开始新的累加
从而得到最大子段和状态转移方程:
if(s[i-1]>0)
s[i]=s[i-1]+a[i];
else
s[i]=a[i];
最后我们计算完毕后找出其中最大值,即为最大子段和。
现在,我们想一想,其实我们可以将找到最大值这个步骤放入计算的循环内
因而,
版本二:
#include <iostream> #include <algorithm> using namespace std; int a[1000]; int len; int site5(int a[],int len) { int maxs=0,sum=0; for(int i=0;i<len;i++) { sum+=a[i]; if(maxs<sum) maxs=sum; if(sum<0) sum=0; } return maxs; } void showa(int a[],int len) { for(int i=0;i<len;i++) { cout<<a[i]<<' '; } cout<<endl; } int main() { while(cin>>len&&len) { for(int i=0;i<len;i++) { cin>>a[i]; } showa(a,len); //cout<<site(a,len)<<endl; cout<<site5(a,len)<<endl; //cout<<g_site(a,len)<<endl; } return 0; }
相比于上面的数组记录状态,我们只用一个sum来记录状态,当为负时,我们将状态归零,从新开始计算,同时,我们也将寻求最大值放入了循环。
但这样其实舍弃了一点东西……当有多个子段和值相同时,我们仅记录了一个状态,如果要寻找其他的我们就不是很方便了。
以上,为本次对最大子段和的学习。
2016.4.22