最大连续子序列定义如下:
给定一个数组a[ n ] ,数组元素均为自然数集(有正数,有负数),请求出该数组一个连续的子序列,使得这个子序列的和值最大,示例如下
a[] = {1, 2, -9, 5, 6, -3, 7, 8, -89, 10}
那么它的最大连续子序列为 {5,6,-3,7,8} ,和值 = 23
这个问题,最自然的想法是,暴力破解,即三重循环(实现见下面代码violenceOn3Sum),依次求出所有连续子序列的和值,这种算法,想法简单,但是效率极其低下,时间复杂度O(n^3) ,当n>10的4次方,基本等不到结果
·对暴力破解的优化
仔细观察上面的三重循环,会发现,最里面的循环其实做了【大量的重复工作】,我们其实完全可以在第二层循环的时候就计算第三层的最大值,并且在循环的推进过程中,及时更新局部最大值,这样,就能减少重复工作,将时间复杂度优化为O(n^2)
·分治法
分治法的思路每次降低问题的规模为原来的一半,我们每次将数组均分为2,那么最大连续子序列要么完全位于左边数组,要么完全位于右边数组,要么横跨左右两边数组,按照这个思路,写递归程序divideSum,通过简单的分析,我们可以得到时间复杂度关系如下(其中c为常数)
T(n) = 2T(n/2) + c*n
进一步递推
T(n) = 2T(n/2) + c*n
= 4T(n/4) + 2*c*n
= 8T(n/8) + 3*c*n
....
= 2^k T(1) + k*c*n
其中 k = log2(n)
代入可得 T(n) = log2n + c*n* log2n
故时间复杂度是 O(n*log2n)
·动态规划
动态规范的方法是最难想到,但也是最高效的方法,在这里,我们定义相关量如下
s[ i ] 表示数组的所有子序列里面满足如下两个条件的子序列的和:
连续子序列包含a[i],
i是连续子序列里面的最大下标
那么很容易证明s[ i ] 满足如下关系 s[ i ] = max{a[ i ] , s[ i-1] + a[ i ]} ;
整个问题的最优解,即,求整个数组的最大连续子序列和 sum = max { s[ i ] } 其中 i>=1 并且 i<=n
映射到代码,我们只需要用两个变量存储最大值即可,sum存储全局最大的和,sumn_1存储当前的s[ i ] ,并随着循环推进,如果遇到更大的sum,就更新sum,顺带更新这个最大sum对于子序列的数组头尾索引start和end。
这个算法时间复杂度是 O(n) ,经过测试,当n>10的8次方时,3秒钟就能出来结果,效率比起暴力破解的 约 10的5次方倍,运行这个例子可以直观感受到算法的优劣对效率千万倍的差别
注:代码实现为golang
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
var s []int
rand.Seed(time.Now().Unix())
for i := 0; i < 1e8; i++ {
s = append(s, rand.Int()%1000-500) // random generate number x, x>=-500 and x<500
}
// Dynamic programming time complexity=O(n) space complexity = O(1); it's so wonderful
startTime := time.Now().Unix()
sum, start, end := dynamicSum(s)
fmt.Printf("\ndynamicSum = %v,start=%v,end=%v,used time=%v(s)",sum,start,end,time.Now().Unix()-startTime) // when n=1e8 used time=3(s)
//divide and conquer method; time complexity = O(n*log-n)
startTime = time.Now().Unix()
sum, start, end = divideSum(s)
fmt.Printf("\ndivideSum = %v,start=%v,end=%v,used time=%v(s)",sum,start,end,time.Now().Unix()-startTime)
// violence method; time complexity = O(n^2)
startTime = time.Now().Unix()
violenceOn2Sum(s[:100000])
fmt.Printf("\nviolenceOn2Sum used time is=%v(s)\n",time.Now().Unix()-startTime) // when n=1e5; violenceOn2Sum used time is=20(s)
//violence method; time complexity = O(n^3), it's so inefficient
startTime = time.Now().Unix()
violenceOn3Sum(s[:10000])
fmt.Printf("\nviolenceOn3Sum used time is=%v(s)\n",time.Now().Unix()-startTime) // when n=1e4; violenceOn2Sum used time is=20(s)
}
// 完全的暴力破解, O(n^3) 低效到出奇
func violenceOn3Sum(arr []int) (int,int,int) {
start,end := 0,0
sum := arr[0]
for i:= 0; i< len(arr); i++ {
for j := i; j < len(arr); j++ {
localSum := 0;
for k := i; k <= j; k++ {
localSum += arr[k];
}
if localSum > sum {
start = i
end = j
sum = localSum
}
}
}
return sum,start,end
}
// 优化的暴力算法 O(n^2)
func violenceOn2Sum(arr []int) (int,int,int ) {
sum := arr[0]
start, end := 0, 0
for i := 0; i < len(arr); i++ {
localSum := arr[i]
for j := i-1; j >= 0; j-- {
localSum += arr[j]
if localSum > sum {
sum = localSum
start = j
end = i
}
}
}
fmt.Println(arr)
for i := start; i<=end; i++ {
fmt.Printf("%v ",arr[i])
}
fmt.Printf("\nsum=%v\n",sum)
return sum,start,end
}
// 分治法,将数组平均分词两部分,最大连续子序列和,要么位于左侧,要么位于右侧,要么横跨两侧
func divideSum(arr []int) (int,int,int) {
// max sub array Either location left array or location right array or stride over middle item
// so we only need computed the most max of that three result
if len(arr) == 1 {
return arr[0],0,0
}
leftSum,leftStart,leftEnd := divideSum(arr[:len(arr)/2])
rightSum,rightStart,rightEnd := divideSum(arr[len(arr)/2:])
rightStart += len(arr)/2
rightEnd += len(arr)/2
middleStart := len(arr)/2-1
middleLeftSum := arr[middleStart]
sum := middleLeftSum
for i:=middleStart-1;i>=0 ; i-- {
sum += arr[i]
if sum > middleLeftSum {
middleLeftSum = sum
middleStart = i
}
}
middleEnd := len(arr)/2
middleRightSum := arr[middleEnd]
sum = middleRightSum
for i:=middleEnd+1;i<len(arr) ; i++ {
sum += arr[i]
if sum > middleRightSum {
middleRightSum = sum
middleEnd = i
}
}
max := max(leftSum,rightSum,middleLeftSum+middleRightSum)
if max == leftSum {
return leftSum,leftStart,leftEnd
} else if max == rightSum {
return rightSum,rightStart,rightEnd
} else {
return max,middleStart,middleEnd
}
}
func max(arr...int) int {
result :=arr[0]
for _, a := range arr {
if a > result {
result = a;
}
}
return result
}
// 动态规划的方法解决问题
// 定义 通项公式s[n]表示,连续和中最大下标为n的(也就是a[n]是连续子序列最右侧元素的),有递推公式s[n]= max{a[n], s[n-1] + a[n]} ;
// 那么整个数组的最长子序列就是max{s[i]} i属于1到n
func dynamicSum(arr []int) (int, int, int) {
sumn_1 := arr[0] // 记录当i=k-1时候的最长子序列
sum := sumn_1
start,end :=0,0
for index, ai := range arr {
if index == 0 {
continue
}
if sumn_1<0 {
sumn_1 = ai
if sumn_1 > sum {
start = index
end = index
sum = sumn_1
}
} else {
sumn_1 = sumn_1 + ai
if sumn_1 > sum {
end = index
sum = sumn_1
}
}
}
return sum,start,end
}