連續子序列最大和問題的四種經典解答
問題描述
給定(可能是負的)整數序列A1, A2,...,AN, 尋找(並標識)使Sum(Ak)(k >=i, k <= j)的值最大的序列。如果所有的整數都是負的,那麼連續子序列的最大和是那個最大的負數項。最好給出給出最大和連續子序列!!
1 暴力破解法
這個問題有一個最簡單直接的窮舉解決法。我們看問題,既然要求裡面最大的連續子序列。那麼所有的連續子序列將由哪些組成呢?以陣列的第一個元素為例,連續子序列必須是至少包含元素A1,也可能包含從A1到A2...以及從A1到AN。這樣就有N種可能。後面的元素也按照這樣類似的辦法,以該元素開始,包含該元素的單元素陣列,兩個元素陣列...直到包含陣列末尾的陣列。
結果:#include <iostream> #include <cstdlib> #include <vector> using namespace std; int main() { vector<int> array; for(int i=0;i<20;i++) { int j = 50-random()%100; //-49~50 array.push_back(j); } for(vector<int>::iterator it=array.begin();it!=array.end();++it) { cout<<*it<<" "; } cout<<endl; int maxsum=-50; //注意初始化 int low=0; int high=0; for(int i=0;i<array.size();i++) { int sum=0; for(int j=i;j<array.size();j++) { sum+=array[j]; if(sum>maxsum) { maxsum=sum; low=i; high=j; } } } cout<<"subarray:"<<array[low]<<"~"<<array[high]<<endl; cout<<"maxsun="<<maxsum<<endl; return 0; }
時間複雜度分析:兩重迴圈遍歷,複雜度為O(n^2)
2 分治策略(遞迴法)
對這個問題,有一個相對複雜的O(NlogN)的解法,就是使用遞迴。如果要是求出序列的位置的話,這將是最好的演算法了(因為我們後面還會有個O(N)的演算法,但是不能求出最大子序列的位置)。該方法我們採用“分治策略”(divide-and-conquer)。
在我們例子中,最大子序列可能在三個地方出現,或者在左半部,或者在右半部,或者跨越輸入資料的中部而佔據左右兩部分。前兩種情況遞迴求解,第三種情況的最大和可以通過求出前半部分最大和(包含前半部分最後一個元素)以及後半部分最大和(包含後半部分的第一個元素)相加而得到。
#include <iostream>
#include <cstdlib>
#include <vector>
using namespace std;
/*
* find a subarray crossing the mid with the biggist sum
*/
int find_max_crossing_subarray(vector<int> array,int low, int mid, int high)
{
int left_maxsum=-50; //for sum
int right_maxsum=-50; //for sum
int sum =0;
for(int i= mid;i>= low;--i)
{
sum += array[i];
if(sum > left_maxsum)
{
left_maxsum=sum;
}
}
sum=0;
for(int i=mid+1;i<=high;++i)
{
sum += array[i];
if(sum > right_maxsum)
{
right_maxsum=sum;
}
}
return(left_maxsum+right_maxsum);
//cout<<"maxsum_crossing_mid"<<left_maxsum+right_maxsum<<endl;
}
int max(int a, int b, int c)
{
if(a>=b && a>=c)
return a ;
else if(b>=a && b>=c)
return b;
else
return c;
}
/*
* find a subarray on the left or right side of mid
*/
int find_max_subarray(vector<int> array, int low, int high)
{
if(low==high)
{
return array[low];
}
int mid = int((low+high)/2);
int low_maxsum = find_max_subarray(array, low, mid);
int high_maxsum = find_max_subarray(array, mid+1, high);
int crossing_mid_maxsum = find_max_crossing_subarray(array, low, mid, high);
return max(low_maxsum, high_maxsum, crossing_mid_maxsum);
}
int main()
{
vector<int> array;
for(int i=0;i<10;i++)
{
int j = 50-rand()%100; //-49~50
array.push_back(j);
}
for(vector<int>::iterator it=array.begin();it!=array.end();++it)
{
cout<<*it<<" ";
}
cout<<endl;
int maxsum =find_max_subarray(array, 0, array.size()-1);
cout<<"the maxsum="<<maxsum<<endl;
//cout<<"range from"<<array[a[1]]<<"to"<<array[a[2]]<<endl;
return 0;
}
結果:
3 增量演算法(插入演算法)
假設已知A[1~N]的最大順序子序列和,那麼對於A[1~N+1]序列,增加一個項,最大順序子序列和會在哪裡產生呢?(1)不變,A[N+1]項不影響(2)在包括A[N+1]項往前的子序列中產生。
#include <iostream>
#include <cstdlib>
#include <vector>
using namespace std;
int enlarge(vector<int> array,int maxsum, int i)
{
if(i>array.size() || i<=0)
return -1;
int sum=0;
for(int j=i;j>=0;--j)
sum += array[j];
if(sum>maxsum)
maxsum=sum;
return maxsum;
}
int find_maxsum(vector<int> array)
{
if(array.empty())
return -1;
int maxsum=array.at(0);
for(int i=1;i<=array.size()-1;++i)
{
int result = enlarge(array, maxsum, i);
if(result>maxsum && result != -1)
maxsum = result;
}
return maxsum;
}
int main()
{
vector<int> array;
for(int i=0;i<10;++i)
array.push_back(10-rand()%20);
for(vector<int>::iterator it= array.begin();it != array.end();++it)
cout<<*it<<" ";
cout<<endl;
int max = find_maxsum(array);
cout<<"maxsum ="<<max<<endl;
return 0;
}
結果:
複雜度 O(n^2)
4 最優解(線性複雜度 O(N))
參考:http://www.cnblogs.com/CCBB/archive/2009/04/25/1443455.html
//線性的演算法O(N)
long maxSubSum4(const vector<int>& a)
{
long maxSum = 0, thisSum = 0;
for (int j = 0; j < a.size(); j++)
{
thisSum += a[j];
if (thisSum > maxSum)
maxSum = thisSum;
else if (thisSum < 0)
thisSum = 0;
}
return maxSum;
}
很容易理解時間界O(N) 是正確的,但是要是弄明白為什麼正確就比較費力了。其實這個是演算法二的一個改進。分析的時候也是i代表當前序列的起點,j代表當前序列的終點。如果我們不需要知道最佳子序列的位置,那麼i就可以優化掉。
重點的一個思想是:如果a[i]是負數那麼它不可能代表最有序列的起點,因為任何包含a[i]的作為起點的子序列都可以通過用a[i+1]作為起點來改進。類似的有,任何的負的子序列不可能是最優子序列的字首。例如說,迴圈中我們檢測到從a[i]到a[j]的子序列是負數,那麼我們就可以推進i。關鍵的結論是我們不僅可以把i推進到i+1,而且我們實際可以把它一直推進到j+1。
舉例來說,令p是i+1到j之間的任何一個下標,由於前面假設了a[i]+…+a[j]是負數,則開始於下標p的任意子序列都不會大於在下標i並且包含從a[i]到a[p-1]的子序列對應的子序列(j是使得從下標i開始成為負數的第一個下標)。因此,把i推進到j+1是安全的,不會錯過最優解。注意的是:雖然,如果有以a[j]結尾的某序列和是負數就表明了這個序列中的任何一個數不可能是與a[j]後面的數形成的最大子序列的開頭,但是並不表明a[j]前面的某個序列就不是最大序列,也就是說不能確定最大子序列在a[j]前還是a[j]後,即最大子序列位置不能求出。但是能確保maxSum的值是當前最大的子序列和。這個演算法還有一個有點就是,它只對資料進行一次掃描,一旦a[j]被讀入處理就不需要再記憶。它是一個聯機演算法。
聯機演算法:在任意時刻演算法都能夠對它已讀入的資料給出當前資料的解。
常量空間線性時間的聯機演算法幾乎是完美的演算法。