最大连续子序列和问题

最大连续子序列定义如下:

给定一个数组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
}

 

  • 8
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值