少女祈祷中...

基础知识

算法基础

增量方法

插入排序
1
2
3
4
5
6
7
8
INSERTION-SORT(A)
for j = 2 to A.length
key = A[j]
i = j - 1
while i > 0 and A[j] > key
A[i + 1] = A[i]
i = i - 1
A[i + 1] = key
循环不变式

循环不变式主要用来帮助我们理解算法的正确性。关于循环不变式,我们必须证明三条性质:

  • 初始化:循环的第一次迭代之前,它为真
  • 保持:如果循环的某次迭代之前它为真,那么下次迭代之前它仍然为真
  • 终止:在循环终止时,不变式为我们提供一个有用的性质,该性质有助于证明算法是正确的

分治法

分治法的思想:将原问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后再合并这些子问题的解来建立原问题的解
分治模式在每层递归时都有三个步骤:

  • 分解原问题为若干子问题,这些子问题是原问题的规模较小的实例
  • 解决这些子问题,递归地求解各子问题。然而,若子问题的规模足够小,则直接求解
  • 合并这些子问题的解成原问题的解
归并排序
  • 分解:分解待排序的n个元素的序列成各具n/2个元素的两个子序列
  • 解决:使用归并排序递归地排序两个子序列
  • 合并:合并两个已排序的子序列以产生已排序的答案

我们通过调用一个辅助过程 MERGE(A, p, q, r) 来完成合并,其中AA是一个数组, ppqqrr是数组下标,满足pq<rp \leq q < r。该过程假设子数组A[p..q]A[p..q]A[q+1..r]A[q+1..r]都已排好序
过程 MERGE 需要Θ(n)\Theta(n)的时间,其中n=rp+1n=r-p+1是带合并元素的总数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
MERGE(A, p, q, r)
n1 = q - p + 1
n2 = r - q
let L[1..n1 + 1] and R[1..n2 + 1] be new arrays
for i = 1 to n1
L[i] = A[p + i - 1]
for j = 1 to n2
R[j] = A[q + j]
L[n1 + 1] = ∞
R[n2 + 1] = ∞
i = 1
j = 1
for k = p to r
if L[i] <= R[j]
A[k] = L[i]
i = i + 1
else
A[k] = R[j]
j = j + 1
1
2
3
4
5
6
MERGE-SORT(A, p, r)
if p < r
q = ⌊(p + r)/2⌋
MERGE-SORT(A, p, q)
MERGE-SORT(A, q+1, r)
MERGE(A, p, q, r)
分析分治算法

假设T(n)T(n)是规模为nn的一个问题的运行时间:

  • 若问题规模足够小,则直接求解需要常量时间Θ(1)\Theta(1)
  • 假设把原问题分解为aa个子问题,每个子问题的规模是原问题的1/b1/b。为了求解一个规模为n/bn/b的子问题,需要T(n/b)T(n/b)的时间,所以需要aT(n/b)aT(n/b)的时间来求解aa个子问题
  • 分解问题成子问题需时间D(n)D(n)
  • 合并子问题的解成原问题的解需时间C(n)C(n)

那么得到递归式:

T(n)={Θ(1)ncaT(n/b)+D(n)+C(n)otherT(n) = \begin{cases} \Theta(1) \qquad & n \leq c \\ aT(n/b)+D(n)+C(n) \qquad & other \end{cases}

对于归并排序,其最坏情况运行时间T(n)T(n)的递归式:

T(n)={Θ(1)n=12T(n/2)+Θ(n)n>1=Θ(nlgn)T(n) = \begin{cases} \Theta(1) \qquad & n = 1 \\ 2T(n/2)+\Theta(n) \qquad & n>1 \end{cases} = \Theta(nlgn)

函数增长

渐进记号

Θ\Theta记号

对于一个给定的函数g(n)g(n),用Θ(g(n))\Theta(g(n))来表示以下函数的集合:
Θ(g(n))={f(n):存在正常量c1c2n0,使得对所有nn0,有0c1g(n)f(n)c2g(n)}\Theta(g(n))=\{f(n):\text{存在正常量}c_1\text{、}c_2\text{和}n_0\text{,使得对所有}n \geq n_0\text{,有}0\leq c_1g(n) \leq f(n) \leq c_2g(n)\}

我们称g(n)g(n)f(n)f(n)的一个渐进紧确界

OO记号

对于一个给定的函数g(n)g(n),用O(g(n))O(g(n))来表示以下函数的集合:
O(g(n))={f(n):存在正常量cn0,使得对所有nn0,有0f(n)cg(n)}O(g(n))=\{f(n):\text{存在正常量}c\text{和}n_0\text{,使得对所有}n \geq n_0\text{,有}0\leq f(n) \leq cg(n)\}

我们称g(n)g(n)f(n)f(n)的一个渐进上界

Ω\Omega记号

对于一个给定的函数g(n)g(n),用Ω(g(n))\Omega(g(n))来表示以下函数的集合:
Ω(g(n))={f(n):存在正常量cn0,使得对所有nn0,有0cg(n)f(n)}\Omega(g(n))=\{f(n):\text{存在正常量}c\text{和}n_0\text{,使得对所有}n \geq n_0\text{,有}0\leq cg(n) \leq f(n)\}

我们称g(n)g(n)f(n)f(n)的一个渐进下界

oo记号

对于一个给定的函数g(n)g(n),用o(g(n))o(g(n))来表示以下函数的集合:
o(g(n))={f(n):存在正常量cn0,使得对所有nn0,有0f(n)<cg(n)}o(g(n))=\{f(n):\text{存在正常量}c\text{和}n_0\text{,使得对所有}n \geq n_0\text{,有}0\leq f(n) < cg(n)\}
我们称g(n)g(n)f(n)f(n)的一个非渐进紧确上界

ω\omega记号

对于一个给定的函数g(n)g(n),用ω(g(n))\omega(g(n))来表示以下函数的集合:
ω(g(n))={f(n):存在正常量cn0,使得对所有nn0,有0cg(n)<f(n)}\omega(g(n))=\{f(n):\text{存在正常量}c\text{和}n_0\text{,使得对所有}n \geq n_0\text{,有}0\leq cg(n) < f(n)\}
我们称g(n)g(n)f(n)f(n)的一个非渐进紧确下界

比较各种函数
  • 传递性
    f(n)=Θ(g(n))g(n)=Θ(h(n))f(n)=Θ(h(n))f(n) = \Theta(g(n)) \land g(n) = \Theta(h(n)) \rightarrow f(n) = \Theta(h(n))
    f(n)=O(g(n))g(n)=O(h(n))f(n)=O(h(n))f(n) = O(g(n)) \land g(n) = O(h(n)) \rightarrow f(n) = O(h(n))
    f(n)=Ω(g(n))g(n)=Ω(h(n))f(n)=Ω(h(n))f(n) = \Omega(g(n)) \land g(n) = \Omega(h(n)) \rightarrow f(n) = \Omega(h(n))
    f(n)=o(g(n))g(n)=o(h(n))f(n)=o(h(n))f(n) = o(g(n)) \land g(n) = o(h(n)) \rightarrow f(n) = o(h(n))
    f(n)=ω(g(n))g(n)=ω(h(n))f(n)=ω(h(n))f(n) = \omega(g(n)) \land g(n) = \omega(h(n)) \rightarrow f(n) = \omega(h(n))
  • 自反性
    f(n)=Θ(f(n))f(n) = \Theta(f(n))
    f(n)=O(f(n))f(n) = O(f(n))
    f(n)=Ω(f(n))f(n) = \Omega(f(n))
  • 对称性
    f(n)=Θ(g(n))g(n)=Θ(f(n))f(n) = \Theta(g(n)) \leftrightarrow g(n) = \Theta(f(n))
  • 转置对称性
    f(n)=O(g(n))g(n)=Ω(f(n))f(n) = O(g(n)) \leftrightarrow g(n) = \Omega(f(n))
    f(n)=o(g(n))g(n)=ω(f(n))f(n) = o(g(n)) \leftrightarrow g(n) = \omega(f(n))
  • 类比
    f(n)=O(g(n))abf(n) = O(g(n)) \rightarrow a \leq b
    f(n)=Ω(g(n))abf(n) = \Omega(g(n)) \rightarrow a \geq b
    f(n)=Θ(g(n))a=bf(n) = \Theta(g(n)) \rightarrow a = b
    f(n)=o(g(n))a<bf(n) = o(g(n)) \rightarrow a < b
    f(n)=ω(g(n))a>bf(n) = \omega(g(n)) \rightarrow a > b

标准记号与常用函数

多项式

对于一个dd次渐进正的多项式p(n)p(n),有:

p(n)=Θ(nd)p(n)=\Theta(n^d)

指数

任意底大于1的指数函数比任意多项式函数增长得快:

nb=o(an),a>1n^b = o(a^n),a>1

对数

对所有实数a>0a>0b>0b>0c>0c>0nn,有:

a=blogbaa = b^{log_b a}

logc(ab)=logca+logcblog_c(ab) = log_c a + log_c b

logban=nlogbalog_b a^n = n log_b a

logba=logcalogcblog_b a = \frac{log_c a}{log_c b}

logb(1/a)=logbalog_b(1/a) = -log_b a

logba=1logablog_b a = \frac{1}{log_a b}

alogbc=clogbaa^{log_b c} = c^{log_b a}

任意正的多项式函数都比任意多底数函数增长得快:

lgbn=o(na)lg^b n = o(n^a)

阶乘

斯特林近似公式

n!=2πn(ne)n(1+Θ(1n))n!=\sqrt{2\pi n}(\frac{n}{e})^n(1+\Theta(\frac{1}{n}))

n!=2πn(ne)nean,112n+1<an<112nn!=\sqrt{2\pi n}(\frac{n}{e})^n e^{a_n}, \frac{1}{12n+1}<a_n<\frac{1}{12n}

给出了一个更紧确的上界和下界:

n!=o(nn)n!=o(n^n)

n!=ω(2n)n!=\omega(2^n)

lg(n!)=Θ(nlgn)lg(n!)=\Theta(nlgn)

多重函数

假设f(n)f(n)为实数集上的一个函数,对非负整数ii,我们递归地定义:

f(i)(n)={ni=1f(f(i1)(n))i>1f^{(i)}(n) = \begin{cases} n \qquad & i = 1 \\ f(f^{(i-1)}(n)) \qquad & i>1 \end{cases}

多重对数函数

定义多重对数函数为:

lgn=min{i0:lg(i)n1}lg*n=min\{i \geq 0:lg^{(i)}n \leq 1\}

多重对数是一个增长非常慢的函数:

  • lg2=1lg*2=1
  • lg4=2lg*4=2
  • lg16=3lg*16=3
  • lg65536=4lg*65536=4
  • lg(265536)=5lg*(2^{65536})=5

分治策略

  • 代入法:我们猜测一个界,然后用数学归纳法证明这个界是正确的
  • 递归树法:将递归式转换为一棵树,其结点表示不同层次的递归调用产生的代价。然后采用边界和技术来求解递归式
  • 主方法:可求解形如下面公式的递归式的界:

T(n)=aT(n/b)+f(n),a1,b>1T(n)=aT(n/b)+f(n),a\geq 1,b>1

最大子数组问题

寻找A的和最大的非空连续子数组,这样的连续子数组为最大子数组

使用分治策略的求解方法

A[low..high]A[low..high]的任何连续子数组A[i..j]A[i..j]所处的位置必然是一下三种情况之一:

  • 完全位于子数组A[low..mid]A[low..mid]中,因此lowijmidlow \leq i \leq j \leq mid
  • 完全位于子数组A[mid+1..high]A[mid+1..high]中,因此mid<ijhighmid < i \leq j \leq high
  • 跨越了中点,因此lowimid<jhighlow \leq i \leq mid < j \leq high

过程 FIND-MAX-CROSSING-SUBARRAY(A, low, mid, high) 接收数组AA和下标lowlowmidmidhighhigh为输入,返回一个下标元组划定跨越中点的最大子数组的边界,并返回最大子数组中值的和:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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-right, left-sum + right-sum)
1
2
3
4
5
6
7
8
9
10
11
12
FIND-MAXINUM-SUBARRAY(A, low, high)
if high == low
return (low, high, A[low])
else mid = ⌊(low + high)/2⌋
(left-low, right-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-zum) = 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 运行时间T(n)T(n)的递归式:

T(n)={Θ(1)n=12T(n/2)+Θ(n)n>1=Θ(nlgn)T(n) = \begin{cases} \Theta(1) \qquad & n = 1 \\ 2T(n/2) + \Theta(n) \qquad & n>1 \end{cases} = \Theta(nlgn)

矩阵乘法的Strassen算法

基础的矩阵乘法

复杂度 Θ(n3)\Theta (n^3)

1
2
3
4
5
6
7
8
9
SQUARE-MATRIX-MULTIPLY(A, B)
n = A.rows
let C be a new n * n matrix
for i = 1 to n
for j = 1 to n
c_ij = 0
for k = 1 to n
c_ij = c_ij + a_ik * b_kj
return C
简单的分治算法

假定将AABBCC均分解为4个n/2×n/2n/2 \times n/2的子矩阵:

A=[A11A12A21A22],B=[B11B12B21B22],C=[C11C12C21C22]A = \begin{bmatrix} A_{11} & A_{12} \\ A_{21} & A_{22} \end{bmatrix} , B = \begin{bmatrix} B_{11} & B_{12} \\ B_{21} & B_{22} \end{bmatrix} , C = \begin{bmatrix} C_{11} & C_{12} \\ C_{21} & C_{22} \end{bmatrix}

可以将公式C=ABC = A \cdot B改写为:

[C11C12C21C22]=[A11A12A21A22][B11B12B21B22]\begin{bmatrix} C_{11} & C_{12} \\ C_{21} & C_{22} \end{bmatrix} = \begin{bmatrix} A_{11} & A_{12} \\ A_{21} & A_{22} \end{bmatrix} \cdot \begin{bmatrix} B_{11} & B_{12} \\ B_{21} & B_{22} \end{bmatrix}

1
2
3
4
5
6
7
8
9
10
SQUARE-MATRIX-MULTIPLY-RECURSIVE(A, B)
n = A.rows
let C be a new n * n matrix
if n == 1
c_11 = a_11 * b_11
else partition A, B and C as in equations
C_11 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_11, B_11) + SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_12, B_21)
C_12 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_11, B_12) + SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_12, B_22)
C_21 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_21, B_11) + SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_22, B_21)
C_22 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_21, B_12) + SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_22, B_22)

