分治算法
最近看到《算法导论》的分治策略一节,看到的一个题目可以优化引申出来多种解法,同时也可以帮助理解分治策略的化整为零和动态规划的动态转移方程的思维。
最大子数组问题
最大子数组:数组 A 中的和最大的非空连续子数组。
暴力解法 O(n^2)
这个问题可以用暴力解法,两层循环遍历,时间复杂度为 O(n^2),当然最容易想到的并不是最好的解法。
package main
import (
"fmt"
)
func main() {
A := []int{13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7}
left, right, sum := FindMaxSubArray(A)
fmt.Println(left, right, sum)
}
func FindMaxSubArray(A []int) (left, right, sum int){
// 这里先不考虑 max 取值的问题,可以取切片的第一个元素或者 int 的最小值。
var max int
for i := 0; i < len(A); i++ {
sum = 0
for j := i; j < len(A); j++ {
sum += A[j]
if sum > max {
max = sum
left, right = i, j
}
}
}
return left, right, sum
}
可以得知最大的和为 43,即下标 7, 10 之间的子数组。
分治解法 O(nlgn)
既然这一节是讲分治策略,那么怎么用分治的思想来优化呢。这个解法确实比较难懂,如果让脑袋去跑一遍递归,真的有点累。那么分治本来就是一种局部整体的思想,我们把切片分成三组,左,中,右。那么我们只需要得出,这三个子集的最大值即可。然后再不断分化下去,最后把最大值冒上来。分治解法的关键就是如何用整体局部的思想把问题抽象化。
func FindMaxCrossingSubArray(A []int, low, mid, high int) (int, int, int){
// 这个函数为什么从中间分开算呢?因为得出的结果必须要跟 mid 位相关
var maxLeft, maxRight int
// 取负无穷大就行,这里简化处理
leftSum := -99999
sum := 0
for i := mid; i >= low; i-- {
sum += A[i]
if sum > leftSum {
leftSum = sum
maxLeft = i
}
}
rightSum := -99999
sum = 0
for j := mid + 1; j <= high; j++ {
sum += A[j]
if sum > rightSum {
rightSum = sum
maxRight = j
}
}
return maxLeft, maxRight, leftSum+rightSum
}
func FindMaxSubArray(A []int, low, high int) (int, int, int) {
if low == high {
return low, high, A[low]
} else {
mid := (low + high) >> 1
// 求左半区子数组最大值
leftLow, leftHigh, leftSum := FindMaxSubArray(A, low, mid)
// 求右半区子数组最大值
rightLow, rightHigh, rightSum := FindMaxSubArray(A, mid+1, high)
// 求包含中位数区子数组的最大值
crossLow, crossHigh, crossSum := FindMaxCrossingSubArray(A, low, mid, high)
if leftSum >= rightSum && leftSum >= crossSum {
return leftLow, leftHigh, leftSum
} else if rightSum >= leftSum && rightSum >= crossSum {
return rightLow, rightHigh, rightSum
} else {
return crossLow, crossHigh, crossSum
}
}
}
可以将时间复杂度降低到 O(n) 吗? 动态规划。
线性解法 O(n)
从题目上看,可以发现这道题满足动态规划的思想。可以求得动态转移方程为:F(n) = max(F(n-1)+A[n], A[n])
func FindMaxSubArray(nums []int) (left, right, maxNum int) {
var isLeftMax bool
maxNum = nums[0]
v := make([]int, len(nums))
v[0] = nums[0]
for i := 1; i < len(nums); i++ {
v[i], isLeftMax = max(nums[i], v[i-1] + nums[i])
if isLeftMax {
left = i
}
if v[i] > maxNum {
maxNum = v[i]
right = i
}
}
return left, right, maxNum
}
func max(a, b int) (int, bool) {
if a > b {
return a, true
}
return b, false
}