4.1 最大子数组问题
一.
分治策略中,递归地求解一个问题,在每层递归中应用如下三个步骤:
- 分解 步骤将问题划分为一些子问题,子问题的形式与原问题一样,只是规模更小。
- 解决 步骤递归地求解出子问题。如果子问题的规模足够小,则停止递归,直接求解。
- 合并 步骤将子问题的解组合成原问题的解。
当子问题足够大,需要递归求解时,我们称之为递归情况。当子问题变得足够小,不再需要递归时,我们说递归已经“触底”,进入了基本情况。
子问题的规模不必是原问题规模的一个固定比例。
存在不是等式而是不等式的递归式,例如
T
(
n
)
≤
2
T
(
n
/
2
)
+
Θ
(
n
)
T(n)\leq 2T(n/2)+\Theta(n)
T(n)≤2T(n/2)+Θ(n) 。对此可用大
O
O
O 符号而不是
Θ
\Theta
Θ 符号来描述其解。类似地如
T
(
n
)
≥
2
T
(
n
/
2
)
+
Θ
(
n
)
T(n)\geq 2T(n/2)+\Theta(n)
T(n)≥2T(n/2)+Θ(n) 应该用
Ω
\Omega
Ω 符号来描述其解。
二.
已知一存在负数的数组,求解最大子数组。我们采用分治法设计一个
o
(
n
2
)
o(n^2)
o(n2) 的算法。
原问题如图所示,
我们考察每日的价格变化,第
i
i
i 天的价格变化定义为第
i
i
i 天和第
i
−
1
i-1
i−1 天的价格差,则问题转化为寻找
A
A
A 的和最大的非空连续子数组。我们成这样的连续子数组为最大子数组。
只有当数组中包含负数时,最大子数组问题才有意义。如果所有数组元素都是非负的,最大子数组问题没有任何难度,因为整个数组的和肯定是最大的。
使用分治技术,即将子数组划分为两个规模尽量相等的子数组。找到子数组的中央位置,比如 m i d mid mid ,然后考虑求解两个子数组 A [ l o w . . m i d ] A[low.\space .\space mid] A[low. . mid] (左子数组)和 A [ m i d + 1. . h i g h ] A[mid + 1.\space .\space high] A[mid+1. . high] (右子数组)。于是 A [ l o w . . h i g h ] A[low.\space .\space high] A[low. . high] 的任何连续子数组 A [ i . . j ] A[i.\space .\space j] A[i. . j] 所处的位置必然是:
- 完全位于子数组 A [ l o w . . m i d ] A[low.\space .\space mid] A[low. . mid] 中,因此 l o w ≤ i ≤ j ≤ m i d low\leq i\leq j \leq mid low≤i≤j≤mid 。
- 完全位于子数组 A [ m i d + 1. . h i g h ] A[mid + 1.\space .\space high] A[mid+1. . high] 中,因此 m i d < i ≤ j ≤ h i g h mid<i\leq j\leq high mid<i≤j≤high 。
- 跨越了中点,因此 l o w ≤ i ≤ m i d < j ≤ h i g h low\leq i \leq mid<j\leq high low≤i≤mid<j≤high 。
我们可以递归地求解
A
[
l
o
w
.
.
m
i
d
]
A[low.\space .\space mid]
A[low. . mid] 和
A
[
m
i
d
+
1.
.
h
i
g
h
]
A[mid +1.\space .\space high]
A[mid+1. . high] 的最大子数组,因为这两个子问题仍是最大子数组问题,只是规模更小。因此剩下的工作就是寻找跨越中点的最大子数组,然后在三种情况中选取和最大者。
由于求出的子数组必须跨越中点这一限制,寻找跨越中点的最大子数组并非是原问题的实例。我们很容易在线性时间(相对于子数组
A
[
l
o
w
.
.
h
i
g
h
]
A[low.\space .\space high]
A[low. . high] 的规模)内求出跨越中点的最大子数组。只需找出形如
A
[
i
.
.
m
i
d
]
A[i.\space .\space mid]
A[i. . mid] 和
A
[
m
i
d
+
1.
.
j
]
A[mid+1.\space .\space j]
A[mid+1. . j] 的最大子数组,然后将其合并即可。
FIND-MAX-CROSSING-SUBARRAY(A, low, mid, high)
left-sum = -∞
sum = 0
for i = mid downto low
sum = sum + A[i]
if sum > left-sum
left-sum = sum
max-left = i
right-sum = -∞
sum = 0
for j = mid + 1 to high
sum = sum + A[j]
if sum > right-sum
right-sum = sum
max-right = j
return (max-left, max-left, left-sum + right-sum)
如果子数组
A
[
l
o
w
.
.
h
i
g
h
]
A[low.\space.\space high]
A[low. . high] 包含
n
n
n 个元素(即
n
=
h
i
g
h
−
l
o
w
+
1
n=high-low+1
n=high−low+1),则调用 FIND-MAX-CROSSING-SUBARRAY(A, low, mid, high)
花费
Θ
(
n
)
\Theta(n)
Θ(n) 时间。
而最大子数组问题的分治算法的伪代码如下:
FIND-MAXINUM-SUBARRAY(A, low, high)
if high == low
return (low, high, A[low]) //base case: only one element
else mid = ⌊(low+high)/2⌋
(left-low, left-high, left-sum) =
FIND-MAXINUM-SUBARRAY(A, low, mid)
(right-low, right-high, right-sum) =
FIND-MAXINUM-SUBARRAY(A, mid + 1, high)
(cross-low, cross-high, cross-sum) =
FIND-MAX-CROSSING-SUBARRAY(A, low, mid, high)
if left-sum ≥ right-sum and left-sum ≥ cross-sum
return (left-low, left-high, left-sum)
elseif right-sum ≥ left-sum and right-sum ≥ cross-sum
return (right-low, right-high, right-sum)
else return (cross-low, cross-high, cross-sum)
初始调用 FIND-MAXINUM-SUBARRAY(A, 1, A.length)
会求出
A
[
1.
.
n
]
A[1. \space.\space n]
A[1. . n] 的最大子数组。
对原问题简化,假设原问题的规模为 2 的幂,这样所有子问题的规模均为整数。我们用
T
(
n
)
T(n)
T(n) 表示FIND-MAXINUM-SUBARRAY
求解
n
n
n 个元素的最大子数组的运行时间。分析有:
T
(
n
)
=
{
Θ
(
1
)
若 n = 1
2
T
(
n
/
2
)
+
Θ
(
n
)
若 n > 1
T(n)=\begin{cases}\Theta(1)&\text{若 n = 1}\\2T(n/2)+\Theta(n)&\text{若 n > 1}\end{cases}
T(n)={Θ(1)2T(n/2)+Θ(n)若 n = 1若 n > 1 显然解为
T
(
n
)
=
n
lg
n
T(n)=n\lg n
T(n)=nlgn 。
三.
4. 1-1
当
A
A
A 的所有元素均为负数时, FIND-MAXINUM-SUBARRAY
返回什么?
解:返回其中元素的最大值。
4. 1-2
对最大子数组问题,编写暴力求解方法的伪代码, 其运行时间应该为 Θ ( n 2 ) \Theta(n^2) Θ(n2)。
解:代码如下:
BRUTE-FORCE-FIND-MAXIMUM-SUBARRAY(A)
n = A.length
max-sum = -∞
for l = 1 to n
sum = 0
for h = l to n
sum = sum + A[h]
if sum > max-sum
max-sum = sum
low = l
high = h
return (low, high, max-sum)
4. 1-3
在你的计算机上实现最大子数组问题的暴力算法和递归算法。请指出多大的问题规模 n 0 n_0 n0 是性能交叉点——从此之后递归算法将击败暴力算法?然后,修改递归算法的基本情况——当问题规模小于 n 0 n_0 n0 时采用暴力算法。修改后,性能交叉点会改变吗?
解:
n
0
n_0
n0 与计算机种类等因素有关,这里略去。
如果将该算法修改为
n
≥
n
0
n\geq n_0
n≥n0 时采用分治法,
n
n
n 较小时采用蛮力法,则在交点处的性能几乎翻倍。在
n
0
−
1
n_0 - 1
n0−1 处的性能保持不变(甚至更差,因为增加了开销)。
4. 1-4
假定修改最大子数组问题的定义,允许结果为空子数组,其和为 0 。你应该如何修改现有算法,使它们能允许空子数组为最终结果?
解:在算法的最后判断最大子数组的总和是否小于零,如果小于零则返回空子数组。
4. 1-5
使用如下思想为最大子数组问题设计一个非递归的、线性时间的算法。 从数组的左边界开始,由左至右处理,记录到目前为止已经处理过的最大子数组。若已知 A [ 1. . j ] A[1.\space.\space j] A[1. . j] 的最大子数组,基于如下性质将其扩展为 A [ 1. . j + 1 ] A[1.\space.\space j + 1] A[1. . j+1] 的最大子数组: A [ 1. . j + 1 ] A[1.\space.\space j+1] A[1. . j+1] 的最大子数组要么是 A [ 1. . j ] A[1.\space.\space j] A[1. . j] 的最大子数组,要么是某个子数组 A [ i . . j + 1 ] ( 1 ≤ i ≤ j + 1 ) A[i.\space.\space j+1](1\leq i\leq j+1) A[i. . j+1](1≤i≤j+1)。在已知 A [ 1. . j ] A[1.\space.\space j] A[1. . j] 最大子数组的情况下,可以在线性时间内找出形如 A [ i . . j + 1 ] A[i.\space.\space j+1] A[i. . j+1] 的最大子数组。
解:代码如下:
ITERATIVE-FIND-MAXIMUM-SUBARRAY(A)
n = A.length
max-sum = -∞
sum = -∞
for j = 1 to n
currentHigh = j
if sum > 0
sum = sum + A[j]
else
currentLow = j
sum = A[j]
if sum > max-sum
max-sum = sum
low = currentLow
high = currentHigh
return (low, high, max-sum)