SQUARE-MATRIX-MULTIPLY-RECURSIVE 运行时间T(n)T(n)的递归式:

T(n)={Θ(1)n=18T(n/2)+Θ(n2)n>1=Θ(n3)T(n) = \begin{cases} \Theta(1) \qquad & n = 1 \\ 8T(n/2) + \Theta(n^2) \qquad & n>1 \end{cases} = \Theta(n^3)

Strassen方法

Strassen运行时间T(n)T(n)的递归式:

T(n)={Θ(1)n=17T(n/2)+Θ(n2)n>1=Θ(nlg7)T(n) = \begin{cases} \Theta(1) \qquad & n = 1 \\ 7T(n/2) + \Theta(n^2) \qquad & n>1 \end{cases} = \Theta(n^{lg_7})

代入法求解递归式

代入法求解递归式分为两步:

  • 猜测解的形式
  • 用数学归纳法求出解的常数,并证明解是正确的

例如,确定下列递归式的上界:

T(n)=2T(n/2)+nT(n)=2T(\lfloor n/2 \rfloor)+n

猜测其解为T(n)=O(nlgn)T(n)=O(nlgn)
首先假定次上界对所有正数m<nm<n都成立,特别对于m=n/2m=\lfloor n/2 \rfloor,有T(n/2)cn/2lg(n/2)T(\lfloor n/2 \rfloor) \leq c\lfloor n/2 \rfloor lg(\lfloor n/2 \rfloor)。将其带入递归式,有:

T(n)2(cn/2lg(n/2))+ncnlg(n/2)+n=cnlgncnlg2+n=cnlgncn+ncnlgnT(n) \leq 2(c\lfloor n/2 \rfloor lg(\lfloor n/2 \rfloor)) + n \leq cn lg(n/2) + n \\ = cnlgn-cnlg2+n\\ =cnlgn-cn+n\\ \leq cnlgn
其中,只要c1c \leq 1,最后一步都会成立

递归树求解递归式

递归树中,每个结点表示一个单一子问题的代价,子问题对应某次递归函数调用
我们将树中的每层中的代价求和,得到每层代价,然后将所有层的代价求和,得到所有层次的递归调用的总代价

主方法求解递归式

a1a\leq 1b>1b>1是常数,f(n)f(n)是一个函数,T(n)T(n)是定义在非负整数上的递归式:

T(n)=aT(n/b)+f(n)T(n)=aT(n/b)+f(n)

  • 若对某个常数ϵ>0\epsilon >0f(n)=O(nlogbaϵ)f(n)=O(n^{log_ba-\epsilon}),则T(n)=Θ(nlogba)T(n)=\Theta(n^{log_ba})
  • 若对于某个常数k0k\geq 0f(n)=Θ(nlogbalgkn)f(n)=\Theta(n^{log_ba}lg^kn),则T(n)=Θ(nlogbalgk+1n)T(n)=\Theta(n^{log_ba}lg^{k+1}n)
  • 若对某个常数ϵ>0\epsilon >0f(n)=Ω(nlogba+ϵ)f(n)=\Omega(n^{log_ba+\epsilon}),且对某个常数c<1c<1和所有足够大的nnaf(n/b)cf(n)af(n/b) \leq cf(n),则T(n)=Θ(f(n))T(n)=\Theta(f(n))

随机算法

雇佣问题

1
2
3
4
5
6
7
HIRE-ASSISTANT(n)
best = 0
for i = 1 to n
interview candidate i
if candidate i is better than candidate best
best = i
hire candidate i
最坏情况分析

在最坏情况下,当应聘者质量按出现的次序严格递增时,总费用是O(chn)O(c_hn)

随机算法

如果一个算法的行为不仅由输入决定,而且也由随机数生成器产生的数值决定,则称这个算法是随机的

指示器随机变量

给定一个样本空间SS和一个事件AA,那么事件AA对应的指示器随机变量I{A}I\{A\}定义为:

I{A}={1if A occurs0if A does not occurI\{A\} = \begin{cases} 1 \qquad & if\ A \ occurs \\ 0 \qquad & if \ A \ does\ not\ occur \end{cases}

给定一个样本空间SSSS中的一个事件AA,设XA=I{A}X_A=I\{A\},那么E[XA]=Pr{A}E[X_A]=Pr\{A\}

用指示器随机变量分析雇佣问题

应聘者ii比应聘者1到i1i-1更有资格的概率为1/i1/i
E[Xi]=1/iE[X_i]=1/i
所以有:
E[X]=E[i=inXi]=i=1nE[Xi]=i=1n1/i=lnn+O(1)E[X]\\ =E[\sum_{i=i}^n X_i]\\ = \sum_{i=1}^nE[X_i]\\ =\sum_{i=1}^n1/i\\ =lnn+O(1)
算法 HIRE-ASSISTANT 总的雇佣费用平均情形下为O(chlnn)O(c_hlnn)

随机算法

过程 RANDOMIZED-HIRE-ASSISTANT 的雇佣费用期望是O(chlnn)O(c_hlnn)

1
2
3
4
5
6
7
8
RANDOMIZED-HIRE-ASSISTANT(n)
randomly permute the list of candidates
best = 0
for i = 1 to n
interview candidate i
if candidate i is better than candidate best
best = i
hire candidate i
随机排列数组

假设所有优先级都不同,则过程 PERMUTE-BY-SORTING 产生输入的均匀随机排列:

1
2
3
4
5
6
PERMUTE-BY-SORTING(A)
n = A.length
let P[1..n] be a new array
for i = 1 to n
P[i] = RANDOM(1, n^3)
sort A, using P as sort keys

过程 RANDOMIZE-IN-PLACE 可计算出一个均匀随机排列:

1
2
3
4
RANDOMIZE-IN-PLACE(A)
n = A.length
for i = 1 to n
swap A[i] with A[RANDOM(i, n)]

排序和顺序统计量

堆排序

是一个数组,它可以被看成一个近似的完全二叉树

  • 树上的每一个结点对应数组中的一个元素
  • 除了最底层外,该树是完全充满的
  • A.lengthA.length表示数组元素的个数
  • A.heapsizeA.heap-size表示有多少个堆元素存储在该数组中
  • 0A.heapsizeA.length0 \leq A.heap-size \leq A.length
  • 一个堆中的结点的高度就为该结点到叶结点最长简单路径上边的数目

heap

计算父结点、左孩子和右孩子的下标:

1
2
3
4
5
6
7
8
PARENT(i)
return ⌊i/2⌋

LEFT(i)
return 2i

RIGHT(i)
return 2i + 1

最大堆中,最大堆性质指除了根以外的所有结点ii都要满足:

A[PARENT(i)]A[i]A[PARENT(i)] \geq A[i]

最小堆中,最小堆性质指除了根以外的所有结点ii都要满足:

A[PARENT(i)]A[i]A[PARENT(i)] \leq A[i]

维护堆的性质

假定根结点为 LEFT(i)RIGHT(i) 的二叉树都是最大堆,MAX-HEAPIFY 通过让A[i]A[i]的值在最大堆中“逐级下降”,从而使得以下标ii为根结点的子树重新遵循最大堆的性质:

1
2
3
4
5
6
7
8
9
10
11
12
MAX-HEAPIFY(A, i)
l = LEFT(i)
r = RIGHT(i)
if l <= A.heap-size and A[l] > A[i]
largest = l
else
largest = i
if r <= A.heap-size and A[r] > A[largest]
largest = r
if largest != i
exchange A[i] with A[largest]
MAX-HEAPIFY(A, largest)

因为每个孩子的子树的大小至多为2n/32n/3(最坏情况发生在树的最底层恰好半满的时候),我们可以用下面递归式刻画 MAX-HEAPIFY 的运行时间:

T(n)T(2n/3)+Θ(1)=O(lgn)=O(h)T(n) \leq T(2n/3)+\Theta(1) = O(lgn) = O(h)

建堆

可以用自底向上的方法利用过程 MAX-HEAPIFY 把一个大小为n=A.lengthn=A.length的数组A[1..n]A[1..n]转换为最大堆:

1
2
3
4
BUILD-MAX-HEAP(A)
A.heap-size = A.length
for i = ⌊A.length/2⌋ downto 1
MAX-HEAPIFY(A, i)

在一个高度为hh的结点上运行 MAX-HEAPIFY 的代价是O(h)O(h),我们可以将 BUILD-MAX-HEAP 的总代价表示为:

h=0lgnn2h+1O(h)=O(nh=0lgnh2h)=O(nh=0h2h)=O(n)\sum^{\lfloor lgn \rfloor}_{h=0} \lceil \frac{n}{2^{h+1}} \rceil O(h) = O(n \sum^{\lfloor lgn \rfloor}_{h=0} \frac{h}{2^h}) = O(n \sum^{\infty}_{h=0} \frac{h}{2^h}) =O(n)

因此,我们可以在线性时间内,把一个无序数组构造成一个最大堆

堆排序算法

HEAPSORT 过程的时间复杂度是O(nlgn)O(nlgn)

1
2
3
4
5
6
HEAPSORT(A)
BUILD-MAX-HEAP(A)
for i = A.length downto 2
exchange A[1] with A[i]
A.heap-size = A.heap-size - 1
MAX-HEAPIFY(A, 1)

优先队列

优先队列是一种用来维护由一组元素构成的集合SS的数据结构,其中的每一个元素都有一个相关的值,称为关键字。一个最大优先队列支持以下操作:

  • INSERT(S, x):把元素xx插入集合SS
  • MAXIMUM(S):返回SS中具有最大键字的元素
  • EXTRACT-MAX(S):去掉并返回SS中的具有最大键字的元素
  • INCREASE-KEY(S, x, k):将元素xx的关键字增加到kk,这里假设kk的值不小于xx的原关键字值

优先队列可以用堆来实现:
HEAP-MAXMIUM 时间复杂度Θ(1)\Theta(1)

1
2
HEAP-MAXMIUM(A)
return A[1]

HEAP-EXTRACT-MAX 时间复杂度O(lgn)O(lgn)

1
2
3
4
5
6
7
8
HEAP-EXTRACT-MAX(A)
if A.heap-size < 1
error "heap underflow"
max = A[1]
A[1] = A[A.heap-size]
A.heap-size = A.heap-size - 1
MAX-HEAPIFY(A, 1)
return max

HEAP-INCREASE-KEY 时间复杂度O(lgn)O(lgn)

1
2
3
4
5
6
7
HEAP-INCREASE-KEY(A, i, key)
if key < A[i]
error "new key is smaller than current key"
A[i] = key
while i > 1 and A[PARENT(i)] < A[i]
exchange A[i] with A[PARENT(i)]
i = PARENT(i)

MAX-HEAP-INSERT 时间复杂度O(lgn)O(lgn)

1
2
3
4
MAX-HEAP-INSERT(A, key)
A.heap-size = A.heap-size + 1
A[A.heap-size] = -∞
HEAP-INCRERASE-KEY(A, A.heap-size, key)

快速排序

快速排序描述

对一个子数组A[p..r]A[p..r]进行快速排序的三步分治过程:

  • 分解:数组A[p..r]A[p..r]被划分为两个(可能为空)子数组A[p..q1]A[p..q-1]A[q+1..r]A[q+1..r],使得A[p..q1]A[p..q-1]中的每一个元素都小于等于A[q]A[q],而A[q]A[q]也小于等于A[q+1..r]A[q+1..r]中的每个元素。其中,计算下标qq也是划分过程的一部分
  • 解决:通过递归调用快速排序,对子数组A[p..q1]A[p..q-1]A[q+1..r]A[q+1..r]进行排序
  • 合并:因为子数组都是原址排序的,所以不需要合并操作:数组A[p..r]A[p..r]已经有序
1
2
3
4
5
QUICKSORT(A, p, r)
if p < r
q = PARTITION(A, p, r)
QUICKSORT(A, p, q-1)
QUICKSORT(A, q+1, r)

PARTITION 过程实现了对子数组A[p..r]A[p..r]的原址重排:

1
2
3
4
5
6
7
8
9
PARTITION(A, p, r)
x = A[r]
i = p - 1
for j = p to r-1
if A[j] <= x
i = i + 1
exchange A[i] with A[j]
exchange A[i+1] with A[r]
return i+1

partition

快速排序性能

最坏情况划分

当划分产生的两个子问题分别包含了n1n-1个元素和0个元素时,为最坏情况
此时算法递归式可以表示为:

T(n)=T(n1)+T(0)+Θ(n)=T(n1)+Θ(n)=Θ(n2)T(n)=T(n-1)+T(0)+\Theta(n) =T(n-1) + \Theta(n) = \Theta(n^2)

最好情况划分

在可能的最平衡的划分中,PARTITION 得到的两个子问题的规模都不大于n/2n/2
此时算法递归式可以表示为:

T(n)=2T(n/2)+Θ(n)=Θ(nlgn)T(n)=2T(n/2)+\Theta(n)=\Theta(nlgn)

平衡的划分

任何一种常数比例的划分都会产生深度为Θ(lgn)\Theta(lgn)的递归树,其中每一层的时间代价都是O(n)O(n)
因此,只要划分是常数比例的,算法的运行时间总是O(nlgn)O(nlgn)

对于平均情况的直观观察

当好和差的划分交替出现时,快速排序的时间复杂度与全是好的划分时一样,仍然是O(nlgn)O(nlgn)。区别只是OO符号中隐含的常数因子要略大一些

快速排序随机化版本

通过对序列p,..,rp,..,r的随机抽样,我们期望在平均情况下,对输入数组的划分是比较均衡的:

1
2
3
4
RANDOMIZED-PARTITION(A, p, r)
i = RANDOM(p, r)
exchange A[r] with A[i]
return PARTITION(A, p, r)
1
2
3
4
5
RANDOMIZED-QUICKSORT(A, p, r)
if p < r
q = RANDOMIZED-PARTITION(A, p, r)
RANDOMIZED-QUICKSORT(A, p, q-1)
RANDOMIZED-QUICKSORT(A, q+1, r)

快速排序分析

最坏情况分析

快速排序的最坏情况运行时间是Θ(n2)\Theta(n^2)

期望运行时间

快速排序的期望运行时间是O(nlgn)O(nlgn)

线性时间排序

排序算法的下界

决策树模型

比较排序可以被抽象为一棵决策树
决策树是一棵完全二叉树,它可以表示在给定输入规模情况下,某一给定排序算法对所有元素的比较操作
在决策树中,每个内部结点都以i:ji:j标记,其中iijj满足1i,jn1 \leq i,j \leq nnn是输入序列中的元素个数
每一个内部结点表示一次比较aiaja_i \leq a_j

  • 左子树表示一旦我们确定aiaja_i \leq a_j之后的后续比较
  • 右子树表示一旦我们确定ai>aja_i > a_j之后的后续比较

对于一个正确的比较排序算法来说,nn个元素的n!n!种可能的排列都应该出现在决策树的叶结点上。而且,每一个叶结点都必须是可以从根结点经由某条路径到达的

decisionTree

最坏情况的下界

在最坏情况下,任何比较排序算法都需要做Ω(nlgn)\Omega(nlgn)次比较:
考虑一棵高度为hh,具有ll个可达叶结点的决策树,它对应一个对nn个元素所做的比较排序。因为输入数据的n!n!种可能的排列都是叶结点,所以有n!ln! \leq l。由于在一个高度为hh的二叉树中,叶结点的数目 不多于2h2^h,所以有:

n!l2hn! \leq l \leq 2^h

对该式两边取对数,有hlg(n!)=Ω(nlgn)h \geq lg(n!) = \Omega(nlgn)

计数排序

计数排序假设nn个输入元素中的每一个都是在0到kk区间内的一个整数,其中kk为某个整数。当k=O(n)k=O(n)时,排序的运行时间为Θ(n)\Theta(n)

在计数排序的代码中,假设输入是一个数组A[1..n]A[1..n]A.length=nA.length = nB[1..n]B[1..n]存放排序的输出,C[0..k]C[0..k]提供临时存储空间:

1
2
3
4
5
6
7
8
9
10
11
COUNTING-SORT(A, B, k)
let C[0..k] be a new array
for i = 0 to k
C[i] = 0
for j = 1 to A.length
C[A[j]] = C[A[j]] + 1
for i = 1 to k
C[i] = C[i-1] + C[i]
for j = A.length downto 1
B[C[A[j]]] = A[j]
C[A[j]] = C[A[j]] - 1

计数排序时间复杂度Θ(k+n)\Theta(k+n),当k=O(n)k=O(n)时,时间复杂度Θ(n)\Theta(n)

基数排序

假设nndd位的元素存放在数组AA中,其中第1位是最低位,第dd位是最高位:

1
2
3
RADIX-SORT(A, d)
for i = 1 to d
use a stable sort to sort array A on digit i

给定一个bb位数和任何正整数rbr\leq b,如果 RADIX-SORT 使用的稳定排序算法对数据取值区间是0到kk的输入进行排序耗时Θ(n+k)\Theta(n+k),那么它就可以在Θ((b/r)(n+2r))\Theta((b/r)(n+2^r))时间内将这些数据排好序

桶排序

桶排序假设输入数据服从均匀分布,平均情况下它的时间代价为O(n)O(n)

假设输入是一个包含nn个元素的数组AA,且每个元素A[i]A[i]满足0A[i]<10 \leq A[i] < 1。算法还需要一个临时数组B[0..n1]B[0..n-1]来存放链表(即桶),并假设存在一种用于维护这些链表的机制:

1
2
3
4
5
6
7
8
9
10
BUCKET-SORT(A)
n = A.length
let B[0..n-1] be a new array
for i = 0 to n-1
make B[i] an empty list
for i = 1 to n
insert A[i] into list B[⌊nA[i]⌋]
for i = 0 to n-1
sort list B[i] with insertion sort
concatenate the lists B[0]..B[n-1] together in order

桶排序的期望运行时间为:

Θ(n)+nO(21/n)=Θ(n)\Theta(n)+n \cdot O(2-1/n) = \Theta(n)

中位数和顺序统计量

最小值和最大值

假设该集合元素存放在数组AA中,且A.length=nA.length = n

1
2
3
4
5
6
MINIMUM(A)
min = A[1]
for i = 2 to A.length
if min > A[i]
min = A[i]
return min

为了确定最小值,必须要做n1n-1次比较

同时找到最小值和最大值

最多3n/23\lfloor n/2 \rfloor次比较就可以同时找到最小值和最大值:
首先,我们将一对输入元素相互进行比较,然后把较小的与当前最小值比较,把较大的与当前最大值进行比较。这样,对每两个元素共需3次比较:

  • 如果nn奇数,我们就将最小值和最大值的初值设为第一个元素的值,然后成对地处理余下的元素
  • 如果nn偶数,就对前两个元素做一次比较,以决定最小值和最大值的初值,然后成对处理余下的元素

期望为线性时间的选择算法

RANDOMIZED-SELECT 返回数组A[p..r]A[p..r]中第ii小的元素:

1
2
3
4
5
6
7
8
9
10
11
RANDOMIZED-SELECT(A, p, r, i)
if p == r
return A[p]
q = RANDOMIZE-PARTITOOPN(A, p, r)
k = q - p + 1
if i == k
return A[q]
else if i < k
return RANDOMIZED-SELECT(A, p, q-1, r)
else
return RANDOMIZED-SELECT(A, q+1, r, i-k)

RANDOMIZED-SELECT 的最坏情况运行时间为Θ(n2)\Theta(n^2),期望运行时间为Θ(n)\Theta(n)

最坏情况为线性时间的选择算法

通过执行下列步骤,算法 SELECT 可以确定一个有n>1n>1个不同元素的输入数组中的第ii小的元素

  • 将输入数组的nn个元素划分为n/5\lfloor n/5 \rfloor组,每组5个元素,且至多只有一组由剩下的n mod 5n \ mod \ 5个元素组成
  • 寻找这n/5\lceil n/5 \rceil组中每一组的中位数:首先对每组元素进行插入排序,然后确定每组有序元素的中位数
  • 对第2步中找出的n/5\lceil n/5 \rceil个中位数,递归调用 SELECT 以找出其中位数xx(如果有偶数个中位数,为了方便,约定xx是较小的中位数)
  • 利用修改过的 PARTITION 版本,按中位数的中位数xx对输入数组进行划分。让kk比划分的低区中的元素数目多1,因此xx是第kk小的元素,并且有nkn-k个元素在划分的高区
  • 如果i=ki=k,则返回xx。如果i<ki<k,则在低区递归调用 SELECT 来找出第ii小的元素。如果i>ki>k,则在高区递归查找第iki-k小的元素

select

在第2步找出的中位数中,至少有一半大于或等于中位数的中位数xx。因此,在这n/5\lceil n/5 \rceil个组中,除了当nn不能被5整除时产生的所含元素少于5的那个组和包含xx的那个组之外,至少有一半的组中有3个元素大于xx。不算这两个组,大于xx的元素个数至少为:

3(12n52)3n1063(\lceil \frac{1}{2} \lceil \frac{n}{5} \rceil\rceil - 2) \geq \frac{3n}{10}-6

类似地,至少有3n/1063n/10-6个元素小于xx。因此,在最坏情况下,在第5步中,SELECT 的递归调用最多作用于7n/10+67n/10+6个元素。
由此可以得到如下递归式:

T(n){Θ(1)n<140T(n/5)+T(7n/10+6)+O(n)n140=O(n)T(n) \leq \begin{cases} \Theta(1) \qquad & n < 140 \\ T(\lceil n/5 \rceil) + T(7n/10+6) + O(n) \qquad & n\geq 140 \end{cases} = O(n)

高级设计和分析技术

动态规划

我们通常按如下4个步骤来设计一个动态规划算法:

  • 刻画一个最优解的结构特征
  • 递归地定义最优解的值
  • 计算最优解的值,通常采用自底向上的方法
  • 利用计算出的信息构造一个最优解

钢条切割

给定一段长度为nn英寸的钢条和一个价格表pi(i=1,2,...,n)p_i(i=1,2,...,n),求切割钢条方案,使得销售收益rnr_n最大

自顶向下 CUT-ROD过程,加入了备忘机制,时间复杂度Θ(n2)\Theta(n^2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
MEMORIZED-CUT-ROD(p, n)
let r[0..n] be a new array
for i = 0 to n
r[i] = -∞
return MEMORIZED-CUT-ROD-AUX(p, n, r)

MEMORIZED-CUT-ROD-AUX(p, n, r)
if r[n] >=0
return r[n]
if n == 0
q = 0
else
q = -∞
for i = 1 to n
q = max(q, p[i] + MEMORIZED-CUT-ROD-AUX(p, n-i, r))
r[n] = q
return q

自底向上版本,时间复杂度Θ(n2)\Theta(n^2)

1
2
3
4
5
6
7
8
9
BOTTOM-UP-CUT-ROD(p, n)
let r[0..n] be a new array
r[0] = 0
for j = 1 to n
q = -∞
for i = 1 to j
q = max(q, p[i] + r[j-i])
r[j] = q
return r[n]
重构解

BOTTOM-UP-CUT-ROD 的扩展版本,它对长度为jj的钢条不仅计算最大收益值rjr_j,还保存最优解对应的第一段钢条的切割长度sjs_j

1
2
3
4
5
6
7
8
9
10
11
EXTENDED-BOTTOM-UP-CUT-ROD(p, n)
let r[0..n] and s[0..n] be new arrays
r[0] = 0
for j = 1 to n
q = -∞
for i = 1 to j
if q < p[i] + r[j-i]
q = p[i] + r[j-i]
s[j] = i
r[j] = q
return r and s

最后输出长度为nn的钢条的完整的最优切割方案:

1
2
3
4
5
PRINT-CUT-ROD-SOLUTION(p, n)
(r, s) = EXTENDED-BOTTOM-UP-CUT-ROD(p, n)
while n > 0
print s[n]
n = n - s[n]

矩阵链乘法

给定一个nn个矩阵的序列(矩阵链)A1,A2,...,An\langle A_1,A_2,...,A_n\rangle,我们希望计算它们的乘积:

A1A2...AnA_1A_2...A_n

由于矩阵乘法满足结合律,因此任何加括号的方法都会得到相同的计算结果

我们称有如下性质的矩阵乘积链为完全括号化的:它是单一矩阵,或者是两个完全括号化的矩阵乘积链的积
给定nn个矩阵的链A1,A2,...,An\langle A_1,A_2,...,A_n\rangle,矩阵AiA_i的规模为pi1pi(1in)p_{i-1} * p_i(1 \leq i \leq n),求完全括号化方案,使得计算乘积A1A2...AnA_1A_2...A_n所需标量乘法次数最少

m[i,j]m[i,j]表示计算矩阵Ai..jA_{i..j}所需标量乘法次数的最小值,则AiAi+1...AjA_iA_{i+1}...A_j最小代价括号化方案的递归求解公式为:

m[i,j]{0i=jminik<j{m[i,k]+m[k+1,j]+pi1pkpj}i<jm[i,j] \leq \begin{cases} 0 \qquad & i = j \\ min_{i \leq k < j}\{m[i,k]+m[k+1,j]+p_{i-1}p_kp_j\} \qquad & i <j \end{cases}

MATRIX-CHAIN-ORDER 时间复杂度O(n3)O(n^3),另外还需要Θ(n2)\Theta(n^2)的内存空间来保存表mmss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
MATRIX-CHAIN-ORDER(p)
n = p.length - 1
let m[1..n,1..n] and s[1..n-1,2..n] be new tables
for i = 1 to n
m[i,i] = 0
for l = 2 to n //l is the chain length
for i = 1 to n-l+1
j = i + l - 1
m[i,j] = ∞
for k = i to j-1
q = m[i, k] + m[k+1, j] + p_i-1p_kp_j
if q < m[i,j]
m[i,j] = q
s[i,j] = k
return m and s

调用 PRINT-OPTIMAL-PARENS 可输出A1,A2,...,An\langle A_1,A_2,...,A_n\rangle的最优括号化方案:

1
2
3
4
5
6
7
8
PRINT-OPTIMAL-PARENS(s, i, j)
if i == j
print "A"
else
print "("
PRINT-OPTIMAL-PARENS(s, i, s[i,j])
PRINT-OPTIMAL-PARENS(s, s[i,j]+1, j)
print ")"

最长公共子序列(LCS)

给定一个序列X=x1,x2...xmX = \langle x_1,x_2...x_m\rangle,令一个序列Z=z1,z2,...,zkZ=\langle z_1,z_2,...,z_k\rangle满足如下条件时称为XX子序列:存在一个严格递增的XX的下标序列i1,i2,...,ik\langle i_1,i_2,...,i_k\rangle,对所有j=1,2,...,kj=1,2,...,k,满足xi=zjx_i=z_j
给定两个序列X=x1,x2...xmX = \langle x_1,x_2...x_m\rangleY=y1,y2,...,ynY =\langle y_1,y_2,...,y_n\rangle,求XXYY长度最长的公共子序列

我们定义c[i,j]c[i,j]表示XiX_iYjY_jLCS 的长度,可得如下公式:

c[i,j]{0i=0j=0c[i1,j1]+1i,j>0xi=yimax(c[i,j1],c[i1,j])i,j>0xiyic[i,j] \leq \begin{cases} 0 \qquad & i = 0 \lor j = 0 \\ c[i-1,j-1]+1 \qquad & i,j >0 \land x_i = y_i \\ max(c[i,j-1],c[i-1,j]) & i,j >0 \land x_i \neq y_i \end{cases}

LCS-LENGTH 时间复杂度Θ(mn)\Theta(mn)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
LCS-LENGTH(X, Y)
m = X.length
n = Y.length
let b[1..m,1..n] and c[0..m,0..n] be new tables
for i = 1 to m
c[i,0] = 0
for j = 0 to n
c[0,j] = 0
for i = 1 to m
for j = 1 to n
if xi == yj
c[i,j] = c[i-1,j-1] + 1
b[i,j] = "↖"
elseif c[i-1,j] >= c[i,j-1]
c[i,j] = c[i-1,j]
b[i,j] = "↑"
else
c[i,j] = c[i,j-1]
b[i,j] = "←"
return c and b

调用 PRINT-LCS 可打印出XXYY的一个 LCS

1
2
3
4
5
6
7
8
9
10
PRINT-LCS(b, X, i, j)
if x == 0 or j == 0
return
if b[i,j] == "↖"
PRINT-LCS(b, X, i-1, j-1)
print xi
elseif b[i,j] == "↑"
PRINT-LCS(b, X, i-1, j)
else
PRINT-LCS(b, X, i, j-1)

最优二叉搜索树

给定一个nn个不同关键字的已排序的序列K=k1,k2,...,kn,k1<k2<...<knK=\langle k_1,k_2,...,k_n \rangle ,k_1<k_2<...<k_n,我们希望用这些关键字构造一棵二叉搜索树,对每个关键字kik_i,都有一个概率pip_i表示其搜索频率。有些要搜索的值可能不在KK中,因此我们还有n+1n+1个“伪关键字”d0,d1,...,dnd_0,d_1,...,d_n表示不在KK中的值。d0d_0表示所有小于k1k_1的值,dnd_n表示所有大于knk_n的值,对i=1,2,...,n1i=1,2,...,n-1,伪关键字did_i表示所有在kik_iki+1k_{i+1}之间的值。对每个伪关键字did_i,也都有一个概率qiq_i表示对应的搜索频率。每个关键字kik_i是一个内部结点,而每个伪关键字did_i是一个叶结点:

binaryTree

有如下公式:

i=1npi+i=0nqi=1\sum^{n}_{i=1}p_i+\sum^n_{i=0}q_i=1

TT中进行一次搜索的期望代价为:

E[cost]=1+i=1ndepthT(ki)pi+i=0ndepthT(di)qiE[cost]=1+\sum^{n}_{i=1}depth_T(k_i) \cdot p_i + \sum^n_{i=0}depth_T(d_i) \cdot q_i

对于一个给定的概率集合。我们希望构造一棵期望搜索代价最小的二叉搜索树,我们称为最优二叉搜索树

定义e[i,j]e[i,j]为在包含关键字ki,...,kjk_i,...,k_j的最优二叉搜素树中进行一次有所的期望代价,其中iii \geq ijnj \leq nji1j\geq i-1(当j=i1j=i-1时,子树不包含实际关键字,只包含伪关键字di1d_{i-1}

当一棵子树成为一个结点的子树时,对于包含关键字ki,..kjk_i,..k_j的子树,子树的期望搜索代价的增加值为:

w(i,j)=l=ijpl+l=i1jqlw(i,j)=\sum^j_{l=i}p_l+\sum^j_{l=i-1}q_l

递归公式:

e[i,j]{qi1j=i1minirj{e[i,r1]+e[r+1,j]+w(i,j)}ije[i,j] \leq \begin{cases} q_{i-1} \qquad & j=i-1 \\ min_{i \leq r \leq j}\{e[i,r-1]+e[r+1,j]+w(i,j)\} \qquad & i \leq j \end{cases}

OPTIMAL-BST 时间复杂度Θ(n3)\Theta(n^3)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
OPTIMAL-BST(p, q, n)
let e[1..n+1,0..n],w[1..n+1,0..n] and root[1..n,1..n] be new tables
for i = 1 to n+1
e[i,i-1] = q_{i-1}
w[i,i-1] = q_{i-1}
for l = 1 to n
for i = 1 to n-l+1
j = i+l-1
e[i,j] = ∞
w[i,j] = w[i,j-1] + p_j +q_j
for r = i to j
t = e[i,r-1] + e[r+1,j] + w[i,j]
if t < e[i,j]
e[i,j] = t
root[i,j] = r
return e and root

贪心算法

活动选择问题

假定有一个nn活动的集合S={a1,a2,...,an}S=\{a_1,a_2,...,a_n\}。这些活动使用同一个资源,而这个资源在某个时刻只能供一个活动使用。每个活动aia_i都有一个开始时间sis_i结束时间fif_i,其中0si<fi<0\leq s_i < f_i < \infty。如果被选中,任务aia_i发生在半开时间区间[si,fi)[s_i,f_i)期间。如果两个活动aia_iaja_j满足[si,fi)[s_i,f_i)[sj,fj)[s_j,f_j)不重叠,则称它们是兼容

活动选择问题中,我们希望选出一个最大兼容活动集,假定活动已按结束时间的单调递增顺序排列:

f1f2..fn1fnf_1\leq f_2 \leq .. \leq f_{n-1} \leq f_n

最优子结构

c[i,j]c[i,j]表示集合SijS_{ij}的最优解的大小,则可得递归式:

c[i,j]{0Sij=maxakSij{c[i,k]+c[k,j]+1}Sijc[i,j] \leq \begin{cases} 0 \qquad & S_{ij} = \emptyset \\ max_{a_k \in S_{ij}}\{c[i,k]+c[k,j]+1\} \qquad & S_{ij} \neq \emptyset \end{cases}

贪心选择

考虑任意非空子问题SkS_k,令ama_mSkS_k中结束时间最早的活动,则ama_mSkS_k的某个最大兼容活动子集中

递归贪心算法

求解原问题可调用 RECURSIVE-ACTIVITY-SELECTOR(s, f, 0, n),在输入活动已按结束时间排序的前提下,时间复杂度Θ(n)\Theta(n)

1
2
3
4
5
6
7
8
RECURSIVE-ACTIVITY-SELECTOR(s, f, k, n)
m = k + 1
while m <= n and s[m] < f[k]
m = m + 1
if m <= n
return {a_m} ∪ RECURSIVE-ACTIVITY-SELECTOR(s, f, m, n)
else
return ∅
迭代贪心算法

在输入活动已按结束时间排序的前提下,时间复杂度Θ(n)\Theta(n)

1
2
3
4
5
6
7
8
9
GREEDY-ACTIVITY-SELECTOR(s, f)
n = s.length
A = {a_1}
k = 1
for m = 2 to n
if s[m] >= f[k]
A = A ∪ {a_m}
k = m
return A

霍夫曼编码

前缀码

前缀码,即没有任何码字是其他码字的前缀。前缀码可以保证达到最优数据压缩率

使用二叉树来表示前缀码:其叶结点为给定的字符。字符的二进制码字用从根结点到该字符叶结点的简单路径表示。其中 0 意味着“转向左孩子”,1 意味着“转向右孩子”:

Huffman

文件的最优编码方案总是对应一棵满二叉树,即每个非叶结点都有两个孩子结点
CC为字母表且所有字符的出现频率为正数,则最优前缀码对应的树恰好有C|C|个叶结点,每个叶结点对应字母表中的一个字符,且恰有C1|C|-1个内部结点

给定一棵对应前缀码的树TT。对于字母表CC中的每个字符cc,令属性c.freqc.freq表示cc在文件中出现的频率,令dT(c)d_T(c)表示cc的叶结点在树中的深度。则编码文件需要:

B(T)=cCc.freqdT(c)B(T)=\sum_{c \in C} c.freq \cdot d_T(c)

个二进制位,我们将B(T)B(T)定义为TT的代价

构造霍夫曼编码

假定CC是一个nn个字符的集合,而其中每个字符cCc \in C都是一个对象,其属性c.freqc.freq给出了字符的出现频率。算法使用了一个以属性freqfreq为关键字最小优先队列QQ

1
2
3
4
5
6
7
8
9
10
HUFFMAN(C)
n = |C|
Q = C
for i = 1 to n-1
allocate a new node z
z.left = x = EXTRACT-MIN(Q)
z.right = y = EXTRACT-MIN(Q)
z.freq = x.freq + y.freq
INSERT(Q, z)
return EXTRACT-MIN(Q)

假定QQ是使用最小二叉堆实现,HUFFMAN 时间复杂度O(nlgn)O(nlgn)
如果将最小二叉堆换为 van Emde Boas 树,时间复杂度O(nlglgn)O(nlglgn)

摊还分析

聚合分析

这种方法用来确定一个nn个操作的序列的总代价的上界T(n)T(n)。因而每个操作的平均代价为T(n)/nT(n)/n。我们将平均代价作为每个操作的摊还代价,因此所有操作具有相同的摊还代价

栈操作

考虑由nnPUSHPOPMULTIPOP 组成的操作序列在一个空栈上的执行情况。其代价至多为O(n)O(n),一个操作的平均时间为O(n)/n=O(1)O(n)/n=O(1)。所以,所有三种栈操作的摊还代价都是O(1)O(1)

二进制计数器递增

我们用一个位数组A[0..k1]A[0..k-1]作为计数器,其中A.length=kA.length = k。当计数器中保存的二进制值为xx时,xx的最低位保存在A[0]A[0]中,而最高位保存在A[k1]A[k-1]中。初始时x=0x=0,因此对所有i=0,1,..,k1i=0,1,..,k-1A[i]=0A[i]=0。为了将1(模2k2^k)加到计数器的值上,我们使用如下过程:

1
2
3
4
5
6
7
INCEREMENT(A)
i = 0
while i < A.length and A[i] == 1
A[i] = 0
i = i + 1
if i < A.length
A[i] = 1

一般地,对一个初值为0的计数器,在执行一个由nnINCEREMENT 操作组成的序列的过程中,A[i]A[i]会翻转n/2i\lfloor n/2^i \rfloor次。总翻转次数为:

i=0k1n2i<ni=012i=2n\sum^{k-1}_{i=0}\lfloor \frac{n}{2^i} \rfloor < n \sum^{\infty}_{i=0}\frac{1}{2^i}=2n

因此,对一个初值为0的计数器,执行一个nnINCEREMENT 操作的序列的最坏情况时间为O(n)O(n)。每个操作的平均代价,即摊还代价为O(n)/n=O(1)O(n)/n=O(1)

核算法

用核算法进行摊还分析时,我们对不同操作赋予不同费用,赋予某些造成的费用可能多于或少于其实际代价。我们将赋予一个操作的费用称为它的摊还代价
当一个操作的摊还代价超出其实际代价时,我们将差额存入数据结构中的特定对象,存入的差额称为信用
对于后续操作中摊还代价小于实际代价的情况,信用可以用来支付差额

如果用cic_i表示第ii个操作的真实代价,用ci^\hat{c_i}表示其摊还代价,则对任意nn个操作的序列,要求:

i=1nci^i=1nci\sum^n_{i=1} \hat{c_i} \geq \sum^n_{i=1}c_i

即数据结构所关联的信用必须一直为非负值

栈操作

为操作赋予如下摊还代价:

操作代价
PUSH2
POP0
MULTIPOP0

用1美元支付压栈操作的实际代价,将剩余1美元存为信用,用来支付将来出栈操作的代价
由于栈中的每个元素都存有1美元的信用,而栈中的元素始终是非负的,因此可以保证总信用值是非负的
因此,对任意nnPUSHPOPMULTIPOP 操作组成的序列,总摊还代价为总实际代价的上界由于总摊还代价为O(n)O(n),总实际代价也是O(n)O(n)

二进制计数器递增

为操作赋予如下摊还代价:

操作代价
一次置位操作2

当进行置位时,用1美元支付置为操作的实际代价,另1美元存为信用,用来支付将来复位操作的代价
由于每位都存有1美元的信用,而计数器中1的个数始终是非负的,因此可以保证总信用值是非负的
因此,对任意nnINCREMENT 操作,总摊还代价为总实际代价的上界。由于总摊还代价为O(n)O(n),总实际代价也是O(n)O(n)

势能法

我们将对一个初始数据结构D0D_0执行nn个操作。对每个i=1,2,...,ni=1,2,...,n,令cic_i为第ii个操作的实际代价,令DiD_i为在数据结构Di1D_{i-1}上执行第ii个操作得到的结果数据结构。势函数Φ\Phi将每个数据结构DiD_i映射到一个实数Φ(Di)\Phi(D_i),此值即为关联到数据结构DiD_i的势。第ii个操作的摊还代价ci^\hat{c_i}用势函数Φ\Phi定义为:

ci^=ci+Φ(Di)Φ(Di1)\hat{c_i}=c_i+\Phi(D_i)-\Phi(D_{i-1})

nn个操作的总摊还代价为:

i=1nci^=i=1n(ci+Φ(Di)Φ(Di1))=i=1nci+Φ(Dn)Φ(D0)\sum^n_{i=1} \hat{c_i} = \sum^n_{i=1}(c_i+\Phi(D_i)-\Phi(D_{i-1}))=\sum^n_{i=1}c_i+\Phi(D_n)-\Phi(D_0)

如果能定义一个势函数Φ\Phi,使得Φ(Dn)Φ(D0)\Phi(D_n) \geq \Phi(D_0),则总摊还代价i=1nci^\sum^n_{i=1} \hat{c_i}给出了总实际代价i=1nci\sum^n_{i=1}{c_i}的一个上界

我们通常将Φ(D0)\Phi(D_0)简单定义为0,然后说明对所有ii,有Φ(Di)0\Phi(D_i) \geq 0

栈操作

对于初始的空栈D0D_0,有Φ(D0)=0\Phi(D_0)=0。由于栈中对象数目永远不可能为负,所以第ii步操作得到的栈DiD_i具有非负的势,即:

Φ(Di)0=Φ(D0)\Phi(D_i) \geq 0 = \Phi(D_0)

如果第ii个操作是 PUSH 操作,此时栈中包含ss个对象,则势差为:

Φ(Di)Φ(Di1)=(s+1)s=1\Phi(D_i)-\Phi(D_{i-1}) = (s+1)-s = 1

PUSH 摊还代价为:

ci^=ci+Φ(Di)Φ(Di1)=1+1=2\hat{c_i} = c_i+\Phi(D_i)-\Phi(D_{i-1}) = 1+1=2

如果第ii个操作是 MULTIPOP 操作,将k=min(k,s)k'=min(k,s)个对象弹出栈,则势差为:

Φ(Di)Φ(Di1)=k\Phi(D_i)-\Phi(D_{i-1}) =-k'

MULTIPOP 摊还代价为:

ci^=ci+Φ(Di)Φ(Di1)=kk=0\hat{c_i} = c_i+\Phi(D_i)-\Phi(D_{i-1}) = k'-k'=0

如果第ii个操作是 POP 操作,此时栈中包含ss个对象,则势差为:

Φ(Di)Φ(Di1)=(s1)s=1\Phi(D_i)-\Phi(D_{i-1}) = (s-1)-s = -1

POP 摊还代价为:

ci^=ci+Φ(Di)Φ(Di1)=11=0\hat{c_i} = c_i+\Phi(D_i)-\Phi(D_{i-1}) = 1-1=0

每个操作的摊还代价都是O(1)O(1),因此,nn个操作的总摊还代价为O(n)O(n),为总实际代价的上界,所以nn个操作的最坏情况时间为O(n)O(n)

二进制计数器递增

将计数器执行iiINCREMENT 操作后的势定义为bib_iii次操作后计数器中1的个数
假设第iiINCREMENT 操作将tit_i个位复位,则其实际代价至多为ti+1t_i+1。势差为:

Φ(Di)Φ(Di1)(bi1ti+1)bi1=1ti\Phi(D_i)-\Phi(D_{i-1}) \leq (b_{i-1}-t_i+1)-b_{i-1}=1-t_i

摊还代价为:

ci^=ci+Φ(Di)Φ(Di1)(ti+1)+(1ti)=2\hat{c_i} = c_i+\Phi(D_i)-\Phi(D_{i-1}) \leq (t_i+1)+(1-t_i)=2

如果计数器从0开始,则Φ(D0)=0\Phi(D_0)=0。由于对所有ii均有Φ(Di)0\Phi(D_i)\geq 0,因此,一个nnINCREMENT 操作的序列的总摊还代价是总实际代价的上界,最坏情况时间为O(n)O(n)

动态表

表扩张
1
2
3
4
5
6
7
8
9
10
11
12
TABLE-INSERT(T, x)
if T.size == 0
allocate T.table with 1 slot
T.size = 1
if T.num == T.size
allocate newtable with 2*T.size slots
insert all items in T.table into newtable
free T.table
T.table = newtable
T.size = 2 * T.size
insert x into T.table
T.num = T.num + 1

ii个操作的代价为:

ci={ii1=2k1otherc_i = \begin{cases} i \qquad & i-1 = 2^k \\ 1 \qquad & other \end{cases}

nnTABLE-INSERT 操作的总代价为:

i=1ncin+j=0lgn2j<n+2n=3n\sum^n_{i=1}c_i \leq n+\sum^{\lfloor lgn \rfloor}_{j=0}2^j<n+2n=3n

图算法

基本的图算法

图的表示

邻接链表

对于图G=(V,E)G=(V,E)来说,其邻接链表表示由一个包含V|V|条链表的数组AdjAdj所构成,每个结点有一条链表
对于每个结点uVu \in V,邻接链表Adj[u]Adj[u]包含所有与结点uu之间有边相连的结点vv

  • 如果GG是一个有向图,则对于边(u,v)(u,v)来说,结点vv将出现在链表Adj[u]Adj[u]里,因此,所有邻接链表的长度之和等于E|E|
  • 如果GG是一个无向图,则对于边(u,v)(u,v)来说,结点vv将出现在链表Adj[u]Adj[u]里,结点uu将出现在链表Adj[v]Adj[v]里,因此,所有邻接链表的长度之和等于2E2|E|
  • 不论是有向图还是无向图,邻接链表存储空间需求均为Θ(V+E)\Theta(V+E)

对邻接链表稍加修改,就可以用来表示权重图:只需要将边(u,v)(u,v)的权重值w(u,v)w(u,v)存放放在结点uu的邻接链表里
邻接链表的一个潜在缺陷是无法快速判断一条边(u,v)(u,v)是否是图中的一条边

linkedlistgraph

邻接矩阵

GG的邻接矩阵表示由一个V×V|V| \times |V|的矩阵A=(aij)A=(a_{ij})予以表示,该矩阵满足下述条件:

aij={1(i,j)E0othera_{ij} = \begin{cases} 1 \qquad & (i,j) \in E \\ 0 \qquad & other \end{cases}

不管一个图有多少条边,邻接矩阵的空间需求均为Θ(V2)\Theta(V^2)
邻接矩阵也可以用来表示权重图:直接将边(u,v)E(u,v) \in E的权重w(u,v)w(u,v)存放在邻接矩阵中的第uu行第vv列记录上

matrixgraph

广度优先搜索(BFS)

为了跟踪算法的进展,广度优先搜索在概念上将每个结点涂上白色、灰色或黑色。所有结点在一开始的时候均涂上白色。在算法的推进过程中,这些结点可能会变成灰色或黑色
如果边(u,v)E(u,v) \in E且结点uu是黑色,则结点vv既可能是灰色也可能是黑色。也就是说,所有与黑色结点邻接的结点都以被发现。对于灰色结点来说,其邻接结点中可能存在未被发现的白色结点
在执行广度优先搜索的过程中将构造出一棵广度优先树

假定输入图G=(V,E)G=(V,E)是以邻接链表所表示的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
BFS(G, s)
for each vertex u in G.V - {s}
u.color = WHITE
u.d = ∞
u.π = NIL
s.color = GRAY
s.d = 0
s.π = NIL
Q = ∅
ENQUEUE(Q, s)
while Q != ∅
u = DEQUEUE(Q)
for each v in G.Adj[u]
if v.color == WHITE
v.color = GRAY
v.d = u.d + 1
v.π = u
ENQUEUE(Q, v)
u.color = BLACK

bfs

分析

广度优先搜索的总运行时间为O(V+E)O(V+E)

最短路径

我们定义从源结点ss到结点vv最短路径距离δ(s,v)\delta(s,v)为从结点ss到结点vv之间所有路径里面最少的边数
如果从结点ss到结点vv之间没有路径,则δ(s,v)=\delta(s,v) = \infty
我们称从结点ss到结点vv的长度为δ(s,v)\delta(s,v)的路径为ssvv最短路径

G=(V,E)G=(V,E)为一个有向图或无向图,又假设 BFSss为源结点在图GG上运行。那么在算法执行过程中,BFS 将发现从源结点ss可以到达的所有结点vVv \in V,并在算法终止时,对于所有的vV,v.d=δ(s,v)v \in V,v.d = \delta(s,v)。而且,对于任意可以从ss到达的结点vsv \neq s,从源结点ss到结点vv的其中一条最短路径为从结点ss到结点v.πv.\pi的最短路径再加上边(v.π,v)(v.\pi,v)

广度优先树

我们定义图GG前驱子图Gπ=(Vπ,Eπ)G_\pi =(V_\pi,E_\pi),其中Vπ={vV:v.πNIL}{s}V_\pi = \{v\in V:v.\pi \neq NIL\} \cup \{s\}Eπ={(v.π,v):vVπ{s}}E_\pi = \{(v.\pi,v):v \in V_\pi - \{s\}\}
当运行在一个有向或无向图G=(V,E)G=(V,E)上时,BFS 过程所建造出来的π\pi属性使得前驱子图Gπ=(Vπ,Eπ)G_\pi=(V_\pi,E_\pi)成为一棵广度优先树

PRINT-PATH 可打印出从源结点ss到结点vv的一条最短路径上的所有结点,这里假定 BFS 已经计算出一棵广度优先树:

1
2
3
4
5
6
7
8
PRINT-PATH(G, s, v)
if v == s
print s
elseif v.π == NIL
print "no path from" s "to" v "exists"
else
PRINT-PATH(G, s, v.π)
print v

深度优先搜索(DFS)

我们定义图GG前驱子图Gπ=(V,Eπ)G_\pi =(V,E_\pi),其中Eπ={(v.π,v):vVv.πNIL}E_\pi = \{(v.\pi,v):v \in V \land v.\pi \neq NIL\}
与广度优先搜索不同,深度优先搜索的前驱子图可能由多颗树组成,因为搜索可能从多个源结点重复进行
深度优先搜索的前驱子图形成一个由多颗深度优先树构成的深度优先森林,森林EπE_\pi中的边仍然称为树边

像广度优先搜索一样,深度优先搜索算法在搜索过程中也是对结点进行涂色来指明结点的状态。每个结点的初始颜色都是白色,在结点被发现后变为灰色,在其邻接链表被扫描完成后变为黑色。该方法可以保证每个结点仅在一棵深度优先树中出现,因此,所有的深度优先树是不相交的

深度优先算法在每个结点盖上一个时间戳。每个结点vv有两个时间戳:第一个时间戳v.dv.d记录结点vv第一次被发现的时间(涂上灰色的时候),第二个时间戳v.fv.f记录的是搜索完成对vv的邻接链表扫描的时间(涂上黑色的时候)

输入图GG既可以是无向图,可以是有向图。变量timetime是一个全局变量,用来计算时间戳:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
DFS(G)
for each vertex u in G.V
u.color = WHITE
u.π = NIL
time = 0
for each vertex u in G.V
if u.color == WHITE
DFS-VISIT(G, u)

DFS-VISIT(G, u)
time = time + 1
u.d = time
u.color = GRAY
for each v in G.Adj[u]
if v.color == WHITE
v.π = u
DFS-VISIT(G, v)
u.color = BLACK
time = time + 1
u.f = time

dfs

分析

深度优先搜索的总运行时间为O(V+E)O(V+E)

深度优先搜索的性质

括号化定理:在对有向或无向图G=(V,E)G=(V,E)进行的任意深度优先搜索中,对于任意两个结点uuvv来说,下面三种情况只有一成立:

  • 区间[u.d,u.f][u.d,u.f]和区间[v.d,v.f][v.d,v.f]完全分离,在深度优先森林中,结点uu不是结点vv的后代,结点vv也不是结点uu的后代
  • 区间[u.d,u.f][u.d,u.f]完全包含在区间[v.d,v.f][v.d,v.f]内,在深度优先树中,结点uu是结点vv的后代
  • 区间[v.d,v.f][v.d,v.f]完全包含在区间[u.d,u.f][u.d,u.f]内,在深度优先树中,结点vv是结点uu的后代

后代区间的嵌套:在有向或无向图GG的深度优先森林中,结点vv是结点uu的真后代当且仅当u.d<v.d<v.f<u.fu.d<v.d<v.f<u.f成立
白色路径定理:在有向或无向图G=(V,E)G=(V,E)的深度优先森林中,结点vv是结点uu的后代当且仅当在发现结点uu的时间u.du.d,存在一条从结点uu到结点vv的全部由白色结点所构成的路径

边的分类
  • 树边:为深度优先森林GπG_\pi中的边,如果结点vv是因算法对边(u,v)(u,v)的探索而首先被发现,则(u,v)(u,v)是一条树边
  • 后向边:后向边(u,v)(u,v)是将结点uu连接到其在深度优先树中(一个)祖先结点vv的边。由于有向图中可以有自循环,自循环也被认为是后向边
  • 前向边:是将结点uu连接到其在深度优先树中一个后代结点vv的边(u,v)(u,v)
  • 横向边:指其他所有的边。这些边可以连接同一棵深度优先树中的结点,只要其中一个结点不是另外一个结点的祖先,也可以连接不同深度优先树中的两个结点

结点vv的颜色能够告诉我们关于该条边的一些信息:

  • 结点vv白色表明该条边(u,v)(u,v)是一条树边
  • 结点vv灰色表明该条边(u,v)(u,v)是一条后向边
  • 结点vv黑色表明该条边(u,v)(u,v)是一条前向边或横向边

在对无向图GG进行深度优先搜索时,每条边要么是树边,要么是后向边

拓扑排序

对于一个有向无环图G=(V,E)G=(V,E)来说,其拓扑排序是GG中所有结点的一种线性次序,该次序满足如下条件:
如果图GG包含边(u,v)(u,v),则结点uu在拓扑排序中处于结点vv的前面(如果图GG包含环路,则不可能排出一个线性次序)

下面的简单算法可以对一个有向无环图进行拓扑排序,完成时间Θ(V+E)\Theta(V+E)

1
2
3
4
TOPOLOGICAL-SORT(G)
call DFS(G) to compute finishing times v.f for each vertex v
as each vertex is finished, insert it onto the front of a linked list
return the linked list of vertices

强连通分量

有向图G=(V,E)G=(V,E)的强连通分量是一个最大结点集合CVC \subseteq V,对于该集合中的任意一对结点uuvv来说,路径uvu \leadsto v和路径vuv \leadsto u同时存在

下面的 Kosaraju 算法使用两次深度优先搜索来计算有向图G=(V,E)G-=(V,E)的强连通分量,时间复杂度Θ(V+E)\Theta(V+E)

1
2
3
4
5
6
7
STRONGLY-CONNECTED-COMPONENTS(G)
call DFS(G) to compute finishing times u.f for each vertex u
compute G^T
call DFS(G^T), but in the main loop of DFS, consider the vertices
in order of decreasing u.f (as computed in line 1)
output the vertices of each tree in the depth-first forest formed in line 3 as a
separate strongly connected component

最小生成树

最小生成树的形成

假定有一个连通无向图G=(V,E)G=(V,E)和权重函数w:ERw:E\rightarrow \Reals。我们希望找出图GG的一棵最小生成树

使用贪心策略来生成最小生成树:

1
2
3
4
5
6
GENERIC-MST(G, w)
A = ∅
while A does not form a spanning tree
find an edge(u,v) that is safe for A
A = A ∪ {(u,v)}
return A

Kruskal算法

Kruskal 算法使用一个不相交集合数据结构来维护几个互不相交的元素集合。每个集合代表当前森林中的一棵树。通过测试 FIND-SET(u) 是否等于 FIND-SET(v) 来判断结点uu和结点vv是否属于同一棵树,使用 UNION 过程来对两棵树进行合并
时间复杂度O(ElgV)O(ElgV)

1
2
3
4
5
6
7
8
9
10
MST-KRUSKAL(G, w)
A = ∅
for each vertex v in G.V
MAKE-SET(v)
sort the edges of G.E into nondecreasing order by weight w
for each edge(u,v) in G.E, taken in nondecreasing order by weight
if FIND-SET(u) != FIND-SET(v)
A = A ∪ {(u,v)}
UNION(u,v)
return A

Prim算法

Prim 算法每一步在连接集合AAAA之外的结点的所有边中,选择一条轻量级边加入到AA

连通图GG和最小生成树的根结点rr将作为算法的输入。在算法的执行过程中,所有不在AA中的结点都存放在一个基于keykey属性的最小优先队列QQ中。对每个结点vv,属性v.keyv.key保存的是连接vv和树中结点的所有边中最小边的权重。属性v.πv.\pi给出的是结点vv在树中的父结点

若使用二叉最小优先队列,时间复杂度O(VlgV+ElgV)=O(ElgV)O(VlgV+ElgV)=O(ElgV);若使用斐波那契堆来实现最小优先队列,时间复杂度O(E+VlgV)O(E+VlgV)

1
2
3
4
5
6
7
8
9
10
11
12
MST-PRIM(G, w, r)
for each u in G.V
u.key = ∞
u.π = NIL
r.key = 0
Q = G.V
while Q != ∅
u = EXTRACT-MIN(Q)
for each v in G.Adj[u]
if v in Q and w(u,v) < v.key
v.π = u
v.key = w(u,v)

单源最短路径

最短路径问题中,我们给定一个带权重的有向图G=(V,E)G=(V,E)和权重函数w:ERw: E\rightarrow \Reals,该权重函数将每条边映射到实数值的权重上
图中一条路径p=v0,v1,...,bkp=\langle v_0,v_1,...,b_k \rangle的权重w(p)w(p)是构成该路径的所有边的权重之和:

w(p)=i=1kw(vi1,vi)w(p)=\sum^k_{i=1}w(v_{i-1},v_i)

定义从结点auu到结点vv最短路径权重δ(u,v)\delta(u,v)如下:

δ(u,v){min{w(p):upv}if exists a path from u to vother\delta(u,v)\leq \begin{cases} min\{w(p):u \leadsto^p v \} \qquad & if \ exists \ a\ path\ from\ u \ to \ v \\ \infty \qquad & other \end{cases}

从结点uu到结点vv最短路径定义为任何一条权重为w(p)=δ(w,v)w(p)=\delta(w,v)的从uuvv的路径pp

常规概念

负权重的边

如果图G=(V,E)G=(V,E)不包含从源结点ss可以到达的权重为负值的环路,则对于所有的结点vVv \in V,最短路径权重δ(s,v)\delta(s,v)都有精确定义,即使其取值为负数
如果图GG包含从ss可以达到的权重为负值的环路,则最短路径权重无定义

环路

最短路径不能包含权重为负值的环路
最短路径不能包含权重为正值的环路

最短路径的表示

π\pi值所诱导的前驱子图Gπ=(Vπ,Eπ)G_\pi=(V_\pi,E_\pi)定义如下:

Vπ={vV:v.πNIL}{s}V_\pi = \{v\in V:v.\pi \neq NIL\} \cup \{s\}

Eπ={(v.π,v)E:vVπ{s}}E_\pi = \{(v.\pi,v) \in E:v \in V_\pi - \{s\}\}

在算法终止时,GπG_\pi是一棵最短路径树
最短路径不一定是唯一的,最短路径树也不一定是唯一的

松弛操作

对于每个结点vv,我们维持一个属性v.dv.d来记录从原结点ss到结点vv的最短路径权重的上界。我们称v.dv.dssvv最短路径估计。我们使用下面运行时间为Θ(V)\Theta(V)的算法来对最短路径估计和前驱结点进行初始化:

1
2
3
4
5
INITIALIZE-SINGLE-SOURCE(G, s)
for each vertex v in G.V
v.d = ∞
v.π = NIL
s.d = 0

RELAX 过程对边(u,v)(u,v)O(1)O(1)时间内进行松弛操作:

1
2
3
4
RELAX(u, v, w)
if v.d > u.d + w(u,v)
v.d = u.d + w(u,v)
v.π = u
最短路径和松弛操作的性质

三角不等式性质:对于任何边(u,v)E(u,v) \in E,有δ(s,v)δ(s,u)+w(u,v)\delta(s,v) \leq \delta(s,u)+w(u,v)
上界性质:对于所有的结点vVv \in V,有v.dδ(s,v)v.d \geq \delta(s,v)。一旦v.dv.d的取值达到δ(s,v)\delta(s,v),其值将不再发生变化
非路径性质:如果从结点ss到结点vv之间不存在路径,则有v.d=δ(s,v)=v.d=\delta(s,v)=\infty
收敛性质:对于某些结点u,vVu,v \in V,如果suvs \leadsto u \rightarrow v是图GG中的一条最短路径,并且在对边(u,v)(u,v)进行松弛前的任意时间有u.d=δ(s,u)u.d=\delta(s,u),则在之后的所有时间有v.d=δ(s,v)v.d=\delta(s,v)
路径松弛性质:如果p=v0,v1,...,vkp=\langle v_0,v_1,...,v_k \rangle是从源结点s=v0s=v_0到结点vkv_k的一条最短路径,且对pp中的边所进行的松弛的次序为(v0,v1)(v_0,v_1)(v1,v2)(v_1,v_2),…,(vk1,vk)(v_{k-1},v_k),则vk.d=δ(s,vk)v_k.d=\delta(s,v_k),该性质的成立与任何其他的松弛操作无关,即使这些松弛操作是与对pp上的边所进行的松弛操作是穿插进行的

Bellman-Ford算法

Bellman-Ford 算法解决一般情况下的单元最短路径问题,边的权重可以为负值
Bellman-Ford 算法返回 TRUE 值当且仅当输入图不包含可以从源结点到达的权重为负值的环路:

1
2
3
4
5
6
7
8
9
BELLMAN-FORD(G, w, s)
INITIALIZE-SINGLE-SOURCE(G, s)
for i = 1 to |G.V| - 1
for each edge(u,v) in G.E
RELAX(u, v, w)
for each edge(u,v) in G.E
if v.d > u.d + w(u,v)
return FALSE
return TRUE

Bellman-Ford 算法总运行时间O(VE)O(VE)

有向无环图中的单源最短路径问题

根据结点的拓扑排序次序来对带权重的有向无环图G=(V,E)G=(V,E)进行边的松弛操作,我们便可以在O(V,E)O(V,E)时间内计算出从单个源结点到所有结点之间的最短路径:

1
2
3
4
5
6
DAG-SHORTEST-PATHS(G, w, s)
topologically sort the vertices of G
INITIALIZE-SINGLE-SOURCE(G, s)
for each vertex u, taken in topologically sorted order
for each vertex v in G.Adj[u]
RELAX(u, v, w)

Dijkstra算法

Dijkstra 算法要求所有边的权重都为非负值:

1
2
3
4
5
6
7
8
9
DIJKSTRA(G, w, s)
INITIALIZE-SINGLE-SOURCE(G, s)
S = ∅
Q = G.V
while Q != ∅
u = EXTRACT-MIN(Q)
S = S ∪ {u}
for each vertex v in G.Adj[u]
RELAX(u, v, w)

Dijkstra 算法总运行时间依赖于最小优先队列的实现:

  • 通过利用结点的编号为1V1 - |V|来维持最小优先队列。在这种情况下,每次 INSERTDECREASE-KEY 操作的执行时间为O(1)O(1),每次 EXTRACT-MIN 的操作时间为O(V)O(V),算法总运行时间为O(V2+E)=O(V2)O(V^2+E)=O(V^2)
  • 若图为稀疏图,特别地,如果E=o(V2/lgV)E=o(V^2/lgV),则可以通过二叉堆来实现最小优先队列,每次 EXTRACT-MIN 的操作时间为O(lgV)O(lgV),每次 DECREASE-KEY 的操作时间为O(lgV)O(lgV),构建最小二叉堆的成本为O(V)O(V),算法总运行时间为O((V+E)lgV)O((V+E)lgV)。若所有结点都可以从源结点到达,则该时间为O(ElgV)O(ElgV)
  • 若使用斐波那契堆来时间最小优先队列,每次 EXTRACT-MIN 的操作时间为O(lgV)O(lgV),每次 DECREASE-KEY 的操作时间为O(1)O(1),算法总运行时间为O(VlgV+E)O(VlgV+E)

差分约束和最短路径

差分约束系统

设向量x=(x1,x2,...,xn)x=(x_1,x_2,...,x_n)为差分约束系统AxbAx \leq b的一个解,设dd为任意常数,则x+d=(x1+d,x2+d,...,xn+d)x+d=(x_1+d,x_2+d,...,x_n+d)也是该差分约束系统的一个解

约束图

给定差分约束系统AxbAx \leq b,其对应的约束图是一个带权重的有向图G=(V,E)G=(V,E),这里:

V={v0,v1,...,vn}V=\{v_0,v_1,...,v_n\}

E={(vi,vj):vjvibk是一个约束条件}{(v0,v1),(v0,v2),...(v0,vn)}E=\{(v_i,v_j):v_j-v_i \leq b_k\text{是一个约束条件}\} \cup \{(v_0,v_1),(v_0,v_2),...(v_0,v_n)\}

  • 约束图包含一个额外的结点v0v_0,用来保证图中至少存在一个结点,从其出发可以到达所以其他的结点
  • 如果xjxibkx_j-x_i \leq b_k是一个差分约束条件,则边(vi,vj)(v_i,v_j)的权重为w(vi,vj)=bkw(v_i,v_j)=b_k
  • 所有从结点v0v_0发出的边的权重为0

给定差分约束系统AxbAx \leq b,设G=(V,E)G=(V,E)是该差分约束系统所对应的约束图。如果图GG不包含权重为负值的环路,则:

x=(δ(v0,v1),δ(v0,v2),...,δ(v0,vn))x=(\delta(v_0,v_1),\delta(v_0,v_2),...,\delta(v_0,v_n))

是该系统的一个可行解。如果图GG包含权重为负值的环路,则该系统没有可行解

求解差分约束系统

一个有nn个未知变量和mm个约束条件的差分约束系统所生成的约束图有n+1n+1个结点和m+nm+n条边。使用 Bellman-Ford 算法可以在O((n+1)(m+n))=O(n2+mn)O((n+1)(m+n)) = O(n^2+mn)时间内求解该系统

所有结点对的最短路径

假定结点的编号为1,2,...,V1,2,...,|V|,因此,算法的输入将是一个n×nn \times n的矩阵WW,该矩阵代表的是一个有nn个结点的有向图G=(V,E)G=(V,E)的边的权重,即W=(wij)W=(w_{ij}),其中:

wij={0i=jwijij(i,j)Eij(i,j)Ew_{ij} = \begin{cases} 0 \qquad & i=j \\ w_{ij} \qquad & i \neq j \land (i,j) \in E \\ \infty \qquad & i \neq j \land (i,j) \notin E \end{cases}

我们允许存在负权重的边,但假定图中不包括权重为负值的环路

所有结点对最短路径算法的表格输出也是一个n×nn \times n的矩阵D=(dij)D=(d_{ij}),其中dijd_{ij}代表的是从结点ii到结点jj的一条最短路径的权重
前驱结点矩阵Π=(πij)\Pi =(\pi_{ij}),其中πij\pi_{ij}i=ji=j或从iijj不存在路径时为NILNIL,在其他情况下给出的是从结点ii到结点jj的某条最短路径上结点jj的前驱结点。由矩阵Π\Pi的第ii行所诱导的子图一个是一棵根结点为ii的最短路径树。对于每个结点iVi \in V,定义图GG对于结点ii的前驱子图为Gπ,i=(Vπ,i,Eπ,i)G_{\pi,i}=(V_{\pi,i},E_{\pi,i}),其中:

Vπ,i={jV:πijNIL}{i}Eπ,i={(πij,j):jVπ,i{i}}V_{\pi,i}=\{j\in V:\pi_{ij} \neq NIL\} \cup \{i\} \qquad E_{\pi,i}=\{(\pi_{ij},j):j\in V_{\pi,i}-\{i\}\}

如果Gπ,iG_{\pi,i}是一棵最短路径树,则下面的过程将打印出从结点ii到结点jj的一条最短路径:

1
2
3
4
5
6
7
8
PRINT-ALL-PAIRS-SHORTEST-PATH(Π, i, j)
if i == j
print i
elseif π_{ij} == NIL
print "no path from" i "to" j "exists"
else
PRINT-ALL-PAIRS-SHORTEST-PATH(Π, i, π_{ij})
print j

最短路径和矩阵乘法

最短路径的结构

考虑从结点ii到结点jj的一条最短路径pp,假定pp至多包含mm条边,还假定没有权重为负值的环路,且mm为有限值。如果i=ji=j,则pp的权重为0且不包含任何边。如果结点ii和结点jj不同,则将路径分解为ipkji \leadsto^{p'}k \rightarrow j,其中路径pp'至多包含m1m-1条边,则有:

δ(i,j)=δ(i,k)+wkj\delta(i,j)=\delta(i,k)+w_{kj}

所有结点对最短路径问题的递归解

lij(m)l^{(m)}_{ij}为从结点ii到结点jj的至多包含mm条边的任意路径中的最小权重。当m=0m=0时,从结点ii到结点jj之间存在一条没有边的最短路径当且仅当i=ji=j,因此有:

lij(m)={0i=jijl^{(m)}_{ij} = \begin{cases} 0 \qquad & i=j \\ \infty \qquad & i \neq j \end{cases}

递归定义:

lij(m)=min{lij(m1),min1kn{lij(m1)+wkj}}=min1kn{lij(m1)+wkj}l^{(m)}_{ij} = min\{l^{(m-1)}_{ij},min_{1 \leq k \leq n}\{l^{(m-1)}_{ij} +w_{kj}\}\} = min_{1 \leq k \leq n}\{l^{(m-1)}_{ij} + w_{kj}\}

最短路径权重可以由下面的公式给出:

δ(i,j)=lij(n1)=lij(n)=lij(n+1)=...\delta(i,j)=l^{(n-1)}_{ij}=l^{(n)}_{ij}=l^{(n+1)}_{ij} = ...

自底向上计算最短路径权重

EXTEND-SHORTEST-PATHS 过程可以在给定wwL(m1)L^{(m-1)}的情况下,计算出L(m)L^{(m)},算法运行时间Θ(n3)\Theta(n^3)

1
2
3
4
5
6
7
8
9
EXTEND-SHORTEST-PATHS(L, W)
n = L.rows
let L' = (l'_{ij}) be a new n * n matrix
for i = 1 to n
for j = 1 to n
l'_{ij} = ∞
for k = 1 to n
l'_{ij} = min(l'_{ij}, l_{ik} + w_{kj})
return L'

ABA \cdot B表示由算法 EXTEND-SHORTEST-PATHS(A, B) 所返回的矩阵“乘积”,我们可以计算出下面由n1n-1个矩阵所构成的矩阵序列:
L(1)=L(0)W=WL^{(1)} = L^{(0)} \cdot W = W
L(2)=L(1)W=W2L^{(2)} = L^{(1)} \cdot W = W^2
L(3)=L(2)W=W3L^{(3)} = L^{(2)} \cdot W = W^3

L(n1)=L(n2)W=Wn1L^{(n-1)} = L^{(n-2)} \cdot W = W^{n-1}

SLOW-ALL-PAIRS-SHORTEST-PATHS 过程可以在Θ(n4)\Theta(n^4)的时间内计算出矩阵L(n1)=Wn1L^{(n-1)} = W^{n-1}

1
2
3
4
5
6
7
SLOW-ALL-PAIRS-SHORTEST-PATHS(W)
n = W.rows
L(1) = W
for m = 2 to n-1
let L(m) be a new n * n matrix
L(m) = EXTEND-SHORTEST-PATHS(L(m-1), W)
return L(n-1)

可以仅用lg(n1)\lceil lg(n-1) \rceil个矩阵乘积来计算矩阵L(n1)L^{(n-1)}
L(1)=WL^{(1)} = W
L(2)=W2=WWL^{(2)} = W^2 = W \cdot W
L(4)=W4=W2W2L^{(4)} = W^4 = W^2 \cdot W^2
L(8)=W8=W4W4L^{(8)} = W^8 = W^4 \cdot W^4

L(2lg(n1))=W2lg(n1)=W2lg(n1)1W2lg(n1)1L^{(2^{\lceil lg(n-1) \rceil})} = W^{2^{\lceil lg(n-1) \rceil}} = W^{2^{\lceil lg(n-1) \rceil-1}} \cdot W^{2^{\lceil lg(n-1) \rceil-1}}
由于2lg(n1)n12^{\lceil lg(n-1) \rceil}\geq n-1,最后有L(2lg(n1))=L(n1)L^{(2^{\lceil lg(n-1) \rceil})}=L^{(n-1)}
FASTER-ALL-PAIRS-SHORTEST-PATHS 过程使用重复平方技术来计算上述矩阵序列,运行时间为Θ(n3lgn)\Theta(n^3lgn)

1
2
3
4
5
6
7
8
FASTER-ALL-PAIRS-SHORTEST-PATHS(W)
n = W.rows
L(1) = W
m = 1
while m < n-1
let L(2m) be a new n * n matrix
L(2m) = EXTEND-SHORTEST-PATHS(L(m), L(m))
return L(m)

Floyd-Warshall算法

所有结点对最短路径问题的一个递归解

dij(k)d^{(k)}_{ij}为从结点ii到结点jj的所有中间结点全部取自集合{1,2,...,k}\{1,2,...,k\}的一条最短路径的权重。递归定义dij(k)d^{(k)}_{ij}如下:

dij(k)={wijk=0min(dij(k1),dik(k1)+dkj(k1))k1d^{(k)}_{ij} = \begin{cases} w_{ij} \qquad & k=0 \\ min(d^{(k-1)}_{ij},d^{(k-1)}_{ik}+d^{(k-1)}_{kj}) \qquad & k \geq 1 \end{cases}

矩阵D(n)=(dij(n))D^{(n)} = (d^{(n)}_{ij})给出的就是最后的答案:对于所有的i,jVi,j \in Vdij(n)=δ(i,j)d^{(n)}_{ij}=\delta(i,j)

自底向上计算最短路径权重

FLOYD-WARSHALL 算法的输入为一个n×nn \times n的矩阵WW,返回最短路径权重矩阵D(n)D^{(n)},运行时间为Θ(V3)\Theta(V^3)

1
2
3
4
5
6
7
8
9
FLOYD-WARSHALL(W)
n = W.rows
D(0) = W
for k = 1 to n
let D(k) = (d(k)_{ij}) be a new n * n matrix
for i = 1 to n
for j = 1 to n
d(k)_{ij} = min(d(k-1)_{ij},d(k-1)_{ik}+d(k-1)_{kj})
return D(n)
构建一条最短路径

Floyd-Warshall 算法中,有多种不同的方法来构建最短路径

  • 先计算最短路径权重矩阵DD,然后从DD矩阵来构造前驱矩阵Π\Pi
  • 可以在计算矩阵D(k)D^{(k)}的同时计算前驱矩阵Π\Pi。即计算一个矩阵序列Π(0)\Pi^{(0)}Π(1)\Pi^{(1)},…,Π(n)\Pi^{(n)}。定义πij(k)\pi^{(k)}_{ij}为从结点ii到结点jj的一条所有中间结点都取自集合{1,2,...,k}\{1,2,...,k\}的最短路径上jj的前驱结点

k=0k=0时,从iijj的一条最短路径没有中间结点,因此:

πij(0)={NILi=jwij=iijwij<\pi^{(0)}_{ij} = \begin{cases} NIL \qquad & i=j \lor w_{ij} = \infty \\ i \qquad & i \neq j \land w_{ij} < \infty \end{cases}

对于k1k \geq 1,如果考虑路径ikj,kji \leadsto k \leadsto j,k \neq j,则所选择的结点jj的前驱与我们选择的从结点kk到结点jj的一条中间结点全部取自集合{1,2,...,k1}\{1,2,...,k-1\}的最短路径上的前驱是一样的。否则,所选择的结点jj的前驱与选择的从结点ii到结点jj的一条中间结点全部取自集合{1,2,...,k1}\{1,2,...,k-1\}的最短路径上的前驱是一样的。也就是说,对于k1k \geq 1,有:

πij(k)={πij(k1)dij(k1)dik(k1)+dkj(k1)πkj(k1)dij(k1)>dik(k1)+dkj(k1)\pi^{(k)}_{ij} = \begin{cases} \pi^{(k-1)}_{ij} \qquad & d^{(k-1)}_{ij} \leq d^{(k-1)}_{ik}+d^{(k-1)}_{kj} \\ \pi^{(k-1)}_{kj} \qquad & d^{(k-1)}_{ij} > d^{(k-1)}_{ik}+d^{(k-1)}_{kj} \end{cases}

有向图的传递闭包

定义图GG传递闭包为图G=(V,E)G^*=(V,E^*),其中E={(i,j):如果图G中包含一条从结点i到结点j的路径}E^*=\{(i,j):\text{如果图}G\text{中包含一条从结点}i\text{到结点}j\text{的路径}\}

一种时间复杂度为Θ(n3)\Theta(n^3)的计算图GG的传递闭包的办法是给EE的每条边赋予权重1,然后运行 Floyd-Warshall 算法。如果存在一条从结点ii到结点jj的路径,则有dij<nd_{ij}<n;否则dij=d_{ij}=\infty

另一种类似办法是:以逻辑或操作(\lor)和逻辑与操作(\land)来替换 Floyd-Warshall 算法中的算术操作 min+,以此节省时间和空间

对于i,j,k=1,2,...,ni,j,k=1,2,...,n,定义:如果图GG中存在一条从结点ii到结点jj的所有中间结点都取自集合{1,2,...,k}\{1,2,...,k\}的路径,则tij(k)t^{(k)}_{ij}为1;否则,tij(k)t^{(k)}_{ij}为0。递归定义如下:

tij(0)={0ij(i,j)E1i=j(i,j)Et^{(0)}_{ij} = \begin{cases} 0 \qquad & i \neq j \land (i,j) \notin E \\ 1 \qquad & i = j \lor (i,j) \in E \end{cases}

对于k1k \geq 1

tij(k)=tij(k1)(tik(k1)tkj(k1))t^{(k)}_{ij} = t^{(k-1)}_{ij} \lor (t^{(k-1)}_{ik} \land t^{(k-1)}_{kj})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TRANSITIVE-CLOSURE(G)
n = |G.V|
let T(0) = (t(0)_{ij}) be a new n * n matrix
for i = 1 to n
for j = 1 to n
if i == j or (i,j) in G.E
t(0)_{ij} = 1
else
t(0)_{ij} = 0
for k = 1 to n
let T(k) = (t(k)_{ij}) be a new n * n matrix
for i = 1 to n
for j = 1 to n
t(k)_{ij} = t(k-1)_{ij} || (t(k-1)_{ik} && t(k-1)_{kj})
return T(n)

用于稀疏图的Johnson算法

Johnson 算法可以在O(V2lgV+VE)O(V^2lgV+VE)时间内找到所有结点对之间的最短路径

Johnson 算法使用的技术称为重新赋予权重
如果图G=(V,E)G=(V,E)中所有的边权重ww均为非负值,则可以通过对每一个结点运行一次 Dijkstra 算法来找到所有结点对之间的最短路径;如果使用斐波那契堆最小优先队列,该算法的运行时间O(V2lgV+VE)O(V^2lgV+VE)
如果图G=(V,E)G=(V,E)包含权重为负值的边,但没有权重为负值的环路,则只要计算出一组新的非负权重值,然后使用同样的方法。新赋予的权重w^\hat{w}必须满足以下两个重要性质:

  • 对于所有结点对u,vVu,v \in V,一条路径pp是在使用权重函数ww时从结点uu到结点vv的一条最短路径,当且仅当pp是在使用权重函数w^\hat{w}时从uuvv的一条最短路径
  • 对于所有的边(u,v)(u,v),新权重w^(u,v)\hat{w}(u,v)为非负值
重新赋予权重来维持最短路径

给定带权重的有向图G=(V,E)G=(V,E),其权重函数为w:ERw:E \rightarrow \Reals,设h:VRh:V \rightarrow \Reals为任意函数,该函数将结点映射到实数上。对于每条边(u,v)E(u,v) \in E,定义:

w^(u,v)=w(u,v)+h(u)h(v)\hat{w}(u,v)=w(u,v)+h(u)-h(v)

p=v0,v1,...,vkp=\langle v_0,v_1,...,v_k \rangle为从结点v0v_0到结点vkv_k的任意一条理解,那么pp是在使用权重函数ww时从结点v0v_0到结点vkv_k的一条最短路径,当且仅当pp是在使用权重函数w^\hat{w}时从结点v0v_0到结点vkv_k的一条最短路径,即w(p)=δ(v0,vk)w(p)=\delta(v_0,v_k)当且仅当w^(p)=δ^(v0,vk)\hat{w}(p)=\hat{\delta}(v_0,v_k)。而且,图GG在使用权重函数ww时不包含权重为负值的环路,当且仅当pp在使用权重函数w^\hat{w}也不包括权重为负值的环路。

计算所有结点对之间的最短路径

Johnson 算法在执行过程中需要使用 Bellman-Ford 算法和 Dijkstra 算法作为子程序来计算所有结点对之间的最短路径。该算算假定所有的边都保持在邻接链表里,其返回一个V×V|V| \times |V|的矩阵D=dij,dij=δ(i,j)D=d_{ij},d_{ij}=\delta(i,j),或者报告输入图包含一个权重为负值的环路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
JOHNSON(G, w)
compute G', where G'.V = G.V ∪ {s},
G'.E = G.E ∪ {(s,v):v in G.V}
and w(s,v) = 0 for all v in G.V
if BELLMAN-FOLD(G', w, s) == FALSE
print "the input graph contains a negative-weight cycle"
else
for each vertex v in G'.V
set h(v) to the value of δ(s, v) computed by the Bellman-Ford algorithm
for each edge(u, v) in G'.E
\hat_w(u,v) = w(u,v) + h(u) - h(v)
let D = (d_{uv}) be a new n * n matrix
for each vertex u in G.V
run DIJKSTRA(G, \hat_w, u) to compute \hat_δ(u, v) for all v in G.V
for each vertex v in G.V
d_{uv} = \hat_δ(u, v) + h(v) - h(u)
return D

如果使用斐波那契堆来实现 Dijkstra 算法里的最小优先队列,则 Johnson 算法的运行时间为O(V2lgV+VE)O(V^2lgV+VE),使用更简单的二叉最小堆实现则运行时间为O(VElgV)O(VElgV)

最大流

流网络

流网络和流

流网络G=(V,E)G=(V,E)是一个有向图,图中每条边(u,v)E(u,v) \in E有一个非负的容量值c(u,v)0c(u,v) \geq 0
如果边集合EE包含一条边(u,v)(u,v),则图中不存在反方向的边(v,u)(v,u)
在图中不允许自循环,对于每个结点vVv \in V,流网络都包含一条路径svts \leadsto v \leadsto t
流网络图是连通的,且由于除源结点外的每个结点都至少有一条进入的边,有EV1|E| \geq |V|-1

flowNetwork

流的形式化定义:
G=(V,E)G=(V,E)为一个流网络,其容量函数为cc。设ss为网络的源结点,tt为汇点。GG中的流是一个实值函数ffV×VRV \times V \rightarrow \Reals,满足下面的两条性质:

  • 容量限制:对于所有的结点uvVu,v \in V,要求0f(u,v)c(u,v)0 \leq f(u,v) \leq c(u,v)
  • 流量守恒:对于所有的结点uV{s,t}u \in V- \{s,t\},要求:

vVf(u,v)=vVf(v,u)\sum_{v\in V}f(u,v) = \sum_{v\in V}f(v,u)

(u,v)E(u,v) \notin E时,从结点uu到结点vv之间没有流,因此f(u,v)=0f(u,v)=0

一个流ff的值f|f|定义如下:

f=vVf(s,v)vVf(v,s)|f|=\sum_{v \in V}f(s,v)-\sum_{v \in V}f(v,s)

Ford-Fulkerson方法

Ford-Fulkerson 方法循环增加流的值。在开始的时候,对于所有的结点u,vVu,v \in Vf(u,v)=0f(u,v)=0,给出的初始流值为0。在每次迭代中,我们将图GG的流值进行增加,方法就是在一个关联的残存网络GfG_f中寻找一条增广路径。重复对流进行这一过程,直到残存网络中不再增加增广路径为止:

1
2
3
4
5
FORD-FULKERSON-METHOD(G, s, t)
initialize flow f to 0
while there exists an augmenting path p in the residual network Gf
augment flow f along p
return f
残存网络

残存网络由那些仍有空间对流量进行调整的边构成。流网络的一条边可以允许的额外流量等于该边的容量减去该边上的流量:

  • 如果该差值为正,则将该条边置于图GfG_f中,并将其残存容量设置为cf(u,v)=c(u,v)f(u,v)c_f(u,v)=c(u,v)-f(u,v);同时将边(v,u)(v,u)加入到图GfG_f中,并将其残存容量设置为cf(v,u)=f(u,v)c_f(v,u)=f(u,v)
  • 如果边(u,v)(u,v)的流量等于其容量,则其cf(u,v)=0c_f(u,v)=0,该条边将不属于图GfG_f

形式化地,假定有一个流网络G=(V,E)G=(V,E),其源结点为ss,汇点为tt。设ff为图GG中的一个流,考虑结点对u,vVu,v \in V,定义残存容量cf(u,v)c_f(u,v)

cf(u,v)={c(u,v)f(u,v)(u,v)Ef(v,u)(v,u)E0otherc_f(u,v) = \begin{cases} c(u,v)-f(u,v) \qquad & (u,v) \in E\\ f(v,u) \qquad & (v,u) \in E\\ 0 \qquad & other \end{cases}

给定一个流网络G=(V,E)G=(V,E)和一个流ff,则由ff所诱导的图GG残存网络Gf=(V,Ef)G_f=(V,E_f),其中:

Ef={(u,v)V×V:cf(u,v)>0}E_f = \{(u,v) \in V \times V:c_f(u,v)>0\}

如果ffGG的一个流,ff'是对应的残存网络GfG_f中的一个流,定义fff \uparrow f'为流ff'对流ff的递增,它是一个从V×VRV \times V \rightarrow \Reals的函数,其定义如下:

(ff)(u,v)={f(u,v)+f(u,v)f(v,u)(u,v)E0other(f \uparrow f')(u,v) = \begin{cases} f(u,v)+f'(u,v)-f'(v,u) \qquad & (u,v) \in E\\ 0 \qquad & other \end{cases}

增广路径

给定流网络G=(V,E)G=(V,E)和流ff,增广路径pp是残存网络GfG_f中一条从源结点ss到汇点tt的简单路径
我们称在一条增广路径pp上能够为每条边增加的流量的最大值为路径pp残存容量,该容量由下面的表达式给出:

cf(p)=min{cf(u,v):(u,v)属于路径p}c_f(p)=min\{c_f(u,v):(u,v)\text{属于路径}p\}

流网络的切割

流网络G=(V,E)G=(V,E)中的一个切割(S,T)(S,T)将结点集合VV划分为SST=VST=V-S两个集合,使得sSs \in StTt \in T。若ff是一个流,则定义横跨切割(S,T)(S,T)净流量f(S,T)f(S,T)如下:

f(S,T)=uSvTf(u,v)uSvTf(v,u)f(S,T)=\sum_{u\in S}\sum_{v \in T}f(u,v)-\sum_{u\in S}\sum_{v \in T}f(v,u)

切割(S,T)(S,T)容量

c(S,T)=uSvTc(u,v)c(S,T)=\sum_{u\in S}\sum_{v \in T}c(u,v)

一个网络的最小切割是整个网络中容量最小的切割

ff为流网络GG的一个流,该流网络的源结点为ss,汇点为tt,设(S,T)(S,T)为流网络GG的任意切割,则横跨切割(S,T)(S,T)的净流量为f(S,T)=ff(S,T)=|f|
流网络GG中任意流ff的值不能超过GG的任意切割的容量
ff为流网络G=(V,E)G=(V,E)中的一个流,该流网络的源结点为ss,汇点为tt,则下面的条件是等价的:

  • ffGG的一个最大流
  • 残存网络GfG_f不包括任何增广路径
  • f=c(S,T)|f|=c(S,T),其中(S,T)(S,T)是流网络GG的某个切割
基本的Ford-Fulkerson算法

Ford-Fulkerson 方法的每次迭代中,寻找某条增广路径pp,然后使用pp来对流ff进行修改(增加)。通过为每条边(u,v)E(u,v) \in E更新流属性(u,v).f(u,v).f来计算流网络G=(V,E)G=(V,E)中的最大流:

1
2
3
4
5
6
7
8
9
10
FORD-FULKERSON(G, s, t)
for each edge(u,v) in G.E
(u,v).f = 0
while there exists a path p from s to t in the residual network Gf
cf(p) = min{cf(u,v):(u,v)is in p}
for each edge(u,v) in p
if (u,v) in E
(u,v).f = (u,v).f + cf(p)
else
(u,v).f = (u,v).f - cf(p)

如果ff^*表示转换后网络中的一个最大流,则 Ford-Fulkerson 算法的运行时间为O(Ef)O(E|f^*|)

Edmonds-Karp算法

通过在算法第3行寻找增广路径的操作中使用广度优先搜索来改善 Ford-Fulkerson 算法的效率:
在残存网络中选择的增广路径是一条从源结点ss到汇点tt的最短路径,其中每条边的权重为单位距离
Edmonds-Karp 算法的运行时间为O(VE2)O(VE^2)