笔记
本节给出了分治法的一个例子。给定一个数组
A
[
1..
n
]
A[1..n]
A[1..n],找出一个元素和为最大的连续子数组
A
[
i
.
.
j
]
A[i..j]
A[i..j],其中
1
≤
i
≤
j
≤
n
1 ≤ i ≤ j ≤ n
1≤i≤j≤n,称这样的子数组为最大子数组。例如,下图所示数组中,第
8
8
8个元素到第
11
11
11个元素之间的子数组为最大子数组。
求解最大子数组问题,最简单的方法是暴力检查所有的子数组,从中找出和为最大的子数组。对于一个有
n
n
n个元素的数组,一共有
C
n
2
+
C
n
1
=
n
(
n
−
1
)
/
2
+
n
=
Θ
(
n
2
)
C_n^2+C_n^1=n(n-1)/2+n=Θ(n^2)
Cn2+Cn1=n(n−1)/2+n=Θ(n2)个子数组。参考练习4.1-2可知,计算每个子数组的和只需要
O
(
1
)
O(1)
O(1)时间。因此暴力求解法的所花费的时间为
Θ
(
n
2
)
Θ(n^2)
Θ(n2)。
除了暴力求解法,最大子数组问题还可以使用分治法求解,并且分治法具有更优的时间复杂度。假定要寻找子数组
A
[
l
o
w
.
.
h
i
g
h
]
A[low..high]
A[low..high]的最大子数组,我们从中央位置
m
i
d
=
⌊
(
l
o
w
+
h
i
g
h
)
/
2
⌋
mid=⌊(low+high)/2⌋
mid=⌊(low+high)/2⌋将
A
[
l
o
w
.
.
h
i
g
h
]
A[low..high]
A[low..high]划分为两个子数组,
A
[
l
o
w
.
.
m
i
d
]
A[low..mid]
A[low..mid]和
A
[
m
i
d
+
1..
h
i
g
h
]
A[mid+1..high]
A[mid+1..high]。于是,
A
[
l
o
w
.
.
h
i
g
h
]
A[low..high]
A[low..high]的任何一个子数组
A
[
i
.
.
j
]
A[i..j]
A[i..j]必然是以下三种情况之一:
• 完全位于子数组
A
[
l
o
w
.
.
m
i
d
]
A[low..mid]
A[low..mid]中,即
l
o
w
≤
i
≤
j
≤
m
i
d
low ≤ i ≤ j ≤ mid
low≤i≤j≤mid。
• 完全位于子数组
A
[
m
i
d
+
1..
h
i
g
h
]
A[mid+1..high]
A[mid+1..high]中,即
m
i
d
<
i
≤
j
≤
h
i
g
h
mid < i ≤ j ≤ high
mid<i≤j≤high。
• 跨越了中央位置
m
i
d
mid
mid,即
l
o
w
≤
i
≤
m
i
d
<
j
≤
h
i
g
h
low ≤ i ≤ mid < j ≤ high
low≤i≤mid<j≤high。
根据以上分析,可以递归求解最大子数组问题。对于一个子数组
A
[
l
o
w
.
.
h
i
g
h
]
A[low..high]
A[low..high],首先寻找跨越中央位置的最大子数组,然后分别递归求解
A
[
l
o
w
.
.
m
i
d
]
A[low..mid]
A[low..mid]和
A
[
m
i
d
+
1..
h
i
g
h
]
A[mid+1..high]
A[mid+1..high]的最大子数组,比较这三种情况的最大子数组,从中选出元素和最大者作为
A
[
l
o
w
.
.
h
i
g
h
]
A[low..high]
A[low..high]的最大子数组。
分治法的关键在于寻找跨越中央位置的最大子数组。对于一个子数组
A
[
l
o
w
.
.
h
i
g
h
]
A[low..high]
A[low..high],任何跨越中央位置
m
i
d
mid
mid的子数组必然都由两个子数组
A
[
i
.
.
m
i
d
]
A[i..mid]
A[i..mid]和
A
[
m
i
d
+
1..
j
]
A[mid+1..j]
A[mid+1..j]组成,其中
l
o
w
≤
i
≤
m
i
d
low ≤ i ≤ mid
low≤i≤mid并且
m
i
d
<
j
≤
h
i
g
h
mid < j ≤ high
mid<j≤high。因此,我们只需要找出形如
A
[
i
.
.
m
i
d
]
A[i..mid]
A[i..mid]和A
[
m
i
d
+
1..
j
]
[mid+1..j]
[mid+1..j]的最大子数组,然后将二者合并即可。下面给出寻找跨越中央位置的最大子数组的伪代码。
接下来给出分治法求解最大数组问题的伪代码。
要寻找数组
A
[
1..
n
]
A[1..n]
A[1..n]的最大子数组,只需要调用FIND-MAXIMUM -SUBARRAY
(
A
,
1
,
n
)
(A, 1, n)
(A,1,n)即可。
下面分析分治法求解最大子数组问题的时间复杂度。对于长度为
n
n
n的数组,求解最大子数组的时间用
T
(
n
)
T(n)
T(n)表示。
T
(
n
)
T(n)
T(n)由三部分组成:
• 递归求解子数组
A
[
1..
m
i
d
]
A[1..mid]
A[1..mid]的最大子数组的时间
T
(
n
/
2
)
T(n/2)
T(n/2);
• 递归求解子数组
A
[
m
i
d
+
1..
n
]
A[mid+1..n]
A[mid+1..n]的最大子数组的时间
T
(
n
/
2
)
T(n/2)
T(n/2);
• 求解跨越中央位置的最大子数组的时间,这一时间为
Θ
(
n
)
Θ(n)
Θ(n)。
所以有递归式
T
(
n
)
=
2
T
(
n
/
2
)
+
Θ
(
n
)
T(n) = 2T(n/2) +Θ(n)
T(n)=2T(n/2)+Θ(n)。求解这个递归式,得到
T
(
n
)
=
Θ
(
n
l
g
n
)
T(n) = Θ(nlgn)
T(n)=Θ(nlgn)。
练习
4.1-1 当
A
A
A的所有元素均为负数时,FIND-MAXIMUM-SUBARRAY返回什么?
解
返回数值最大的那个负数,即绝对值最小的负数。
4.1-2 对最大子数组问题,编写暴力求解方法的伪代码,其运行时间应该为
Θ
(
n
2
)
Θ(n^2)
Θ(n2)。
解
4.1-3 当你的计算机上实现最大子数组问题的暴力算法和递归算法。请指出多大的问题规模
n
0
n_0
n0是性能交叉点——从此之后递归算法将击败暴力算法?然后,修改递归算法的基本情况——当问题规模小于
n
0
n_0
n0时采用暴力算法。修改后,性能交叉点会改变吗?
略
4.1-4 假定修改最大子数组问题的定义,允许结果为空子数组,其和为
0
0
0。你应该如何修改现有算法,使它们能允许空子数组为最终结果?
解
先对整个数组遍历一遍,检查是否所有元素都为负数。如果所有元素都为负数,则算法输出空子数组。如果数组中存在正数,则调用FIND-MAXIMUM –SUBARRAY求解。
4.1-5 使用如下思想为最大子数组问题设计一个非递归的、线性时间的算法。从数组的左边界开始,由左至右处理,记录到目前为止已经处理过的最大子数组。若已知
A
[
1..
j
]
A[1..j]
A[1..j]的最大子数组,基于如下性质将解扩展为
A
[
1..
j
+
1
]
A[1..j+1]
A[1..j+1]的最大子数组:
A
[
1..
j
+
1
]
A[1..j+1]
A[1..j+1]的最大子数组要么是
A
[
1..
j
]
A[1..j]
A[1..j]的最大子数组,要么是某个形如
A
[
i
.
.
j
+
1
]
A[i..j+1]
A[i..j+1]的最大子数组
(
1
≤
i
≤
j
+
1
)
(1 ≤ i ≤ j+1)
(1≤i≤j+1)。在已知形如
A
[
i
.
.
j
]
A[i..j]
A[i..j]的最大子数组的情况下,可以在常数时间内找出形如
A
[
i
.
.
j
+
1
]
A[i..j+1]
A[i..j+1]的最大子数组。
解
与分治法不同,这是典型的增量法。本题的关键在于:在已知以
A
[
j
]
A[j]
A[j]结尾的最大子数组的情况下,找出以
A
[
j
+
1
]
A[j+1]
A[j+1]结尾的最大子数组。假设以
A
[
j
]
A[j]
A[j]结尾的最大数组为
A
[
i
.
.
j
]
(
1
≤
i
≤
j
)
A[i..j] (1 ≤ i ≤ j)
A[i..j](1≤i≤j)。分两种情况:
(1) 如果
A
[
i
.
.
j
]
A[i..j]
A[i..j]各元素之和
s
u
m
{
A
[
i
.
.
j
]
}
>
0
{\rm sum}\{A[i..j]\} > 0
sum{A[i..j]}>0,那么以
A
[
j
+
1
]
A[j+1]
A[j+1]结尾的最大数组为
A
[
i
.
.
j
+
1
]
A[i..j+1]
A[i..j+1]。这一点可以用反证法来说明。假设以
A
[
j
+
1
]
A[j+1]
A[j+1]结尾的最大数组为
A
[
k
.
.
j
+
1
]
A[k..j+1]
A[k..j+1],其中
1
≤
k
≤
j
+
1
1 ≤ k ≤ j+1
1≤k≤j+1并且
k
≠
i
k ≠ i
k=i。又分两种情况讨论。
1)
1
≤
k
≤
j
1 ≤ k ≤ j
1≤k≤j:由于以
A
[
j
]
A[j]
A[j]结尾的最大子数组为
A
[
i
.
.
j
]
A[i..j]
A[i..j],所以
s
u
m
{
A
[
k
.
.
j
]
}
≤
s
u
m
{
A
[
i
.
.
j
]
}
{\rm sum}\{A[k..j]\} ≤ {\rm sum}\{A[i..j]\}
sum{A[k..j]}≤sum{A[i..j]},从而有
s
u
m
{
A
[
k
.
.
j
+
1
]
}
≤
s
u
m
{
A
[
i
.
.
j
+
1
]
}
{\rm sum}\{A[k..j+1]\} ≤ {\rm sum}\{A[i..j+1]\}
sum{A[k..j+1]}≤sum{A[i..j+1]}。如果
s
u
m
{
A
[
k
.
.
j
+
1
]
}
<
s
u
m
{
A
[
i
.
.
j
+
1
]
}
{\rm sum}\{A[k..j+1]\} < {\rm sum}\{A[i..j+1]\}
sum{A[k..j+1]}<sum{A[i..j+1]},那么
A
[
k
.
.
j
+
1
]
A[k..j+1]
A[k..j+1]肯定不是以
A
[
j
+
1
]
A[j+1]
A[j+1]结尾的最大数组,这与假设矛盾。如果
s
u
m
{
A
[
k
.
.
j
+
1
]
}
=
s
u
m
{
A
[
i
.
.
j
+
1
]
}
{\rm sum}\{A[k..j+1]\} = {\rm sum}\{A[i..j+1]\}
sum{A[k..j+1]}=sum{A[i..j+1]},那么如果假设成立,即
A
[
k
.
.
j
+
1
]
A[k..j+1]
A[k..j+1]是以
A
[
j
+
1
]
A[j+1]
A[j+1]结尾的最大数组,那么
A
[
i
.
.
j
+
1
]
A[i..j+1]
A[i..j+1]也同样是以
A
[
j
+
1
]
A[j+1]
A[j+1]结尾的最大数组。
2)
k
=
j
k = j
k=j:此时假设的以
A
[
j
+
1
]
A[j+1]
A[j+1]结尾的最大数组为
A
[
j
+
1
]
A[j+1]
A[j+1]本身。由于
s
u
m
{
A
[
i
.
.
j
]
}
>
0
{\rm sum}\{A[i..j]\} > 0
sum{A[i..j]}>0,所以
s
u
m
{
A
[
i
.
.
j
+
1
]
}
>
A
[
j
+
1
]
{\rm sum}\{A[i..j+1]\} > A[j+1]
sum{A[i..j+1]}>A[j+1]。这说明
A
[
j
+
1
]
A[j+1]
A[j+1]本身肯定也不是以
A
[
j
+
1
]
A[j+1]
A[j+1]结尾的最大数组,这与假设矛盾。
(2) 如果
A
[
i
.
.
j
]
A[i..j]
A[i..j]各元素之和
s
u
m
{
A
[
i
.
.
j
]
}
≤
0
{\rm sum}\{A[i..j]\} ≤ 0
sum{A[i..j]}≤0,那么
A
[
j
+
1
]
A[j+1]
A[j+1]结尾的最大数组为
A
[
j
+
1
]
A[j+1]
A[j+1]本身。这一点同样可以用反证法来说明,这里就不赘述。
下面给出该算法的伪代码。
对于一个包含
n
n
n个元素的数组,该算法一共包含
n
n
n次迭代,每次迭代花费
Θ
(
1
)
Θ(1)
Θ(1)时间。因此,该算法的运行时间为
Θ
(
n
)
Θ(n)
Θ(n)。
本节代码链接:
https://github.com/yangtzhou2012/Introduction_to_Algorithms_3rd/tree/master/Chapter04/Section_4.1