2018-01-14 21:14:58
一、最大子段和問題
問題描述:給定n個整數(可能有負數)組成的序列a1,a2,...,an,求該序列的最大子段和。如果所有整數都是負數,那么定義其最大子段和為0。
方法一、最大子段和的簡單算法
顯然可以在O(n^2)的時間復雜度上完成這個問題。但是是否可以對算法進行優化呢?答案是肯定的。
方法二、分治算法
朴素的分法是二分,問題就是如何merge,在merge的時候,因為已經確定了會包含邊界點,所以可以在O(n)的時間復雜度上完成merge時的最大子段和。
因此分治公式是:T(n) = 2T(n/2)+O(n)
根據主定理可以算得,分治算法的時間復雜度為O(nlgn)。
int maxSubSum(int[] a,int L,int R){ if(L == R) return a[L] > 0 ? a[L] : 0; else { int mid = L + (R - L) / 2; int sumL = maxSubSum(a, L, mid); int sumR = maxSubSum(a, mid + 1, R); int tmpL = 0; int tmpR = 0; int sum = 0; for (int i = mid; i >=0 ; i--) { sum += a[i]; if(sum>tmpL) tmpL = sum; } sum = 0; for (int i = mid+1; i <=R ; i++) { sum += a[i]; if(sum>tmpR) tmpR = sum; } return Math.max(sumL,Math.max(sumR,tmpL+tmpR)); } }
方法三、動態規划
這種兩端都是變化的問題是很難優化的,最好可以讓一端固定,這樣就會大大簡化分析難度。於是將j暫時從原式子中提取出來,將剩下的命名為b[j]。
所以原問題就變成了這樣。
根據b[j]的定義,b[j]是指以a[j]結尾的最大子段和。因此有如下公式:
b[j] = max{b[j - 1] + a[j] , a[j]} 1=<j<=n
有了b[j],再對他取個極值,就可以得到原問題的解。
時間復雜度為O(n)
int maxSum(int[] a) { int res = 0; int b = 0; for (int i = 0; i < a.length; i++) { b = Math.max(b + a[i], a[i]); if (b > res) res = b; } return res; }
二、推廣問題
- 最大子矩陣和問題
問題描述:給定一個m*n的整數矩陣A,試求矩陣A的一個子矩陣,使其各個元素之和最大。
問題分析:事實上,只需要將矩陣“壓扁”就可以規約到最大子段和問題,具體來說就是將多行進行求和變為一行,這樣就可以直接使用上述問題的解法。將多行“壓縮”成一行有多種可行方案,需要遍歷一下,花費O(m^2),最大子段和動態規划算法花費O(n),所以總的時間消耗是O(m^2*n)。
- 最大m子段和問題
問題描述:給定n個整數(可能有負數)組成的序列a1,a2,...,an,以及一個正整數m,要求確定該序列的m個不相交子段,使這m個子段的總和最大。
問題分析:設b(i, j)表示數組a的前j項中i個子段和的最大值,且第i個子段含a[j](1<=i<=m,i<=j<=n),則所求的最優值顯然為max b(m, j),其中 m <= j <= n。
與最大子段和類似。計算b(i,j)的遞歸式子為:
b(i, j) = max{ b(i, j-1) + a[j] , max{ b(i - 1, t) + a[j] 其中t = i - 1 ~ j - 1} }
初始時,b(0, j) = 0; b(i, 0) = 0。
#include "stdafx.h" #include <iostream> using namespace std; int MaxSum(int m,int n,int *a); int main() { int a[] = {0,2,3,-7,6,4,-5};//數組腳標從1開始 for(int i=1; i<=6; i++) { cout<<a[i]<<" "; } cout<<endl; cout<<"數組a的最大連續子段和為:"<<MaxSum(3,6,a)<<endl; } int MaxSum(int m,int n,int *a) { if(n<m || m<1) return 0; int **b = new int *[m+1]; for(int i=0; i<=m; i++) { b[i] = new int[n+1]; } for(int i=0; i<=m; i++) { b[i][0] = 0; } for(int j=1;j<=n; j++) { b[0][j] = 0; } //枚舉子段數目,從1開始,迭代到m,遞推出b[i][j]的值 for(int i=1; i<=m; i++) { //n-m+i限制避免多余運算,當i=m時,j最大為n,可據此遞推所有情形 for(int j=i; j<=n-m+i; j++) { if(j>i) { b[i][j] = b[i][j-1] + a[j];//代表a[j]同a[j-1]一起,都在最后一子段中 for(int k=i-1; k<j; k++) { if(b[i][j]<b[i-1][k]+a[j]) b[i][j] = b[i-1][k]+a[j];//代表最后一子段僅包含a[j] } } else { b[i][j] = b[i-1][j-1]+a[j];//當i=j時,每一項為一子段 } } } int sum = 0; for(int j=m; j<=n; j++) { if(sum<b[m][j]) { sum = b[m][j]; } } return sum; }
上述算法顯然需要O(m*n^2)計算時間和O(m*n)。可以看一下具體矩陣是怎么填寫的。
注意到上述算法中,計算b[i][j]時只用到了b的當前行的前一個數以及上一行的一個極值。因此我們可以定義兩個數組,一個數組來保存當前行,一個數組來保存上一行的極值。並且使用數組來保存極值可以邊生成當前行的數值邊進行極值的判斷並進行填充。
#include "stdafx.h" #include <iostream> using namespace std; int MaxSum(int m,int n,int *a); int main() { int a[] = {0,2,3,-7,6,4,-5};//數組腳標從1開始 for(int i=1; i<=6; i++) { cout<<a[i]<<" "; } cout<<endl; cout<<"數組a的最大連續子段和為:"<<MaxSum(3,6,a)<<endl; } int MaxSum(int m,int n,int *a) { if(n<m || m<1) return 0; // b數組記錄第i行的最大i子段和 // c數組記錄第i-1行的極值 int *b = new int[n+1]; int *c = new int[n+1]; // 當然可以全部初始化為0,但事實上,只需要將b[0]初始化為0即可,因為對於下一輪的b值 // 只會直接調用前一輪的b首個數值,其他的數值都是自生成 // 具體的可以看圖就明白了 b[0] = 0; for (int i = 0;i < n+1;i++){ c[i] = 0; } for(int i=1; i<=m; i++) { // 對於 i == j的情況,單獨生成 b[i] = b[i-1] + a[i]; int max = b[i]; // n-m+i限制避免多余運算,當i=m時,j最大為n,可據此遞推所有情形 for(int j=i+1; j<=i+n-m;j++) { b[j] = b[j-1]>c[j-1]?b[j-1]+a[j]:c[j-1]+a[j]; c[j-1] = max; if(max<b[j]) { max = b[j]; } } c[i+n-m] = max; } int sum = 0; for(int j=m; j<=n; j++) { if(sum<b[j]) { sum = b[j]; } } return sum; }
上述算法需要O(m(n-m))的時間復雜度和O(n)的空間。當m或者(n-m)為常數的時候,上述算法只需要O(n)的時間復雜度和O(n)的空間。