一、算法概述
在数学和计算机科学之中,算法是任何良定义的计算过程,该过程取某个值或值的集合作为输入并产生某个值或值的集合作为输出。
1.1 算法分析
所谓的算法分析,是指通过分析而非实验的手段来考察算法的某些性质。
算法的正确性,要求算法对于每一个输入都最终停止,并且产生正确的输出。当不满足算法的正确性时,会产生不正确错算法,对于某个输入不停止,或产生不正确的结果;或产生近似算法,对所有输入都停止,但会产生近似正确的解或产生少数的不正确解。算法的正确性分析要求:
-证明算法对所有的输入都停止;
-证明对每个输入都产生正确的结果。
要注意的是,程序的调试与测试只能证明程序有错,但不能证明程序无错。
典型的,考察插入排序的正确性。考虑数组
A
[
1...
j
−
1
]
A[1 ... j-1]
A[1...j−1],那么证明:
-初始化,
j
=
2
j=2
j=2,
A
[
1...
j
−
1
]
=
A
[
1
]
A[1 ... j-1] = A[1]
A[1...j−1]=A[1],有序;
-循环,第
j
j
j个元素会发生移动,产生
A
[
1...
j
]
A[1 ... j]
A[1...j]有序;
-终止,
j
=
n
+
1
j=n+1
j=n+1,
A
[
1...
j
−
1
]
=
A
[
1...
n
]
A[1 ... j-1] = A[1 ... n]
A[1...j−1]=A[1...n]有序。
从而产生终止且正确排序的数组。
算法的复杂性,预测算法对不同输入所需资源量,包括时间、空间、I/O、通讯、消耗等,一般是输入大小的函数。其用于为求解一个问题选择最佳的算法与最佳的硬件。
时间复杂性描述了一个算法对特定输入产生结果所需要的原子操作数。一般认为原子操作需要常数时间,实际上每个操作需要的时间量可能不同。
空间复杂性描述了一个算法对特定输入产生结果所需要的内存存储空间。
复杂性可以使用多种度量,对于问题R的输入集合Input,复杂性函数Complexity()以及输入规模Size(),包括最坏复杂性,形如
M
a
x
{
C
o
m
p
l
e
x
i
t
y
(
S
i
z
e
(
y
)
)
∣
y
∈
I
n
p
u
t
}
Max\{Complexity(Size(y))|y \in Input\}
Max{Complexity(Size(y))∣y∈Input}以及对于每种输入出现的概率
p
y
p_y
py,平均复杂性形如
∑
y
∈
I
n
p
u
t
p
y
C
o
m
p
l
e
x
i
t
y
(
s
i
z
e
(
y
)
)
\sum_{y \in Input} p_yComplexity(size(y))
y∈Input∑pyComplexity(size(y)) 算法分析的模型通常为随机访问模型【Random-Access-Model,RAM】。
1.2 复杂性函数的阶
算法的阶描述了算法的复杂性增长率。描述阶使用增长函数,在输入规模较大、忽略低阶的情况下描述了渐近效率。
用来表示算法的渐近运行时间的记号使用定义域为自然数集
N
=
{
0
,
1
,
2
,
.
.
.
}
\bm{N} = \{0, 1, 2, ...\}
N={0,1,2,...}的函数来定义的。这些符号用来表示最坏情况运行时间
T
(
n
)
T(n)
T(n),因为
T
(
n
)
T(n)
T(n)一般定义于整数的输入规模上。
首先定义渐近上界记号,即
f
(
n
)
=
O
(
g
(
n
)
)
f(n) = O(g(n))
f(n)=O(g(n)),代表
∃
c
>
0
,
n
0
>
0
,
∀
n
≥
n
0
,
0
<
f
(
n
)
≤
c
g
(
n
)
\exists c>0, n_0>0, \forall n \ge n_0, 0 < f(n) \le cg(n)
∃c>0,n0>0,∀n≥n0,0<f(n)≤cg(n)而
O
(
g
(
n
)
)
O(g(n))
O(g(n))代表了一个集合
O
(
g
(
n
)
)
=
{
f
(
n
)
∣
∃
c
>
0
,
n
0
>
0
,
∀
n
≥
n
0
,
0
<
f
(
n
)
≤
c
g
(
n
)
}
O(g(n)) = \{ f(n) |\exists c>0, n_0 >0, \forall n \ge n_0, 0 < f(n) \le cg(n)\}
O(g(n))={f(n)∣∃c>0,n0>0,∀n≥n0,0<f(n)≤cg(n)}故实际上,
f
(
n
)
=
O
(
g
(
n
)
)
f(n) = O(g(n))
f(n)=O(g(n))实质为
f
(
n
)
∈
O
(
g
(
n
)
)
f(n) \in O(g(n))
f(n)∈O(g(n)),但常记为等号。同样定义渐近下界记号,即
f
(
n
)
=
Ω
(
g
(
n
)
)
f(n) = \Omega(g(n))
f(n)=Ω(g(n)),其中
Ω
(
g
(
n
)
)
=
{
f
(
n
)
∣
∃
c
>
0
,
n
0
>
0
,
∀
n
≥
n
0
,
0
<
c
g
(
n
)
≤
f
(
n
)
}
\Omega(g(n)) = \{ f(n) |\exists c>0, n_0 >0, \forall n \ge n_0, 0 < cg(n) \le f(n)\}
Ω(g(n))={f(n)∣∃c>0,n0>0,∀n≥n0,0<cg(n)≤f(n)}此外,定义
f
(
n
)
=
Θ
(
g
(
n
)
)
f(n) = Θ(g(n))
f(n)=Θ(g(n)),其中
Θ
(
g
(
n
)
)
=
O
(
g
(
n
)
)
∩
Ω
(
g
(
n
)
)
Θ(g(n)) = O(g(n)) \cap \Omega(g(n))
Θ(g(n))=O(g(n))∩Ω(g(n)),同时规定了上界与下界。以及规定严格符号,即
f
(
n
)
=
o
(
g
(
n
)
)
f(n) = o(g(n))
f(n)=o(g(n))与
f
(
n
)
=
w
(
g
(
n
)
)
f(n) = w(g(n))
f(n)=w(g(n))以表示不紧确的渐近上界与下界。
1.3 递归方程
当一个算法包含对自身的递归调用时,其运行时间通常可以用递归方程【Recurrence】表示,迄今为止没有一个通用的方法来解递归问题,考虑递归方程
{
T
(
1
)
=
Θ
(
1
)
T
(
n
)
=
2
T
(
n
/
2
)
+
Θ
(
n
)
,
n
>
1
\left\{\begin{aligned}&T(1) = \Theta(1) \\&T(n) = 2T(n/2) + \Theta(n), n > 1\\\end{aligned}\right.
{T(1)=Θ(1)T(n)=2T(n/2)+Θ(n),n>1
数学归纳法,假设
T
(
n
)
=
Θ
(
n
l
o
g
n
)
T(n) = \Theta(nlogn)
T(n)=Θ(nlogn),那么
T
(
n
)
=
2
T
(
n
/
2
)
+
Θ
(
n
)
=
b
n
l
o
g
(
n
/
2
)
+
c
n
=
b
n
l
o
g
n
−
b
n
l
o
g
2
+
c
n
=
Θ
(
n
l
o
g
n
)
\begin{aligned} T(n)& = 2T(n/2) + \Theta(n) \\ & = bnlog(n/2) + cn \\ & = bnlogn - bnlog2 + cn \\ &= \Theta(nlogn) \end{aligned}
T(n)=2T(n/2)+Θ(n)=bnlog(n/2)+cn=bnlogn−bnlog2+cn=Θ(nlogn)
主定理,对于
T
(
n
)
=
a
T
(
n
/
b
)
+
f
(
n
)
T(n) = aT(n/b) + f(n)
T(n)=aT(n/b)+f(n),那么
T
(
n
)
T(n)
T(n)为
T
(
n
)
=
{
Θ
(
n
l
o
g
b
a
)
,
f
(
n
)
=
O
(
n
l
o
g
b
a
−
ε
)
,
ε
>
0
Θ
(
n
l
o
g
b
a
l
o
g
(
k
+
1
)
n
)
,
f
(
n
)
=
Θ
(
n
l
o
g
b
a
l
o
g
k
n
)
,
k
≥
0
Θ
(
f
(
n
)
)
,
f
(
n
)
=
Ω
(
n
l
o
g
b
a
+
ε
)
,
ε
>
0
,
a
f
(
n
/
b
)
≤
(
1
−
ε
′
)
f
(
n
)
,
ε
′
>
0
T(n) = \left\{\begin{aligned}& Θ(n^{log_ba}), &&f(n) =O(n^{log_ba - ε}), ε>0 \\& Θ(n^{log_ba}log^{(k+1)}n), && f(n) =Θ(n^{log_ba}log^kn), k\ge0 \\& Θ(f(n)), && f(n) =\Omega(n^{log_ba + ε}), ε>0,af(n/b) \le (1 - ε')f(n), ε'>0 \end{aligned}\right.
T(n)=⎩⎪⎪⎨⎪⎪⎧Θ(nlogba),Θ(nlogbalog(k+1)n),Θ(f(n)),f(n)=O(nlogba−ε),ε>0f(n)=Θ(nlogbalogkn),k≥0f(n)=Ω(nlogba+ε),ε>0,af(n/b)≤(1−ε′)f(n),ε′>0在上述式中,
f
(
n
)
=
Θ
(
n
)
f(n) = \Theta(n)
f(n)=Θ(n),
a
=
2
a = 2
a=2,
b
=
2
b = 2
b=2,那么
f
(
n
)
=
Θ
(
n
l
o
g
2
2
l
o
g
0
n
)
f(n) = \Theta(n^{log_{2}2}log^0n)
f(n)=Θ(nlog22log0n),故
T
(
n
)
=
Θ
(
n
l
o
g
2
2
l
o
g
1
n
)
=
Θ
(
n
l
o
g
n
)
T(n) = \Theta(n^{log_{2}2}log^1n) = \Theta(nlogn)
T(n)=Θ(nlog22log1n)=Θ(nlogn)。
二、分治法
归并排序是典型的分治策略,其递归的求解一个问题,在每层递归中应用如下步骤:
-分解,将问题划分为子问题,子问题与原问题形式一致,但规模更小;
-解决,递归的求解子问题,直到子问题的规模足够小,停止递归,直接求解;
-合并,将子问题的解组合成原问题的解。
分治算法的分析需要建立递归方程,并根据算法分析方法进行求解。
2.1 中位数算法
中位数是重要的统计量,可以避免方差过大值的干扰,如薪资水平等。对于由n个数构成的集合
X
X
X,中位数
x
x
x满足
x
∈
X
,
∣
∣
{
y
∈
X
∣
y
<
x
}
∣
−
∣
{
y
∈
X
∣
y
>
x
}
∣
∣
≤
1
x \in X, ||\{y \in X|y<x\}| - |\{y \in X|y>x\}|| \le 1
x∈X,∣∣{y∈X∣y<x}∣−∣{y∈X∣y>x}∣∣≤1如果采取排序选取的方法,算法时间复杂度为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),采用分治法可以达到线性时间复杂度。
令对于共n个数据,其中位数的索引为
i
=
⌊
n
/
2
⌋
i = \lfloor n / 2\rfloor
i=⌊n/2⌋,首先给出算法步骤:
1.将数据分为若干组,每组m个数,m是较小的正整数,共有
⌈
n
/
m
⌉
\lceil n / m\rceil
⌈n/m⌉组,对每组的数据进行排序,姑且选择插入排序,选取出每一组的中位数;
2.对得到的所有组的中位数求中位数,记为M;
3.使用M划分所有数据,令小于M的所有数据构成集合S,大于M的所有数据构成集合L;
4.比较M的索引
k
k
k与中位数的索引
i
i
i:
4.1.若
i
=
k
i = k
i=k,则M即为中位数;
4.2.若
i
<
k
i < k
i<k,则记
i
=
i
i = i
i=i,在S中递归的进行步骤1-4;
4.3.若
i
>
k
i > k
i>k,则记
i
=
i
−
k
i = i - k
i=i−k,在L中递归的进行步骤1-5。
算法的基本思想为,当S与L的集合基数相等时,M即为中位数。接下来进行算法分析:
1.
⌈
n
/
m
⌉
\lceil n / m\rceil
⌈n/m⌉组m个数的排序,时间复杂度为
O
(
⌈
n
/
m
⌉
⋅
m
2
)
O(\lceil n / m\rceil · m^2)
O(⌈n/m⌉⋅m2),且m是较小的正整数,可以认为其是常量,即时间复杂度为
O
(
n
)
O(n)
O(n);
2.对
⌈
n
/
m
⌉
\lceil n / m\rceil
⌈n/m⌉个数取中位数,设该取中位数算法的时间复杂度为
T
(
n
)
T(n)
T(n),那么时间复杂度为
T
(
n
/
m
)
T(n/m)
T(n/m);
3.划分集合,所有元素与M进行比较,时间复杂度为
O
(
n
)
O(n)
O(n);
4.当递归未结束时,设中位数的索引
i
i
i小于M的索引
k
k
k,将要在S中递归的进行该算法,那么在最坏的情况下,L至少有的元素包括比M大的每组的中位数,共
⌊
n
/
2
m
⌋
\lfloor n / 2m\rfloor
⌊n/2m⌋个,以及这些组与M所在组中比每组的中位数大的数,共约
⌊
n
/
2
m
⌋
×
⌊
m
/
2
⌋
\lfloor n / 2m\rfloor \times \lfloor m / 2\rfloor
⌊n/2m⌋×⌊m/2⌋个,精准度受n的奇偶性影响。
因此可以得到递归方程
T
(
n
)
≤
{
Θ
(
1
)
,
n
≤
C
T
(
n
/
m
)
+
T
(
3
n
/
4
)
+
Θ
(
n
)
n
>
C
T(n) \le \left\{\begin{aligned}&\Theta(1), &&n \le C \\ &T(n/m) + T(3n/4) + \Theta(n) && n > C\\ \end{aligned}\right.
T(n)≤{Θ(1),T(n/m)+T(3n/4)+Θ(n)n≤Cn>C可以解得
T
(
n
)
=
O
(
n
)
T(n) = O(n)
T(n)=O(n)。
2.2 快速傅里叶变换算法
傅里叶变换广泛应用于图像处理等领域,离散傅里叶变换的算法复杂度为
O
(
n
2
)
O(n^2)
O(n2),快速傅里叶变换算法的出现大大降低了离散傅里叶变换的执行效率。
离散傅里叶变换的数学描述为,对于时域系数
t
1
,
t
2
,
.
.
.
,
t
n
∈
R
t_1, t_2, ..., t_n\in R
t1,t2,...,tn∈R,
n
=
2
N
n = 2^N
n=2N,求取频域系数
w
j
=
∑
i
=
0
n
t
i
e
x
p
{
j
2
π
i
/
n
}
,
j
=
0
,
1
,
.
.
.
,
n
−
1
w_j = \sum_{i=0}^n t_iexp\{j2\pi i/n\}, j =0, 1, ..., n-1
wj=i=0∑ntiexp{j2πi/n},j=0,1,...,n−1其中,
i
i
i为虚数单位。显然,对于n个频域分量,每个频域分量对n个时域分量的函数求和,其复杂度为
Θ
(
n
2
)
\Theta(n^2)
Θ(n2)。
令
β
n
=
e
x
p
{
2
π
i
/
n
}
\beta_n = exp\{2\pi i/n\}
βn=exp{2πi/n},那么频域分量可以表示成
w
j
=
t
0
+
t
2
β
n
2
j
+
.
.
.
+
t
n
−
2
β
n
(
n
−
2
)
j
+
(
t
1
+
t
3
β
n
2
j
+
.
.
.
+
t
n
−
1
β
n
(
n
−
2
)
j
)
β
n
j
\begin{aligned}w_j =& t_0 + t_2\beta_n^{2j} +...+t_{n-2}\beta_n^{(n-2)j} + \\ & (t_1 + t_3\beta_n^{2j} + ... + t_{n-1}\beta_n^{(n-2)j})\beta_n^j\end{aligned}
wj=t0+t2βn2j+...+tn−2βn(n−2)j+(t1+t3βn2j+...+tn−1βn(n−2)j)βnj又
β
n
2
j
=
e
x
p
{
2
j
2
π
i
/
n
}
\beta_n^{2j} = exp\{2j2\pi i/n\}
βn2j=exp{2j2πi/n},
β
n
/
2
j
=
e
x
p
{
j
4
π
i
/
n
}
\beta_{n/2}^j = exp\{j4\pi i/n\}
βn/2j=exp{j4πi/n},即两者相等,因此上式可以写成
w
j
=
t
0
+
t
2
β
n
/
2
j
+
.
.
.
+
t
n
−
2
β
n
/
2
(
n
−
2
)
j
/
2
+
(
t
1
+
t
3
β
n
/
2
j
+
.
.
.
+
t
n
−
1
β
n
/
2
(
n
−
2
)
j
/
2
)
β
n
j
\begin{aligned}w_j = &t_0 + t_2\beta_{n/2}^{j} +...+t_{n-2}\beta_{n/2}^{(n-2)j/2} + \\ & (t_1 + t_3\beta_{n/2}^{j} + ... + t_{n-1}\beta_{n/2}^{(n-2)j/2})\beta_n^j\end{aligned}
wj=t0+t2βn/2j+...+tn−2βn/2(n−2)j/2+(t1+t3βn/2j+...+tn−1βn/2(n−2)j/2)βnj那么令
w
j
0
=
t
0
+
t
2
β
n
/
2
j
+
.
.
.
+
t
n
−
2
β
n
/
2
(
n
−
2
)
j
/
2
w
j
1
=
t
1
+
t
3
β
n
/
2
j
+
.
.
.
+
t
n
−
1
β
n
/
2
(
n
−
2
)
j
/
2
w_{j0} = t_0 + t_2\beta_{n/2}^{j} +...+t_{n-2}\beta_{n/2}^{(n-2)j/2} \\ w_{j1} = t_1 + t_3\beta_{n/2}^{j} + ... + t_{n-1}\beta_{n/2}^{(n-2)j/2}
wj0=t0+t2βn/2j+...+tn−2βn/2(n−2)j/2wj1=t1+t3βn/2j+...+tn−1βn/2(n−2)j/2其中
β
n
/
2
k
j
=
e
x
p
{
2
π
i
n
/
2
⋅
k
j
}
\beta_{n/2}^{kj} = exp\{\frac{2\pi i}{n/2}·kj\}
βn/2kj=exp{n/22πi⋅kj}又因为
e
x
p
{
2
π
i
}
=
c
o
s
2
π
+
i
s
i
n
2
π
=
1
exp\{2\pi i\} = cos2\pi + isin2\pi = 1
exp{2πi}=cos2π+isin2π=1,故有
β
n
/
2
k
j
=
e
x
p
{
2
π
i
n
/
2
⋅
k
j
}
/
e
x
p
{
2
k
π
i
}
=
e
x
p
{
2
π
i
n
/
2
⋅
k
j
−
2
k
π
i
}
=
e
x
p
{
2
π
i
n
/
2
⋅
k
(
j
−
n
/
2
)
}
=
β
n
/
2
k
(
j
−
n
/
2
)
\begin{aligned}\beta_{n/2}^{kj}& = exp\{\frac{2\pi i}{n/2}·kj\}/exp\{2k\pi i\} \\ &= exp\{\frac{2\pi i}{n/2}·kj - 2k\pi i\} \\ &= exp\{\frac{2\pi i}{n/2}·k(j - n/2)\} \\ &= \beta_{n/2}^{k(j-n/2)} \end{aligned}
βn/2kj=exp{n/22πi⋅kj}/exp{2kπi}=exp{n/22πi⋅kj−2kπi}=exp{n/22πi⋅k(j−n/2)}=βn/2k(j−n/2)那么
w
j
0
w_{j0}
wj0对于
j
≥
n
/
2
j \ge n/2
j≥n/2,有
w
j
0
=
t
0
+
t
2
β
n
/
2
j
+
.
.
.
+
t
n
−
2
β
n
/
2
(
n
−
2
)
j
/
2
=
t
0
+
t
2
β
n
/
2
j
−
n
/
2
+
.
.
.
+
t
n
−
2
β
n
/
2
(
n
−
2
)
(
j
−
n
/
2
)
/
2
=
w
(
j
−
n
/
2
)
0
\begin{aligned}w_{j0} &= t_0 + t_2\beta_{n/2}^{j} +...+t_{n-2}\beta_{n/2}^{(n-2)j/2} \\ &= t_0 + t_2\beta_{n/2}^{j-n/2} +...+t_{n-2}\beta_{n/2}^{(n-2)(j-n/2)/2} \\ &= w_{(j-n/2)0}\end{aligned}
wj0=t0+t2βn/2j+...+tn−2βn/2(n−2)j/2=t0+t2βn/2j−n/2+...+tn−2βn/2(n−2)(j−n/2)/2=w(j−n/2)0对于
w
j
1
w_{j1}
wj1同理,那么上述式分治算法的基本思想为:
-将
w
j
w_j
wj拆分为
w
j
0
w_{j0}
wj0与
w
j
1
w_{j1}
wj1;
-递归的求解
w
j
0
w_{j0}
wj0与
w
j
1
w_{j1}
wj1;
-合并为
w
j
w_j
wj,其中
w
j
=
{
w
j
0
+
w
j
1
β
n
j
,
0
≤
j
<
n
/
2
w
(
j
−
n
/
2
)
0
+
w
(
j
−
n
/
2
)
1
β
n
j
,
n
/
2
≤
j
<
n
w_j = \left\{\begin{aligned}&w_{j0}+w_{j1}\beta_{n}^j, &&0 \le j<n/2 \\&w_{(j-n/2)0}+w_{(j-n/2)1}\beta_{n}^j, &&n/2\le j<n \\\end{aligned}\right.
wj={wj0+wj1βnj,w(j−n/2)0+w(j−n/2)1βnj,0≤j<n/2n/2≤j<n因此对于分量为
n
n
n的
w
j
w_j
wj被拆分时,仅需计算
n
/
2
n/2
n/2个
w
j
0
w_{j0}
wj0与
w
j
1
w_{j1}
wj1,以及n个分量
β
n
j
\beta_{n}^j
βnj并迭代。其迭代式为
T
(
n
)
=
{
Θ
(
1
)
,
n
=
2
2
T
(
n
/
2
)
+
Θ
(
n
)
,
n
>
2
T(n)= \left\{\begin{aligned}&\Theta(1), && n = 2 \\ &2T(n/2) + \Theta(n), && n > 2 \end{aligned}\right.
T(n)={Θ(1),2T(n/2)+Θ(n),n=2n>2根据主定理,其算法复杂度为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)。
三、动态规划
动态规划与分治法相似,通过组合子问题的解来求解原问题。通常用来求解最优化问题,这类问题可以有很多可行解,每个解都有一个值,希望寻找具有最优值的解。通常的步骤如下:
-分析一个最优解的结构特征;
-递归的定义最优解的值;
-自底向上的计算最优解的值;
-利用计算信息构造一个最优解。
动态规划重复的利用率关联子问题的解,提升了算法的性能。
3.1 矩阵链乘法算法
对于矩阵
A
1
,
A
2
,
.
.
.
,
A
n
\bm{A}_1, \bm{A}_2, ..., \bm{A}_n
A1,A2,...,An,求计算
A
1
A
2
.
.
.
A
n
\bm{A}_1\bm{A}_2...\bm{A}_n
A1A2...An的最小代价方法。
经典的,对于矩阵乘法
A
p
×
q
B
q
×
r
\bm{A}_{p \times q}\bm{B}_{q \times r}
Ap×qBq×r,计算性能为
O
(
p
q
r
)
O(pqr)
O(pqr);而对于多个矩阵相乘,矩阵乘法满足结合律,那么矩阵链的乘法可以有多种方法。考虑矩阵
A
a
×
b
,
B
b
×
c
,
C
c
×
d
\bm{A}_{a \times b}, \bm{B}_{b \times c}, \bm{C}_{c \times d}
Aa×b,Bb×c,Cc×d,那么有
(
A
B
)
C
=
A
(
B
C
)
(\bm{AB})\bm{C} = \bm{A}(\bm{BC})
(AB)C=A(BC),其中
T
(
(
A
B
)
C
)
=
a
b
c
+
a
c
d
T
(
A
(
B
C
)
)
=
b
c
d
+
a
b
d
T((\bm{AB})\bm{C}) = abc + acd \\ T(\bm{A}(\bm{BC})) = bcd + abd
T((AB)C)=abc+acdT(A(BC))=bcd+abd考虑
a
=
10
,
b
=
100
,
c
=
5
,
d
=
50
a = 10, b = 100, c = 5, d = 50
a=10,b=100,c=5,d=50,那么有
T
(
(
A
B
)
C
)
=
7500
,
T
(
A
(
B
C
)
)
=
750000
T((\bm{AB})\bm{C}) = 7500, T(\bm{A}(\bm{BC})) = 750000
T((AB)C)=7500,T(A(BC))=750000。显然,不同的计算顺序有不同的代价。
为了在矩阵链乘法的解空间中寻找最优解,考虑穷举法,设
p
(
n
)
p(n)
p(n)是计算n个矩阵乘积的方法数,那么
p
(
n
)
p(n)
p(n)的递归方程为
p
(
n
)
=
{
1
,
n
=
1
∑
k
=
1
n
−
1
p
(
k
)
p
(
n
−
k
)
,
n
>
1
p(n) = \left\{\begin{aligned}&1, && n = 1 \\ &\sum_{k=1}^{n-1}p(k)p(n-k), && n > 1 \\\end{aligned}\right.
p(n)=⎩⎪⎪⎨⎪⎪⎧1,k=1∑n−1p(k)p(n−k),n=1n>1根据组合数学,
p
(
n
)
=
Ω
(
4
n
/
n
3
/
2
)
p(n) = \Omega(4^n/n^{3/2})
p(n)=Ω(4n/n3/2),如此之大的解空间无法用枚举方法求出最优解。
再考虑矩阵链乘法的动态规划算法。首先分析优化解的结构特征,定义
A
i
.
.
.
j
=
A
i
A
i
+
1
.
.
.
A
j
\bm{A}_{i...j} = \bm{A}_i\bm{A}_{i+1}...\bm{A}_j
Ai...j=AiAi+1...Aj,并使用
T
(
i
,
j
)
T(i,j)
T(i,j)表示计算
A
i
.
.
.
j
\bm{A}_{i...j}
Ai...j的性能。那么对于计算
A
1...
n
=
A
1...
k
A
k
+
1...
n
\bm{A}_{1...n} = \bm{A}_{1...k}\bm{A}_{k+1...n}
A1...n=A1...kAk+1...n,要保证
k
k
k的选取使其为最优解,需要要求子问题
A
1...
k
\bm{A}_{1...k}
A1...k与
A
k
+
1...
n
\bm{A}_{k+1...n}
Ak+1...n都是一个最优解。
要注意的是,矩阵链乘法的分解会出现重叠的子问题,典型的,考虑
A
1...4
\bm{A}_{1...4}
A1...4,其可以分解成子问题
A
1
A
2...4
\bm{A}_{1}\bm{A}_{2...4}
A1A2...4或
A
1...2
A
3...4
\bm{A}_{1...2}\bm{A}_{3...4}
A1...2A3...4,而
A
2...4
\bm{A}_{2...4}
A2...4又可以分解出子问题
A
2
A
3...4
\bm{A}_2\bm{A}_{3...4}
A2A3...4,可以看出在不同的分解中都出现了
A
3...4
\bm{A}_{3...4}
A3...4的计算,造成了子问题的重叠,重复利用这种重叠的子问题的结果,就可以提高算法的性能。
那么对于某个
k
k
k,使得
A
i
.
.
.
j
=
A
i
.
.
.
k
A
k
+
1...
j
\bm{A}_{i...j} = \bm{A}_{i...k}\bm{A}_{k+1...j}
Ai...j=Ai...kAk+1...j,可以看作两个子矩阵的乘法,其运算性能有
T
(
i
,
j
)
=
T
(
i
,
k
)
+
T
(
k
+
1
,
j
)
+
p
q
r
T(i,j) = T(i,k) + T(k+1,j) + pqr
T(i,j)=T(i,k)+T(k+1,j)+pqr其中
p
,
q
,
r
p, q, r
p,q,r分别是子问题矩阵的行列参数。那么考虑所有的
k
k
k,有
T
(
i
,
j
)
=
{
0
,
i
=
j
m
i
n
i
≤
k
<
j
{
T
(
i
,
k
)
+
T
(
k
+
1
,
j
)
+
p
q
r
}
,
i
<
j
T(i,j) = \left\{\begin{aligned}&0, &&i = j \\ &min_{i \le k < j}\{T(i,k) + T(k+1,j) + pqr\}, && i < j \\\end{aligned}\right.
T(i,j)={0,mini≤k<j{T(i,k)+T(k+1,j)+pqr},i=ji<j并自底向上的计算所有
T
(
i
,
j
)
,
∀
i
,
j
,
k
T(i, j), \forall i, j,k
T(i,j),∀i,j,k,并在向上计算的过程中使用底层重叠子问题的运算结果,直到得到所有的
T
T
T,就可以利用计算信息构造出最优解。其对
i
,
j
,
k
i, j, k
i,j,k进行了迭代,时间性能为
O
(
n
3
)
O(n^3)
O(n3),而需要使用矩阵存储重叠子问题的运算结果
T
T
T,空间性能为
O
(
n
2
)
O(n^2)
O(n2)。
3.2 最长公共子序列问题
考虑序列
X
=
(
x
1
,
.
.
.
,
x
m
)
X = (x_1, ..., x_m)
X=(x1,...,xm)与
Y
=
(
y
1
,
y
2
,
.
.
.
,
y
n
)
Y = (y_1, y_2, ... , y_n)
Y=(y1,y2,...,yn),若存在
(
y
1
,
y
2
,
.
.
.
,
y
n
)
=
(
x
i
,
x
i
+
1
,
.
.
.
,
x
i
+
n
−
1
)
(y_1, y_2, ... , y_n) = (x_{i}, x_{i+1}, ..., x_{i+n-1})
(y1,y2,...,yn)=(xi,xi+1,...,xi+n−1),则称
Y
Y
Y是
X
X
X的子序列。那么对于多个序列,求这些序列的最长公共子序列的最小代价方法。
首先分析最长公共子序列的结构,使用
X
i
X_i
Xi表示
(
x
1
,
.
.
.
,
x
i
)
(x_1, ..., x_i)
(x1,...,xi),使用
L
C
S
X
LCS_X
LCSX表示
X
X
X的子序列。考虑
Z
=
Z
k
Z = Z_k
Z=Zk是序列
X
=
X
m
X = X_m
X=Xm与
Y
=
Y
n
Y = Y_n
Y=Yn的公共子序列,那么有:
-如果
x
m
=
y
n
x_m = y_n
xm=yn,那么必然有
x
m
=
y
n
=
z
k
x_m = y_n = z_k
xm=yn=zk,且
Z
k
−
1
Z_{k-1}
Zk−1是
L
C
S
X
m
−
1
Y
n
−
1
LCS_{X_{m-1}Y_{n-1}}
LCSXm−1Yn−1,且
L
C
S
X
Y
=
L
C
S
X
m
−
1
Y
n
−
1
∧
(
x
m
=
y
n
)
LCS_{XY} = LCS_{X_{m-1}Y_{n-1}} \wedge (x_m = y_n)
LCSXY=LCSXm−1Yn−1∧(xm=yn) -如果
x
m
≠
y
n
x_m \ne y_n
xm=yn且
z
k
≠
x
m
z_k \ne x_m
zk=xm,那么
Z
Z
Z应该是
L
C
S
X
m
−
1
Y
LCS_{X_{m-1}Y}
LCSXm−1Y;
-如果
x
m
≠
y
n
x_m \ne y_n
xm=yn且
z
k
≠
y
n
z_k \ne y_n
zk=yn,那么
Z
Z
Z应该是
L
C
S
X
Y
n
−
1
LCS_{XY_{n-1}}
LCSXYn−1。
综上所述,公共子序列的优化解结构为
L
C
S
X
Y
=
{
L
C
S
X
m
−
1
Y
n
−
1
+
z
k
,
x
m
=
y
n
L
C
S
X
m
−
1
Y
,
x
m
≠
y
n
,
z
k
≠
x
m
L
C
S
X
Y
n
−
1
,
x
m
≠
y
n
,
z
k
≠
y
n
LCS_{XY} = \left\{\begin{aligned} &LCS_{X_{m-1}Y_{n-1}}+z_k, && x_m = y_n\\ &LCS_{X_{m-1}Y}, && x_m \ne y_n, z_k \ne x_m\\ &LCS_{XY_{n-1}}, && x_m \ne y_n, z_k \ne y_n\\ \end{aligned}\right.
LCSXY=⎩⎪⎨⎪⎧LCSXm−1Yn−1+zk,LCSXm−1Y,LCSXYn−1,xm=ynxm=yn,zk=xmxm=yn,zk=yn且有一定的子问题重叠性,从而可以使用动态规划,记
C
i
j
C_{ij}
Cij是
L
C
S
X
i
Y
j
LCS_{X_iY_j}
LCSXiYj的长度,那么LCS长度的递归方程为
C
i
j
=
{
0
,
i
=
0
o
r
j
=
0
C
i
−
1
,
j
−
1
,
i
,
j
>
0
,
x
i
=
y
j
m
a
x
{
C
i
,
j
−
1
,
C
i
−
1
,
j
}
,
i
,
j
>
0
,
x
i
≠
y
j
C_{ij} = \left\{\begin{aligned} &0, && i = 0\ or\ j = 0 \\ &C_{i-1, j-1}, &&i, j >0, x_i = y_j \\ &max\{C_{i, j-1}, C_{i-1, j}\}, &&i, j >0, x_i \ne y_j \\ \end{aligned}\right.
Cij=⎩⎪⎨⎪⎧0,Ci−1,j−1,max{Ci,j−1,Ci−1,j},i=0 or j=0i,j>0,xi=yji,j>0,xi=yj并使用矩阵数据结构自底向上的计算
C
i
j
,
∀
i
,
j
C_{ij}, \forall i, j
Cij,∀i,j。其算法时间性能为
O
(
m
n
)
O(mn)
O(mn),空间性能为
O
(
m
n
)
O(mn)
O(mn)。
四、贪心算法
对于许多优化问题,使用动态规划算法来求最优解是不必要的,可以使用更简单、更高效的算法,即贪心算法,其总是做出局部最优的选择,并寄希望这样的选择能导致全局最优解。贪心算法并不保证得到最优解,但对很多问题确实可以求得最优解。对于贪心算法求解的问题,其需要具有:
-贪心选择性,可以通过做出局部最优选择来构造全局最优解。
-最优子结构,一个问题的最优解就是其子问题的最优解。
4.1 活动选择问题
设n个活动的集合
S
=
{
a
1
,
.
.
.
,
a
n
}
S = \{a_1, ..., a_n\}
S={a1,...,an},各个活动共用同一个资源,这个资源在某个时刻只能供一个活动使用。每个活动
a
i
a_i
ai都有一个开始时间
s
i
s_i
si与结束时间
f
i
f_i
fi,且满足
0
≤
s
i
<
f
i
<
∞
0 \le s_i < f_i < \infty
0≤si<fi<∞。活动
a
i
a_i
ai发生在开区间
[
s
i
,
f
i
)
[s_i, f_i)
[si,fi)期间,且如果两个活动的时间区间不重叠,则称其是相容的。那么考虑一个活动集合
S
S
S及其对应的区间
F
F
F,试找出最大的相容集合,使得集合中的活动数最多。
活动选择问题具有最优子结构性质。令
S
i
j
S_{ij}
Sij表示在
a
i
a_i
ai结束后开始,并在
a
j
a_j
aj结束之前结束的活动的集合。假设求
S
i
j
S_{ij}
Sij的最大的互相兼容的活动子集
A
i
j
A_{ij}
Aij,其包含活动
a
k
a_k
ak。由于最优解包含活动
a
k
a_k
ak,可以得到两个子问题:
-寻找
S
i
k
S_{ik}
Sik的兼容活动集,令
A
i
k
=
A
i
j
∩
S
i
k
A_{ik} = A_{ij} \cap S_{ik}
Aik=Aij∩Sik;
-寻找
S
k
j
S_{kj}
Skj的兼容活动集,令
A
k
j
=
A
i
j
∩
S
k
j
A_{kj} = A_{ij} \cap S_{kj}
Akj=Aij∩Skj。
那么有
A
i
j
=
A
i
k
∪
a
k
∪
A
k
j
A_{ij} = A_{ik} \cup {a_k} \cup A_{kj}
Aij=Aik∪ak∪Akj,且
S
i
j
S_{ij}
Sij的最大兼容任务子集
A
i
j
A_{ij}
Aij包含
∣
A
i
j
∣
=
∣
A
i
k
∣
+
∣
A
k
j
∣
+
1
|A_{ij}| = |A_{ik}| + |A_{kj}| + 1
∣Aij∣=∣Aik∣+∣Akj∣+1个活动。这就使得最优解
A
i
j
A_{ij}
Aij必然包括两个子问题
S
i
k
S_{ik}
Sik与
S
k
j
S_{kj}
Skj的最优解。
考虑任意非空子问题
S
k
S_k
Sk,
a
m
a_m
am是
S
k
S_k
Sk中结束时间最早的活动,则
a
m
a_m
am在
S
k
S_k
Sk的某个最大兼容活动集中。
考察该定理,令
A
k
A_k
Ak是
S
k
S_k
Sk的最大兼容活动集,
a
n
a_n
an是
A
k
A_k
Ak中结束时间最早的活动,
a
m
a_m
am是
S
k
S_k
Sk中结束时间最早的活动。若
a
n
=
a
m
a_n = a_m
an=am,则得证;否则,令
A
k
′
=
A
k
−
{
a
n
}
∪
{
a
m
}
A_k' = A_k - \{a_n\} \cup \{a_m\}
Ak′=Ak−{an}∪{am}由于
A
k
A_k
Ak的活动是相容的,且
f
m
≤
f
n
f_m \le f_n
fm≤fn,故
A
k
′
A_k'
Ak′的活动也是相容的。故有
∣
A
k
∣
=
∣
A
k
′
∣
|A_k| = |A_k'|
∣Ak∣=∣Ak′∣,即
A
k
′
A_k'
Ak′也是一个最大兼容活动集,且包含
S
k
S_k
Sk中结束时间最早的活动
a
m
a_m
am。
根据该定理,虽然可以使用动态规划来求解活动选择问题,但并不需要这样做,相反,可以反复选择最早结束的活动,保留与此活动兼容的活动,知道不再有剩余活动。
贪心算法通常都是这种自顶向下的设计:做出一个选择,然后求解剩下的那些子问题,而不是自底向上的求解出很多字问题,然后再做出选择。
4.2 哈夫曼编码问题
哈夫曼【Huffman】编码是一种可变长的前缀编码,可以有效的压缩数据。哈夫曼设计了一个贪心算法来构造最优前缀码,其实一棵二叉树:
-树的叶结点用字符及其出现的频率标记;
-树的内结点用其子树的叶节点的频率和标记;
-树的边用编码标记。
那么从树的根到某个字符的叶经过的边,就得到了该字符的编码,且满足可变长、是前缀编码。且对于字符表
C
C
C,
∀
c
∈
C
\forall c\in C
∀c∈C,
f
(
c
)
f(c)
f(c)是
c
c
c在数据中出现的频率,
d
T
(
c
)
d_T(c)
dT(c)是叶节点
c
c
c在树中的深度即编码长度,那么树
T
T
T的权和就是编码一个数据所需的位数,为
B
(
T
)
=
∑
c
∈
C
f
(
c
)
d
T
(
c
)
B(T) = \sum_{c \in C}f(c)d_T(c)
B(T)=c∈C∑f(c)dT(c)那么对于输入的字符表
C
C
C与频率表
F
F
F,试求具有最小权和的最优编码树。
其贪心算法思想为,循环的选择具有最低频率的两个字符作为叶节点,形成具有频率和的子树作为新的节点,知道所有字符都成为叶节点,形成树。
首先证明哈夫曼算法的贪心选择性。给定字符表
C
C
C,
∀
c
∈
C
\forall c \in C
∀c∈C,
f
(
C
)
f(C)
f(C)是
c
c
c在文件中出现的频率,设
x
,
y
x, y
x,y是
C
C
C中出现频率最低的数据,那么存在
C
C
C的一个最优前缀码,使得
x
x
x与
y
y
y的码字长度相同,且只有最后一个二进制位不同。
考察该定理,设树
T
T
T是最优前缀编码树,
x
′
x'
x′与
y
′
y'
y′是
T
T
T中深度最大的兄弟结点,并假定
f
(
x
′
)
≤
f
(
y
′
)
f(x') \le f(y')
f(x′)≤f(y′)及
f
(
x
)
≤
f
(
y
)
f(x)\le f(y)
f(x)≤f(y)。由于
x
,
y
x, y
x,y是
C
C
C中出现频率最低的数据,必有
f
(
x
)
≤
f
(
x
′
)
f(x) \le f(x')
f(x)≤f(x′),
f
(
y
)
≤
f
(
y
′
)
f(y) \le f(y')
f(y)≤f(y′)。
交换叶节点
x
x
x与
x
′
x'
x′可以形成新的树
T
′
T'
T′,那么有
B
(
T
)
−
B
(
T
′
)
=
∑
c
∈
C
f
(
c
)
d
T
(
c
)
−
∑
c
∈
C
f
(
c
)
d
T
′
(
c
)
\begin{aligned}B(T) - B(T') & = \sum_{c \in C}f(c)d_T(c) - \sum_{c \in C}f(c)d_{T'}(c) \\ \end{aligned}
B(T)−B(T′)=c∈C∑f(c)dT(c)−c∈C∑f(c)dT′(c)其中
∑
c
∈
C
f
(
c
)
d
T
(
c
)
=
∑
c
∈
C
−
{
x
,
x
′
}
f
(
c
)
d
T
(
c
)
+
f
(
x
)
d
T
(
x
)
+
f
(
x
′
)
d
T
(
x
′
)
\sum_{c \in C}f(c)d_T(c) = \sum_{c \in C-\{x, x'\}}f(c)d_T(c) + f(x)d_T(x) + f(x')d_{T}(x')
c∈C∑f(c)dT(c)=c∈C−{x,x′}∑f(c)dT(c)+f(x)dT(x)+f(x′)dT(x′)故
B
(
T
)
−
B
(
T
′
)
=
∑
c
∈
C
f
(
c
)
d
T
(
c
)
−
∑
c
∈
C
f
(
c
)
d
T
′
(
c
)
=
f
(
x
)
d
T
(
x
)
+
f
(
x
′
)
d
T
(
x
′
)
−
f
(
x
)
d
T
′
(
x
)
−
f
(
x
′
)
d
T
′
(
x
′
)
\begin{aligned}B(T) - B(T') & = \sum_{c \in C}f(c)d_T(c) - \sum_{c \in C}f(c)d_{T'}(c) \\ & = f(x)d_T(x) + f(x')d_{T}(x') - f(x)d_{T'}(x) - f(x')d_{T'}(x') \end{aligned}
B(T)−B(T′)=c∈C∑f(c)dT(c)−c∈C∑f(c)dT′(c)=f(x)dT(x)+f(x′)dT(x′)−f(x)dT′(x)−f(x′)dT′(x′)由于两个树交换叶节点,故有
d
T
(
x
)
=
d
T
′
(
x
′
)
,
d
T
(
x
′
)
=
d
T
′
(
x
)
d_T(x) = d_{T'}(x'), d_{T}(x') = d_{T'}(x)
dT(x)=dT′(x′),dT(x′)=dT′(x),那么
B
(
T
)
−
B
(
T
′
)
=
f
(
x
)
d
T
(
x
)
+
f
(
x
′
)
d
T
(
x
′
)
−
f
(
x
)
d
T
′
(
x
)
−
f
(
x
′
)
d
T
′
(
x
′
)
=
f
(
x
)
d
T
(
x
)
+
f
(
x
′
)
d
T
(
x
′
)
−
f
(
x
)
d
T
(
x
′
)
−
f
(
x
′
)
d
T
(
x
)
=
(
f
(
x
′
)
−
f
(
x
)
)
(
d
T
(
x
′
)
−
d
T
(
x
)
)
\begin{aligned}B(T) - B(T') & = f(x)d_T(x) + f(x')d_{T}(x') - f(x)d_{T'}(x) - f(x')d_{T'}(x') \\ & = f(x)d_T(x) + f(x')d_{T}(x') - f(x)d_{T}(x') - f(x')d_T(x) \\ & = (f(x') - f(x))(d_T(x') - d_T(x)) \end{aligned}
B(T)−B(T′)=f(x)dT(x)+f(x′)dT(x′)−f(x)dT′(x)−f(x′)dT′(x′)=f(x)dT(x)+f(x′)dT(x′)−f(x)dT(x′)−f(x′)dT(x)=(f(x′)−f(x))(dT(x′)−dT(x))注意到
f
(
x
)
≤
f
(
x
′
)
f(x) \le f(x')
f(x)≤f(x′)以及
x
′
x'
x′是最深的叶节点,有
d
T
(
x
′
)
≤
d
T
(
x
)
d_T(x') \le d_T(x)
dT(x′)≤dT(x),那么有
B
(
T
)
−
B
(
T
′
)
≥
0
B(T) - B(T') \ge 0
B(T)−B(T′)≥0同理,
T
′
T'
T′交换叶节点
y
y
y与
y
′
y'
y′形成的新的树
T
′
′
T''
T′′,有
B
(
T
)
≥
B
(
T
′
)
≥
B
(
T
′
′
)
B(T) \ge B(T') \ge B(T'')
B(T)≥B(T′)≥B(T′′)又
T
T
T是最优前缀编码树,则有
B
(
T
)
≤
B
(
T
′
′
)
B(T) \le B(T'')
B(T)≤B(T′′),所以有
B
(
T
)
=
B
(
T
′
′
)
B(T) = B(T'')
B(T)=B(T′′),即
x
x
x与
y
y
y就是深度最深的兄弟叶节点。因此频率最低的字符就是哈夫曼算法的局部最优解。
再考虑哈夫曼编码的优化子结构,给定字符表
C
C
C,
∀
c
∈
C
\forall c \in C
∀c∈C,
f
(
C
)
f(C)
f(C)是
c
c
c在文件中出现的频率,设
x
,
y
x, y
x,y是
C
C
C中出现频率最低的数据,
z
z
z是其父节点,其频率是
f
(
z
)
=
f
(
x
)
+
f
(
y
)
f(z) = f(x) + f(y)
f(z)=f(x)+f(y)。若
T
′
T'
T′是字符表
C
′
=
C
−
{
x
,
y
}
∪
{
z
}
C' = C - \{x, y\} \cup \{z\}
C′=C−{x,y}∪{z}的最优前缀编码树,那么
T
=
T
′
−
{
z
}
∪
{
x
,
y
}
T = T' - \{z\} \cup \{x, y\}
T=T′−{z}∪{x,y}是
C
C
C的最优前缀编码树。
考察该定理,
∀
c
∈
C
−
{
x
,
y
}
\forall c \in C - \{x, y\}
∀c∈C−{x,y},那么有
d
T
(
c
)
=
d
T
′
(
c
)
d_T(c) = d_{T'}(c)
dT(c)=dT′(c),因此有
f
(
c
)
d
T
(
c
)
=
f
(
c
)
d
T
′
(
c
)
f(c)d_T(c) = f(c)d_{T'}(c)
f(c)dT(c)=f(c)dT′(c)由于
d
T
(
x
)
=
d
T
(
y
)
=
d
T
′
(
z
)
+
1
d_T(x) = d_T(y) = d_{T'}(z) + 1
dT(x)=dT(y)=dT′(z)+1,有
f
(
x
)
d
T
(
x
)
+
f
(
y
)
d
T
(
y
)
=
(
f
(
x
)
+
f
(
y
)
)
(
d
T
′
(
z
)
+
1
)
=
f
(
z
)
d
T
′
(
z
)
+
f
(
x
)
+
f
(
y
)
\begin{aligned} f(x)d_T(x) + f(y)d_T(y) & = (f(x) + f(y))(d_{T'}(z) + 1) \\ &= f(z)d_{T'}(z) + f(x) + f(y) \end{aligned}
f(x)dT(x)+f(y)dT(y)=(f(x)+f(y))(dT′(z)+1)=f(z)dT′(z)+f(x)+f(y)即
B
(
T
)
=
∑
c
∈
C
f
(
c
)
d
T
(
c
)
=
∑
c
∈
C
−
{
x
,
y
}
f
(
c
)
d
T
(
c
)
+
f
(
x
)
d
T
(
x
)
+
f
(
y
)
d
T
(
y
)
=
∑
c
∈
C
−
{
x
,
y
}
f
(
c
)
d
T
(
c
)
+
f
(
z
)
d
T
′
(
z
)
+
f
(
x
)
+
f
(
y
)
=
∑
c
∈
C
−
{
x
,
y
}
∪
{
z
}
f
(
c
)
d
T
(
c
)
+
f
(
x
)
+
f
(
y
)
=
B
(
T
′
)
+
f
(
x
)
+
f
(
y
)
\begin{aligned}B(T)& = \sum_{c \in C}f(c)d_T(c) \\ &= \sum_{c \in C - \{x, y\}}f(c)d_T(c) + f(x)d_T(x) + f(y)d_T(y) \\ &= \sum_{c \in C - \{x, y\}}f(c)d_T(c) + f(z)d_{T'}(z) + f(x) + f(y) \\ &= \sum_{c \in C - \{x, y\} \cup \{z\}}f(c)d_T(c) + f(x) + f(y) \\ &= B(T') + f(x) + f(y) \end{aligned}
B(T)=c∈C∑f(c)dT(c)=c∈C−{x,y}∑f(c)dT(c)+f(x)dT(x)+f(y)dT(y)=c∈C−{x,y}∑f(c)dT(c)+f(z)dT′(z)+f(x)+f(y)=c∈C−{x,y}∪{z}∑f(c)dT(c)+f(x)+f(y)=B(T′)+f(x)+f(y)亦即
B
(
T
′
)
=
B
(
T
)
−
f
(
x
)
−
f
(
y
)
B(T') = B(T) - f(x) - f(y)
B(T′)=B(T)−f(x)−f(y)那么,假定
T
T
T不是
C
C
C的最优前缀编码树,即
∃
T
∗
\exists T^*
∃T∗,使得
B
(
T
∗
)
<
B
(
T
)
B(T^*) < B(T)
B(T∗)<B(T),不失一般性的,参考哈夫曼编码的贪心选择性,
T
∗
T^*
T∗一定包含频率最低的字符叶结点
x
,
y
x, y
x,y,令
T
′
∗
=
T
∗
−
{
x
,
y
}
T'^* = T^* - \{x, y\}
T′∗=T∗−{x,y},于是
B
(
T
′
∗
)
=
B
(
T
∗
)
−
f
(
x
)
−
f
(
y
)
<
B
(
T
)
−
f
(
x
)
−
f
(
y
)
=
B
(
T
′
)
B(T'^*) = B(T^*) - f(x) - f(y) < B(T) - f(x) - f(y) = B(T')
B(T′∗)=B(T∗)−f(x)−f(y)<B(T)−f(x)−f(y)=B(T′)即此时,
T
′
T'
T′将不是
C
′
C'
C′的最优前缀编码树,与假设矛盾。
综上所述,哈夫曼算法会生成一个最优前缀码。
五、字符串匹配
字符串匹配的形式化定义如下:设长为
n
n
n的字符数组
T
[
1...
n
]
T[1...n]
T[1...n],模式是一个长度为
m
m
m的字符数组
P
[
1...
m
]
P[1...m]
P[1...m],其中
m
≤
n
m \le n
m≤n,且
T
T
T与
P
P
P的元素都来源于字符集
Σ
\Sigma
Σ,若
∃
s
,
0
≤
s
≤
n
−
m
\exists s, 0 \le s \le n-m
∃s,0≤s≤n−m,使得
T
[
s
+
1...
s
+
m
]
=
P
[
1...
m
]
T[s+1...s+m] = P[1...m]
T[s+1...s+m]=P[1...m],称模式
P
P
P在
T
T
T中匹配,称
s
s
s为有效偏移。
朴素的匹配算法为暴力算法,其时间性能为
O
(
n
−
m
)
O(n-m)
O(n−m),最差时间性能为
Θ
(
(
n
−
m
+
1
)
m
)
\Theta((n-m+1)m)
Θ((n−m+1)m)。实际上,还有许多巧妙的、性能优于暴力算法的算法。
5.1 拉宾-卡普算法
拉宾-卡普【Rabin-Karp】算法基于初等数论,为了便于说明,设
Σ
=
{
0
,
1
,
.
.
.
,
9
}
\Sigma = \{0, 1, ..., 9\}
Σ={0,1,...,9},那么就可以使用长度为
∣
Σ
∣
|\Sigma|
∣Σ∣的十进制数表示
∣
Σ
∣
|\Sigma|
∣Σ∣个连续字符的字符串。给定模式
P
P
P,令
p
p
p表示
P
P
P对应的数值,同样的,
t
s
t_s
ts表示
T
[
s
+
1...
s
+
m
]
T[s+1...s+m]
T[s+1...s+m]对应的数值,那么当
P
P
P在
T
T
T在
s
s
s有效偏移下,有
p
=
t
s
p = t_s
p=ts。
计算
p
p
p与
t
0
t_0
t0,有
p
=
P
[
m
]
+
∣
Σ
∣
(
P
[
m
−
1
]
+
∣
Σ
∣
(
P
[
m
−
2
]
+
.
.
.
+
∣
Σ
∣
(
P
[
2
]
+
∣
Σ
∣
P
[
1
]
)
.
.
.
)
)
t
0
=
T
[
m
]
+
∣
Σ
∣
(
T
[
m
−
1
]
+
∣
Σ
∣
(
T
[
m
−
2
]
+
.
.
.
+
∣
Σ
∣
(
T
[
2
]
+
∣
Σ
∣
T
[
1
]
)
.
.
.
)
)
p = P[m] + |\Sigma|(P[m-1] + |\Sigma|(P[m-2] + ... + |\Sigma|(P[2] + |\Sigma|P[1])...)) \\ t_0 = T[m] + |\Sigma|(T[m-1] + |\Sigma|(T[m-2] + ... + |\Sigma|(T[2] + |\Sigma|T[1])...))
p=P[m]+∣Σ∣(P[m−1]+∣Σ∣(P[m−2]+...+∣Σ∣(P[2]+∣Σ∣P[1])...))t0=T[m]+∣Σ∣(T[m−1]+∣Σ∣(T[m−2]+...+∣Σ∣(T[2]+∣Σ∣T[1])...))且易得计算该值的时间性能为
Θ
(
m
)
\Theta(m)
Θ(m)。而计算
t
s
t_s
ts,可以从
t
s
t_s
ts与
t
s
+
1
t_{s+1}
ts+1的数值关系易得
t
s
+
1
=
∣
Σ
∣
(
t
s
−
∣
Σ
∣
m
−
1
T
[
s
+
1
]
)
+
T
[
s
+
m
+
1
]
t_{s+1} = |\Sigma|(t_s - |\Sigma|^{m-1}T[s+1]) + T[s + m +1]
ts+1=∣Σ∣(ts−∣Σ∣m−1T[s+1])+T[s+m+1]每个上述计算的时间性能为
Θ
(
1
)
\Theta(1)
Θ(1),那么计算
t
t
t的时间性能为
Θ
(
n
−
m
+
1
)
\Theta(n - m + 1)
Θ(n−m+1),并进行时间性能为
Θ
(
(
n
−
m
+
1
)
m
)
\Theta((n - m + 1)m)
Θ((n−m+1)m)的比较即可得到匹配。虽然拉宾-卡普算法的最坏时间性能不比朴素算法好,但是就平均情况和实际情况来说,该算法效果要好得多。
然而,若
p
p
p与
t
t
t的值过大,那么算术运算使用常数时间性能衡量是不合理的。不过,使用初等数论可以解决这个问题:选取一个合适的模
q
q
q来计算
p
p
p和
t
s
t_s
ts的模。对于字母表
Σ
\Sigma
Σ,选取
q
q
q使得
∣
Σ
∣
q
|\Sigma|q
∣Σ∣q在一个计算机的字长内,并调整递归式
t
s
+
1
=
(
∣
Σ
∣
(
t
s
−
T
[
s
+
1
]
h
)
+
T
[
s
+
m
+
1
]
)
m
o
d
q
h
=
∣
Σ
∣
m
−
1
m
o
d
q
t_{s+1} = (|\Sigma|(t_s - T[s + 1]h) + T[s + m + 1])mod\ q \\ h = |\Sigma|^{m-1}mod\ q
ts+1=(∣Σ∣(ts−T[s+1]h)+T[s+m+1])mod qh=∣Σ∣m−1mod q但是基于模
q
q
q得到的结果并不完美:
t
s
m
o
d
q
=
p
m
o
d
q
t_s\ mod\ q = p\ mod\ q
ts mod q=p mod q不能说明
t
s
=
p
t_s = p
ts=p,但反正,模不相等是一定不相等的。因此,可以将模相等作为快速的启发式测试方法用于检测无效偏移
s
s
s,任何满足模相等的偏移
s
s
s还需要进一步检测是真的有效还是一个伪命中点,而这项额外的测试只需要检测
T
[
s
+
1...
s
+
m
]
=
P
[
1...
m
]
T[s+1...s+m] = P[1...m]
T[s+1...s+m]=P[1...m]来完成,且当
q
q
q足够大时,伪命中点会尽量少的出现。
5.2 克努特-莫里斯-普拉特算法
克努特-莫里斯-普拉特【Knuth-Morris-Pratt,KMP】算法是一种线性时间字符串匹配算法,其需要一个辅助函数
π
\pi
π,使用
Θ
(
m
)
\Theta(m)
Θ(m)的时间计算
π
(
T
)
=
π
[
1...
m
]
\pi(T) = \pi[1...m]
π(T)=π[1...m]。算法步骤详见线性表【4.5 KMP算法】。