转义序列

C0C1 控制字符集

C0 控制字符集

  • CO 控制字符集码位范围:0x00 - 0x1F

    • ASCII 中定义控制字符标准
    • 码位限定在 1byte 可以表示,避免终端机需要实现状态机处理多字节控制序列
    • 现只有少数控制字符被使用
  • C0 控制字符码位范围之外,还有定义有两个具备控制符特点的字符

    • 0x7Fdelete
    • 0x20space

C1 控制字符集

  • C1 控制字符集码位范围:0x80 - 0x9F

    • 8bits ISO/IEC 8859 ASCII 扩展提出后
      • 考虑到可打印字符的最高比特位去掉之后不应变成控制字符
      • C0 控制字符集作为低位、最高位置 1,得到 C1 控制字符集
    • C1 码位在经常被私有编码方案(Windows-1252Mac Os Roman)用于提供额外的可打印字符
  • ISO/IEC 8859 ASCII 扩展标准中指定

    • 为兼容 7bits 传输,所有 C1 控制字符使用 ESC 开头的 7bits 字符序列表示

标准 C 转义规则

  • 非打印(包括控制)字符可以通过其 ASCII 码位 16 进制、8 进制表示
    • \0[ooo]:八进制数 oo 码位字符
    • \x[hh]:十六进制数 hh 码位字符
      • \x0a:同 \n
  • 针对常用非打印字符,有如下简写方式
    • \\:反斜杠 \
    • \':单引号 '
    • \":双引号 "
    • \aBEL ASCII 响铃
    • \bBS ASCII退格
    • \fFF ASCII 进纸
    • \nLF/NL ASCII 换行,开启新行
    • \rCR ASCII 回车,“指针移至行首”
    • \tTAB ASCII 制表符
    • \vVT 垂直制表符

ANSI Escape Sequences

ANSI:一种 In-band Signaling 的转义序列标准,用于控制终端上 光标位置、颜色、其他选项

  • 在文本中嵌入的 ANSI 转义序列,终端会将 ANSI 转义序列解释为相应指令,而不是普通字符

    • ANSI 转义序列使用 ASCII 中字符传递所有信息
  • ANSI 转义序列有不同长度,但都

    • ASCII 字符 ESC0x1b) 开头
      • 8 进制表示:\033
      • 16 进制表示:\x1b
    • 第二字节则是 0x45 - 0x5FASCIIi @A-Z[\]^_)范围内的字符
  • 标准规定,在 8bits 环境中

    • ANSI 转义序列前两个字节的序列可以合并为 0x80 - 0x9F 范围内的单个字节(即 C1 控制字符)
    • 但在现代设备上,C1 控制字符码位被用于其他目的,一般不被使用
      • UTF-8 编码对 x80 字符本就需要 2bytes
      • Windows-1252 编码将 C1 控制字符码位挪作他用

No-CSI - 非控制序列

序列(省略 ESC 对应 C1 名称 效果
N 0x8E SS2 - Single Shift 2 从替代 G2 字符集中选择字符
O 0x8F SS3 - Single Shift 3 从替代 G3 字符集中选择字符
P 0x90 DCS - Device Control String 控制设备
D 仅换行,不重置光标至行首
E 换行并充值光标至行首,类似LF
H 制表,类似TAB
M 翻转换行,回到上一行
X 0x98 SOS - Start of String 引用由 ST 终止的一串文本参数
^ 0x9E PM - Privacy Message 引用由 ST 终止的以穿文本参数
_ 0x9F APC - Application Program Command 引用由 ST 终止的一串文本参数
c - RIS - Reset to Initial State 类似clear命令
[ 0x9B CSI - Control String Sequence 控制序列导入器,某些终端中也可以使用0x9D
\ 0x9C ST - String Terminator 终止其他控件得字符串
] 0x9D OCS - Operating System Command 启动操作系统使用的控制字符串
%G 选择 UTF8 作为字符集
#8 DEC 屏幕校准测试,使用E填充整个终端

Control Sequence Introducer

控制序列导入器:ESC[ + 若干参数字节 + 若干中间字节 + 一个最终字节

  • 常见序列只是把参数用作一系列分号分隔的数字,如:1;2;3

    • 缺少的数字视为 0
    • 某些序列(CUU)把 0 视为 1,以使缺少参数情况下有意义
  • 一部分字符定义“私有”,方便终端制造商插入私有序列

    • 参数字节 <=>? 的使用:ESC[?25hESC[?251 打开、关闭光标显示
    • 最终字节 0x70 - 0x7F
组成部分 字符范围 ASCII字符
参数字节 0x30~0x3F 0-9:;<=>?
中间字节 0x20~0x2F 、!"#$%&'()*+,-./
最终字节 0x40~0x7E @A-Z[]^_a-z{}~, `

光标移动

序列内容 名称 效果
[n]A/[n]B/[n]C/[n]D CU[UDFB] - Cursor Up/Down/Forward/Back 光标移动[n]格,在屏幕边缘则无效
[n]E/[n]F Cursor Next Line/Previous Line 光标移动至下[n]行/上[n]行开头
[n]G Cursor Horizontal Absolute 光标移动到第[n]
[n;m]H CUP - Cursor Position 光标绝对位置
[n;m]f Horizontal Vertical Position CUP
[n]J Erase in Display 清除屏幕部分区域:0 - 光标至末尾;1 - 开头至光标;2 - 整个屏幕
[n]K Erase in Line 清除行内部分区域
[n]S Scroll Up 整页向上滚动 [n]
[n]T Scroll Down 整页向下滚动 [n]
s Save Cursor Position 保存光标当前位置
u Restore Cursor Position 恢复光标位置

窗口

序列内容 名称 效果
5i - 打开辅助端口,通常用于本地串行打印机
4i - 关闭辅助端口,通常用于本地串行打印机
6n Device Status Report ESC[n;m]R 报告光标位置

Select Graphic Rendition

  • SGR 选择图形再现:ESC[[n]m
    • [n]:多个参数用 ; 分隔,缺省为 0
    • m:结束字节
样式
设置值 显示效果 取消值
0 所有属性值重置为默认值,用于取消对后续输出影响
1 高亮或粗体 22
2 半亮 22
4 下划线 24
5 闪烁 25
7 反显,前景、背景色交换 27
8 隐藏,前景、背景色相同,可能不支持 28
9 删除线 29
53 上划线 55
11-19 选择替代字体
3/4位色
前景色值 背景色值 颜色 高亮前景色值 高亮背景色值
30 40 黑色 90 100
31 41 红色 91 101
32 42 绿色 92 102
33 43 黄色 93 103
34 44 蓝色 94 104
35 45 紫红色 95 105
36 46 青蓝色 96 106
37 47 白色 97 107
38 48 控制使用256位、RGB色
39 49 默认颜色

ansi_sgr_colors_16

  • 可通过反显 7 实现背景色、高亮 1 实现多高亮色
8bits 色
  • 8bits 色设置格式
    • ESC[38;5;37m:设置256位前景色
    • ESC[48;5;37m:设置256位背景色
  • 预定义 8bits 色情况
    • 0-7:标准颜色,同 ESC[30-37m
    • 8-15:高强度颜色,同 ESC[90-97m
    • 16-23116 + 36*r + 6*g + b($0 leq r,g,b leq 5$ 得到 6 6 6 立方)
    • 232-255:24阶灰阶

ansi_sgr_colors_256

24bits 色
  • 24bits 色设置格式

    • ESC[38;2;<r>;<g>;<b>m:选择 RGB 前景色
    • ESC[48;2;<r>;<g>;<b>m:选择 RGB 辈景色
  • 字符内容体系结构有一个不太受支持的替代版本

    • ESC[38:2:<Color-Space-ID>:<r>:<g>:<b>:<unused>:<CS tolerance>:<Color-Space: 0="CIELUV";1="CIELAB">m:选择 RGB 前景色
    • ESC[48:2:<Color-Space-ID>:<r>:<g>:<b>:<unused>:<CS tolerance>:<Color-Space: 0="CIELUV";1="CIELAB">m:选择 RGB 背景色
  • 支持 libvte 的终端上支持 ISO-8613-3 的 24bits 前景色、背景色设置,如 XtermKonsole
  • 24bits 色的替代版本是 ISO/IEC 8613-6 采用的 ITUT.416 信息技术

Quasi-Newton Method/Variable Metric Method

综述

拟Newton法/变度量法:不需要求解Hesse矩阵,使用一阶导构造 二阶信息的近似矩阵

  • 使用迭代过程中信息,创建近似矩阵$B^{(k)}$代替Hesse矩阵

  • 用以下方程组替代Newton方程,其解$d^{(k)}$作为搜索方向

思想

  • 考虑$\triangledown f(x)$在$x^{(k+1)}$处泰勒展开

  • 取$x = x^{(k)}$,有

    • $s^{(k)} = x^{(k+1)} - x^{(k)}$
    • $y^{(k)} = \triangledown f(x^{(k+1)}) - \triangledown f(x^{(k)})$
  • 要求$B^{(k)}$近似$\triangledown^2 f(x^{(k)})$,带入并将 $\approx$改为$=$,得到拟Newton方程

    并假设$B^{(k)}$对称

  • 拟Newton方程不能唯一确定$B^{(k+1)}$,需要附加条件,自然 的想法就是$B^{(k+1)}$可由$B^{(k)}$修正得到,即

    且修正项$\Delta B^{(k)}$具有“简单形式”

Hesse矩阵修正

对称秩1修正

认为简单指矩阵秩小:即认为$\Delta B^{(k)}$秩为最小值1

  • 设$\Delta B^{(k)} = u v^T$,带入有

    • 这里有的书会设$\Delta B^{(k)} = \alpha u v^T$, 其实对向量没有必要
    • $v^T s^{(k)}$是数,所以$u$必然与共线,同理也没有必要 考虑系数,直接取相等即可
    • 而且系数不会影响最终结果
  • 可取$u = y^{(k)} - B^{(k)} s{(k)}$,取$v$满足 $v^T s^{(k)} = 1$

  • 由$B^{(k)}$的对称性、并希望$B^{(k+1)}$保持对称,需要 $u, v$共线,则有

  • 得到$B^{(k)}$的对称秩1修正公式

算法

  1. 初始点$x^{(1)}$、初始矩阵$B^{(1)} = I$、精度要求 $\epsilon$、置$k=1$

  2. 若$|\triangledown f(x^{(k)})| \leq \epsilon$,停止计算 ,得到解$x^{(k)}$,否则求解以下方程得到$d^{(k)}$

  3. 一维搜索,求解

    得到$\alpha_k$,置$x^{(k+1)}=x^{(k)} + \alpha_k d^{(k)}$

  4. 修正$B^{(k)}$

  5. 置$k = k+1$,转2

特点

  • 缺点

    • 要求$(y^{(k)} - B^{(k)} s^{(k)})^T s^{(k)} \neq 0$, 否则无法继续计算

    • 不能保证正定性传递,只有 $(y^{(k)} - B^{(k)} s^{(k)})^T s^{(k)} > 0$才能保证 $B^{(k+1)}$也正定

    • 即使$(y^{(k)} - B^{(k)} s^{(k)})^T s^{(k)} > 0$, 也可能很小,容易产生较大的舍入误差

对称秩2修正

  • 为克服秩1修正公式缺点,考虑$\Delta B^{(k)}$秩为2,设

  • 带入拟Newton方程有

  • 类似的取

  • 同秩1公式保持对称性推导,得到对称秩2修正公式/BFGS公式

BFGS算法

类似同秩1修正算法,仅第4步使用对称秩2修正公式

Hesse逆修正

对称秩2修正

  • 考虑直接构造近似于$(\triangledown^2 f(x^{(k)}))^{-1}$的 矩阵$H^{(k)}$

  • 这样无需求解线性方程组,直接计算

  • 相应拟Newton方程为

  • 可得$H^{(k)}$的对称秩1修正公式

  • 可得$H^{(k)}$的对称秩2修正公式/DFP公式

DFP算法

类似BFGS算法,只是

  • 使用$H^{(k)}$计算更新方向
  • 使用$H^{(k)}$的对称秩2修正公式修正
  • 对正定二次函数,BFGS算法和DFP算法效果相同
  • 对一般可微(非正定二次函数),一般认为BFGS算法在收敛性质 、数值计算方面均由于DFP算法

Hesse逆的BFGS算法

  • 考虑

  • 两次利用Sherman-Morrison公式,可得

todo

  • 还可以进一步展开

变度量法的基本性质

算法的下降性

定理1

  • 设$B^{(k)}$($H^{(k)}$)是正定对称矩阵,且有 $(s^{(k)})^T y^{(k)} > 0$,则由BFGS(DFS)公式构造的 $B^{(k+1)}$($H^{(k+1)}$)是正定对称的
  • 考虑$B^{(k)}$对称正定,有 $B^{(k)} = (B^{(k)})^{1/2} (B^{(k)})^{1/2}$

  • 带入利用柯西不等式即可证

  • 中间插入正定矩阵的向量内积不等式也称为广义柯西不等式

定理2

  • 若$d^{(k)}$v是下降方向,且一维搜索是精确的,设 $B^{(k)}$($H^{(k)}$)是正定对称矩阵,则有BFGS(DFP) 公式构造的$B^{(k+1)}$($H^{(k+1)}$)是正定对称的
  • 精确一维搜索$(d^{(k)})^T \triangledown f(x^{(k+1)}) = 0$
  • 则有$(s^{(k)})^T y^{(k)} > 0$

定理3

  • 若用BFGS算法(DFP算法)求解无约束问题,设初始矩阵 $B^{(1)}$($H^{(1)}$)是正定对称矩阵,且一维搜索是精确的 ,若$\triangledown f(x^{(k)}) \neq 0$,则产生搜索方向 $d^{(k)}$是下降方向
  • 结合上2个结论,数学归纳法即可

总结

  • 若每步迭代一维搜索精确,或满足$(s^{(k)})^T y^{(k)} > 0$

    • 停止在某一稳定点
    • 或产生严格递减的序列${f(x^{(k)})}$
  • 若目标函数满足一定条件我,可以证明变度量法产生的点列 ${x^{(k)}}$收敛到极小点,且收敛速率超线性

搜索方向共轭性

  • 用变度量法BFGS(DFP)算法求解正定二次函数
$$
min f(x) = \frac 1 2 x^T G x + r^T x + \sigma
$$

若一维搜索是精确的,假设已经进行了m次迭代,则
  • 搜索方向$d^{(1)}, \cdots, d^{(m)}$是m个非零的G共轭方向

  • 对于$j = 1, 2, \cdots, m$,有

$$
B^{(m+1)} s^{(j)} = y^{(j)}
(H^{(m+1)} y^{(j)} = s^{(j)})
$$

且$m = n$时有吧

$$
B^{(n+1)} = G(H^{(n+1)} = G^{-1})
$$

变度量法二次终止

  • 若一维搜索是精确的,则变度量法(BFGS、DFP)具有二次终止
  • 若$\triangle f(x^{(k)}) = 0, k \leq n$,则得到最优解 $x^{(k)}$

  • 否则得到的搜索方向是共轭的,由扩展空间子定理, $x^{(n+1)}$是最优解

Python 函数式编程

functools

total_ordering

total_ordering:允许类只定义__eq__和其他中的一个,其他 富比较方法由装饰器自动填充

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from functools import total_ordering

class Room:
def __init__(self, name, length, width):
self.name = name
self.length = length
self.width = width
self.square_feet = self.length * self.width

@total_ordering
class House:
def __init__(self, name, style):
self.name = name
self.style = style
self.rooms = list()

@property
def living_space_footage(self):
return sum(r.square_feet for r in self.rooms)

def add_room(self, room):
self.rooms.append(room)

def __str__(str):
return "{}: {} squre foot {}".format(
self.name,
self.living_space_footage,
self.style)

def __eq__(self, other):
return self.living_space_footage == other.living_space_footage

def __lt__(self, other):
return self.living_space_footage < other.living_space_footage

Py3std Readme

常用参数说明

  • 函数书写声明同Python全局说明
  • 以下常用参数如不特殊注明,按照此解释

Common

Stream

  • mode="r"/"w"/"a"/"+"/"t"/"b"

    • 含义:文件/管道打开模式
      • t:文本,可省略
      • b:二进制
      • r:读,默认
      • w:写
      • a:追加,大部分支持
      • +:更新模式,同时允许读写
        • r+:文件已存在,读、写
        • w+:清除之前内容,读、写
        • a+:读、追加写
    • 默认:rt/r
  • buffering/bufsize = -1/0/1/int

    • 含义:缓冲模式
      • 0:不缓冲,只在二进制模式中被运行
      • 1:逐行缓冲,只在文本模式中有效
      • 其他正整数:指定固定大小chunk缓冲的大小
      • -1:全缓冲
        • 普通二进制、文本,缓冲chunks大小启发式确定, io.DEFAULT_BUFFER_SIZE查询
        • 终端交互流(.isatty()),逐行缓冲
    • 默认:-1
  • encoding(str)

    • 含义:文件编码
      • utf-8
      • utf-16
      • utf-16-le
      • utf-16-be
      • utf-32
      • gbxxxx
      • 待续
    • 缺省:使用locale.getpreferedencoding()返回值

Threading/Processing

  • block/blocking = True/False

    • 含义:是否阻塞
    • 默认:大部分为True(阻塞)
    • 其他
      • 对返回值不是bool类型的函数,非阻塞时若无法进行 操作,往往会raise Exception
  • timeout = None/num

    • 含义:延迟时间,单位一般是秒
    • 默认:None,无限时间
    • 其他
      • block=False时,一般timeout参数设置无效
  • fn/func/callable(callable)

    • 含义:可调用对象
    • 默认:一般默认值
    • 其他
      • 实参可以是任何可调用对象
        • 函数
        • 方法
        • 可调用对象
  • args = ()/None/tuple/list/*args(arg_1, ...)

    • 含义:函数位置参数
    • 默认:()/None,无参数
  • kwrags/kwds = {}/None/dict/**kwargs(kwarg_1=v1, ...)

    • 含义:函数关键字参数
    • 默认:{}/None,无参数
  • callback=callable

    • 含义:回调函数
      • 异步线程、进程调用才会有该参数
      • 回调函数接收进程/线程返回值作为参数
      • 回调函数最好有返回值,否则会阻塞进程、线程池
    • 默认:None,无参数
  • chunksize=None/1/int

    • 含义:一次传递给子进程的迭代器元素数量
      • 常在进程池迭代调度函数中,较大的chunksize 能减少进程间通信消耗,但会降低灵活性
      • 线程调度相关函数该参数被忽略
    • 默认:None/1,一次传递一个元素
  • daemon=False/None/True

    • 含义:是否为守护进程/线程
      • 默认情况下,主进程(线程)会等待子进程、线程退出 后退出
      • 主进程(线程)不等待守护进程、线程退出后再退出
      • 注意:主进程退出之前,守护进程、线程会自动终止

Python命令行参数

  • -c:解释执行语句
  • -u:强制输入、输出流无缓冲,直接输入,默认全缓冲

排序

总述

  • 按照升序重新排列给定列表中的数据项
  • 为了让问题有意义,列表中的数据项应该能够排序(数据之间 有一种全序关系)
  • 键:在对记录排序时,需要选取的、作为排序的依据的一段信息

目的

  • 排序可能是所求解的问题输出要求
  • 排序能够更方便的求解和列表相关的问题
    • 查找问题
  • 在其他领域的重要算法中,排序也被作为辅助步骤

发展

  • 排序领域已经有很多不错的算法,只需要做$nlog_{x}^{n}$次 比较就能完成长度为$n$的任意数组排序,且没有一种基于 值比较(相较于比较键值部分内容而言)的排序算法能 在本质上操作其,

  • 但是还是需要不断探寻新的算法虽然有些算法比其他的要好, 但是没有任何算法在任何情况下是最优的

    • 有些算法比较简单,但速度较慢
    • 有些算法适合随机排列的输入,而有些适合基本有序的列表
    • 有些算法适合驻留在快速存储器中的列表,而有些适合存储 在磁盘上的大型文件排序

评价

  • 稳定性:排序算法保留等值元素在输入中的相对顺序
    • 一般来说,将相隔很远的键交换位置的算法虽然不稳定, 但往往很快
  • 在位性:排序算法不需要额外的存储空间,除极个别存储单元外

线性表排序

选择排序

  • 每轮选择出剩余元素中最小值,放在对于位置上

顺序表算法

1
2
3
4
5
6
7
8
9
10
SelectionSort(A[0..n-1]):
// 选择排序排序数组
// 输入:可排序数组A[0..n-1]
// 输出:升序排序数组A[0..n-1]
for i = 0 to n-1 do
min = i
for j = i to n-1 do
if A[j] < A[min]
min = j
swap A[i] and A[min]
  • 扫描整个列表找到最小元素,然后和首个元素交换,将其放在 最终位置上
  • 从第2个元素开始寻找之后最小元素,和第2个元素交换,将其 放在最终位置上
  • 重复n-1次,列表有序

链表算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
SelectionSort(linked):
// 选择排序排序链表
// 输入:可排序链表linked头
// 输出:排序后链表头
if linked == NULL:
return NULL
linked_head = ListNode()
// 为方便附设头结点
linked_head.next = linked
unsorted_head = linked_head
// 未排序头结点
// 后续过程中是链表中元素

while unsorted_head.next != NULL:
cur_node = unsorted_head
min_node = unsorted_head
// 全是链表中元素
while cur_node.next != NULL:
if cur_node.next.val < min_node.next.val:
min_node = cur_node
cur_node = cur_node.next

// 若`min_node.next`就是`unsorted_start.next`,以下
// 代码中的断开、重组链表操作会出现环
// 完全切开再重组链表,则需要判断`unsorted_start`
// 是否为空
if unsorted_start.next != min_node.next:
_tmp_node = unsorted_head.next
// 记录原`unsorted_head.next`

unsorted_head.next = min_node.next
// `unsorted_head.next`断开、连接`min_node`

min_node.next = min_node.next.next
// `min_node.next`断开、跳过`min_node`重连
// 若`min_node.next`是`unsorted_start.next`
// 会断开`unsorted_start`和`min_node`

unsorted_head.next.next = _tmp_node
// 原`min_node`重连原`unsorted_head.next`

unsorted_start = unsorted_start.next

return linked_head.next

特点

  • 对任何输入,选择排序键值比较都是$\Theta(n^2)$
  • 键交换次数仅为$\Theta(n)$
    • 选择排序此特性优于许多其他排序算法

冒泡排序

  • 比较表中相邻元素,如果逆序就交换位置
  • 重复多次则最大元素被“沉到”列表最后位置
  • 第2轮比较将第2大元素“沉到”其最终位置
  • 重复比较n-1轮,列表有序

顺序表算法

1
2
3
4
5
6
7
8
BubbleSort(A[0..n-1]):
// 冒泡排序排序数组
// 输入:可排序数组A[0..n-1]
// 输出:升序排序数组A[0..n-1]
for i = 0 to n-2 do
for j = 0 to n-2-i do
if A[j+i] < A[j]
swap A[j] and A[j+1]

链表算法

1
2
3
4
BublleSort(head):
// 冒泡排序排序链表
// 输入:可排序链表首个元素
// 输出:排序后列表首个元素

特点

  • 对任何输入,冒泡排序键值比较都是$\Theta(n^2)$
  • 其交换次数取决于特定输入
    • 最坏情况是遇到降序排列数组,此时键交换次数同比较次数
  • 冒泡排序看起来就像是选择排序的一直交换+最大优先版本

改进

  • 如果对列表比较一轮后没有交换任何元素,说明列表有序,可以 结束算法

Insertion Sort

插入排序:利用减一技术对数组$A[0..n-1]$进行排序

算法

  • 假设对较小数组$A[0..i-2]$排序已经解决
  • 从右至左(方便将将元素右移)扫描有序子数组,直到遇到首个 小于等于$A[i-1]$元素,将$A[i-1]$插入其后
1
2
3
4
5
6
7
8
9
10
11
12
InsertionSort(A[0..n-1])
// 用插入排序对给定数组排序
// 输入:n个可排序元素构成数组
// 输出:非降序排序数组
for i = 1 to n-1 do
v = A[i]
j = i - 1
while j >= 0 and A[j] > v do
A[j+1] = A[j]
j = j - 1
A[j+1] = v
// 上一步比较元素已经赋值给其后,所以应该覆盖其值

特点

  • 插入排序是自顶向下基于递归思想,但是自底向上使用迭代实现 算法效率更高

  • 算法时间效率

    • 算法基本操作是键值比较,比较次数依赖于特定输入
    • 最坏情况(递减数组)下:每轮比较次数都到达最大,此时 键值比较次数$\in \Theta(n^2)$
    • 最优情况(递增数组)下:每轮比较次数仅为1,此时键值 比较次数$\in \Theta(n)$
    • 对随机序列:比较次数$\in \Theta(n^2)$
    • 许多应用中都会遇到基本有序的文件,插入排序能保证良好 性能
  • 减常量法

Shell Sort

插入排序的扩展,Shell排序基于h有序

H有序

数组中任意间隔为h的元素都是有序的,对应的数组称为h有序数组

  • h有序数组:就是h个互相独立的有序数组编织在一起数组
  • h有序数组具有分离、局部有序的特点

算法

  • 使用插入排序对h子数组独立排序,每次交换相隔h的元素
  • h逐渐减小到1,shell排序退化(最后一轮)为插入排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ShellSort(A[0..n-1])
// 用插入排序对给定数组排序
// 输入:n个可排序元素构成数组
// 输出:非降序排序数组
h = 1
while h < n/3
h = h*3 + 1
// 获取初始h值,之后每轮h变为1/3
while h >= 1:
for i = h to n-1:
v = A[i]
j = i - h
while j >= 0 and A[j] > v do
A[j] = A[j-h]
j = j - h
A[j+h] = v
h = h/3

特点

  • Shell排序全衡量子数组规模、有序性,更加高效

  • h递增序列

    • 子数组部分有序程度取决于h递增序列的选择
    • 不同的h递增序列对算法效率有常数倍的变动,但是在实际 应用中效果不明显
  • Shell排序是唯一无法准确描述其对于乱序数组性能特征的排序 方法

  • 减常量法

归并排序

Mergesort

  • 将需要排序的数组A[0..n-1]均分的
  • 对两个子数组递归排序,然后把排序后的子数组合并为有序 数组
1
2
3
4
5
6
7
8
9
10
Mergesort(A[0..n-1]):
// 递归调用MergeSort对数组排序
// 输入:可排序数组A[0..n-1]
// 输出:非降序排列数组A[0..n-1]
if n > 1:
copy A[0..floor(n/2)-1] to B[0..floor(n/2)-1]
copy A[floor(n/2)..n-1] to C[0..ceiling(n/2)-1]
Mergesort(B[0..floor(n/2)-1])
Mergesort(C[0..ceiling(n/2)-11])
Merge(B, C, A)

Merge

  • 初始两只指针指向待合并数组首个元素
  • 比较两个元素大小,将较小元素添加到新数组中,被复制元素 的数组指针右移
  • 重复直到某一数组处理完毕,将未处理完数组复制到新数组尾部
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Merge(B[0..p-1], C[0..q-1], A[0..p+q-1]):
// 将两个有序数组合并为新的有序数组
// 输入:有序数组B[0..p-1]、C[0..q-1]
// 输出:存放有B、C中元素的有序数组A[0..p+q-1]
while i < p and j < q:
if B[i] <= C[j]
A[k] = B[i]
i = i + 1
else:
A[k] = C[j]
j = j + 1
k = k + 1
if i == p:
copy C[j..q-1] to A[k..p+q-1]
else:
copy B[i..p-1] to A[k..p+q-1]

特点

  • 算法时间效率

    • 最坏情况下比较次数$\in \Theta(nlogn)$,为 $nlog_2n-n+1$,十分接近基于比较的排序算法理论上能够 达到的最少次数
  • 相较于快排、堆排序

    • 合并排序比较稳定,缺点在于需要线性额外空间
    • 虽然能做到在位,但是算法过于复杂,只具有理论上意义
  • 算法可以自底向上合并数组的元素对,再合并有序对

    • 这可以避免使用堆栈递归调用时时空开销
  • 可以把数组划分为待排序的多个部分,再对其递归排序,最后 合并在一起

    • 这尤其适合对存在二级存储空间的文件进行排序
    • 也称为Multiway Mergesort
  • 分治算法

快速排序

相较于合并排序按照元素在数组中的位置进行划分,快速排序按照 元素值对其进行划分

Quicksort

  • 对数组中元素重新排列,使得A[s]左边元素均小于A[s],A[s] 右边元素都大于等于A[s]
    • 此时A[s]已经位于它在有序数组中的最终位置
  • 接下来对A[s]左右子数组进行排序
1
2
3
4
5
6
7
8
Quicksort(A[l..]r)
// 对给定可比较数组进行排序
// 输入:可比较数组A[0..n-1]子数组A[l..r]
// 输出:非降序排序的子数组A[l..r]
if l<r
s = partition(A[l..r])
Quicksort(A[l..s-1])
Quicksort(A[s+1..r])

特点

  • 需要选择合适的划分算法J

    • LomutoPartition
    • HoarePartition
  • 算法效率

    • 最优情况:分裂点都位于数组中点,算法比较次数为 $nlog_2n$
    • 最差情况:分裂点都趋于极端(逆序输入),算法比较 次数$\in \Theta(n^2)$
    • 平均:比较次数为$2nlnn \approx 1.39nlog_2n$
    • 且算法内层循环效率非常高,在处理随机排列数组时,速度 比合并排序快
  • 快速排序缺点

    • 不稳定
    • 需要堆栈存储未被排序的子数组参数,尽管可以先对较短 子数组排序使得堆栈大小降低到$O(logn)$,但是还是比 堆排序的$O(1)$差
    • 仍然存在最差情况出现的可能性,即使有很多巧妙地中轴 选择办法
    • 对随机数组排序性能的好坏,不仅与算法具体实现有关, 还和计算机系统结构、数据类型有关
  • 分治算法

算法改良

  • 更好的中轴选择方法

    • randomized quicksort:随机快速排序,使用随机元素作为 中轴
    • median-of-three method:三平均划分法,以数组最左边、 最右边、最中间的中位数作为中轴
  • 子数组足够小时(5~15),改用插入排序;或者直接不对小数组 排序,而是在快速排序结束后使用插入排序对整个近似有序的 数组进行排序

  • 改进划分方法

    • 三路划分:将数组划分为3段,小于、等于、大于中轴元素

比较计数排序

算法

  • 遍历待排序列表中每个元素
  • 计算、记录列表中小于该元素的元素个数
  • 更新大于其的元素的小于元素的元素个数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ComparisonCountingSort(A[0..n-1])
// 用比较计数法排序
// 输入:可排序数组A[0..n-1]
// 输出:将A中可排序数组按照升序排列数组
for i = 0 to n-1 do
count[i] = 0
for i = 0 to n-2 do
for j = i+1 to n-1 do
if A[i] < A[j]
count[j] += 1
else
count[i] += 1
for i = 0 to n-1 do
S[count[i]] = A[i]

特点

  • 算法效率

    • 算法比较次数为$n(n-1)/2$
    • 占用了线性数量的额外空间
  • 算法使得键值的可能移动次数最小化,能够直接将键值放在在 有序数组最终位置

  • 输入增强

分布计数排序

  • 待排序元素来自于某个已知小集合$[l..u]$

算法

  • 扫描列表,计算、存储列表中元素出现频率于数组$F[l..u]$中
  • 再次扫描列表,根据值填入相应位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
DistributionCountingSort(A[0..n-1], l, u):
// 分布计数法排序,对元素来自有限范围整数的数组排序
// 输入:数组[0..n-1],数组中整数位于l、u间
// 输出:A中元素构成非降序数组S[0..n-1]
for j = 0 to u-l do
D[j] = 0
for i = 0 to n=1 do
D[A[i]-l] += 1
for j = 1 to u-l do
D[j] += D[j-1]
// 存储各元素最后出现索引+1
for i = n-1 downto 0 do
j = A[i] - l
S[D[j]-1] = A[i]
D[j] -= 1
// 更新应该存储的位置,类似于压栈
return S

特点

  • 算法效率

    • 如果元素值范围固定,效率为线性(不是基于比较的排序, $nlogn$没有限制)
    • 用空间换时间,其实可以看作是hash
    • 利用了输入列表独特自然属性
  • 变治法(输入增强)

应用

  • 判断线性表元素是否唯一
  • 寻找线性表众数
  • 快速查找

线性表顺序统计量

寻找列表中第k小的元素

  • 也即:求出给定列表中k个最小元素问题

  • 采用partitioning的思路,需要将给定列表根据某个值先行划分

    • Lumuto划分算法

QuickSelect算法

算法

  • 对划分完后出数组,s为分割位置
    • 若:s == k-1,则中轴p就是第k小元素
    • 若:s < k-1,则应该是数组右边划分第k-s小元素
    • 若:s > k-1,则应该是数组左边划分第k小元素
  • 这样就得到规模更小的问题实例
1
2
3
4
5
6
7
8
9
10
11
QuickSelect(A[l..r], k)
// 用基于划分递归算法解决选择问题
// 输入:可排序数组A[0..n-1]的子数组A[l..r]、整数k
// 输出:A[l..r]中第k小元素
s = Partition(A[l..r])
if s = l+k-1
return A[s]
elif s > l+k-1
QuickSelect(A[l..s-1], k)
else
QuickSelect(A[s+1..r], l+k-1-s)

特点

  • 时间效率:和划分算法有关(对Lomuto算法)

    • 最好情况下只需要划分一次即找到,需要比较$n-1$次
    • 最坏情况下需要比较$n(n-1)/2$次,这比直接基于排序更差
    • 数学分析表明,基于划分的算法平均情况下效率是线性的
  • 已经找到复杂算法替代Lomuto算法用于在快速选择中选出 中轴,在最坏情况下仍保持线性时间效率

  • QuickSelect算法可以不用递归实现,且非递归版本中甚至 不需要调整参数k值,只需要最后s == k-1即可

  • 减可变规模:Lomuto算法性质

线性表有序划分

LomutoPartition

算法

  • 考虑子数组A[l..r]分为三段,按顺序排在中轴p之后

    • 小于p的元素,最后元素索引记为s
    • 大于等于p的元素,最后元素索引记为i
    • 未同p比较过元素
  • 从左至右扫描A[l..r],比较其中元素和p大小

    • A[i] >= p,扩大大于等于p元素段
    • A[i] < p,需要扩大小于等于p元素段
1
2
3
4
5
6
7
8
9
10
11
12
LomutoPartition(A[l..r])
// 采用Lomuto算法,用首个元素作中轴划分数组
// 输入:可排序数组A[0..n-1]的子数组A[l..r]
// 输出:A[l..r]的划分、中轴位置
p = A[l]
s = l
for i = l+1 to r
if A[i] < p
s = s+1
swap(A[s], A[i])
swap(A[l], A[s])
return s

HoarePartition

算法

  • 选择一个元素p作为中轴(最简单的,首个元素p=A[l])

  • 从数组A[l..r]两端进行扫描,将扫描到的元素同中轴比较

    • 从左至右的扫描忽略小于中轴元素,直到遇到大于等于中轴 元素停止(从第二个元素开始i=l+1)
    • 从右至左的扫描忽略大于中轴元素,直到遇到小于等于中轴 元素停止(j=r-1)
  • 若扫描停止后

    • 两指针不相交i < j,交换A[i]、A[j],i=i+1、j=j-1, 继续扫描
    • 若指针相交i > j,把中轴和A[j]交换即得到数组一个划分 ,分裂点为s=j
    • 如果指针重合i==j,此元素一定等于p,也得到数组的一个 划分,分裂点为s==i==j
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
HoarePartition(A[l..r])
// 以首元素为中轴,对数组进行划分
// 输入:可排序数组A[1..n]的子数组A[l..r]
// 输出:A[l..r]的一个划分,返回分裂点位置
p = A[l]
i = l
j = r+1
repeat
repeat i = i+1 until A[i] >= p
// 这里i有可能越界,可以添加一个限位器
repeat j = j-1 until j[i] <= p
// 从左右两端都是遇到等于p的元素就停止扫描
// 这样可以保证即使数组中有许多和p相等的元素
// 也能够将数组划分得比较均匀
swap(A[i], A[j])
// 这样写没有关系,不需要在这里给调整i、j
// 因为循环下一步就是调整i、j
until i >= j
swap(A[i], A[j])
// 撤销算法最后一次交换
swap(A[l], A[j])
return j

三路划分

算法

Linear List

线性表综述

线性表:n个数据元素的有限序列

  • 元素个数n定义为线性表长度,n=0时称为空表
  • 非空表中每个元素都有确定的位置

顺序存储结构

顺序存储结构/映像:使用一组地址连续的存储单元依次存储数据 元素

  • 具体参见algorithm/data_structure_intro

顺序表

顺序表:顺序存储结构的线性表

1
2
3
4
5
typedef struct{
ElemType *elem;
int length;
int listsize;
}SqList;
  • 以元素在计算机内物理位置相邻表示线性表中数据元素之间 的逻辑关系

  • 每个数据元素存储位置和线性表起始位置,相差和其在线性表中 位序成正比常数,所以顺序表时一种随机存取存储结构

    • 高级程序语言中数组类型也有随机存取特点,因此通常 用数组描述

时间效率

  • 插入/删除:$O(n)$
    • 主要时间耗费在移动元素上
  • 求表长:$O(1)$
  • 取第n个元素:$O(1)$

链式存储结构

链式/非顺序存储结构/映像:用一组任意存储单元存储线性表 中数据元素,还需存储一个指示其直接后继的信息

  • 具体参见algorithm/data_structure_intro

(线性/单)链表

线性链表:n个节点链接而成

1
2
3
4
typedef struct LNode{
ElemType data;
struct LNode * next;
}LNode, *LinkList;
  • 链表中每个节点只包含一个指针域
    • 数据元素之间的逻辑关系由节点中指针指示,即指针为数据 元素之间的逻辑关系映像
  • 整个链表存取必须从头指针开始
  • 单链表中任何两个元素存储位置之间没有固定联系,是非随机 存取存储结构
  • 头指针:指示链表中第一个节点的存储位置
  • 头节点:在单链表第一个节点之前附设的节点,其数据域可以 不存储信息,也可以存储线性表长度、尾指针等附加信息

时间效率

  • 插入、删除:$O(n)$
    • 已知插入、删除元素确切位置的情况下,仅需修改指针, 而不需要移动元素
  • 取第n个元素:$O(n)$
    • 访问时间依赖元素在列表中位置

说明

  • 在很多场合下,链表是线性表的首选结构

    • 链表不需要事先分配任何存储空间,空间利用合理
    • 插入、删除效率高,只需要重连相关指针
  • 但是存在一些实现问题

    • 求线性表长不如顺序存储结构
  • 链表中结点关系使用指针表示,数据元素在线性表中“位序” 概念淡化,被“位置”代替

因此重新定义带头结点的线性链表

1
2
3
4
5
6
7
8
typedef struct LNode{
ElemType data;
struct LNode * next;
}*Link, * Position;
typedef struct {
Link head, tail;
int len;
}

单链表注意事项

  • 头结点/头指针:处理链表非常方便的技巧

    • 头指针指向链表首个结点:便于调整首个节点位置时,仍然 记住链表
    • 头指针不包含链表信息,本质不属于链表:有些情况下方便 统一代码,不需要特殊考虑链表首个节点
  • 对链表进行可能改变链表的遍历操作:一般使用两个标记 结点/指针

    • 头结点/指针lstart:记住链表
    • 遍历标记指针cur:标记处理结点
  • 交换节点:标记指针需要指向当前处理节点的前一个结点

    • 单链表中只有指向下个节点的指针,若标记指针指向当前 节点,则无法方便将链表同当前节点断开、重连
    • 注意待交换两个节点为同一节点的情况:不同于值交换 ,这种情况可能导致链表错误连接成环
  • 无指针、纯引用对象语言(python)中:只能使用节点对象 遍历链表

    • 变量为链表中节点引用:使用类似普通指针,需要注意 别修改引用节点数据
    • 变量为额外节点引用:内存类似普通指针,使用时注意 解析引用次数

静态链表

静态链表:使用数组描述的链表

1
2
3
4
typedef struct{
ElemType data;
int cur;
}component, SLinkList[MAXSIZE];
  • 数组分量表示节点
  • 使用游标cur作为指针域指示节点在链表中的逻辑位置
  • 第0个分量表示头节点,其指针域cur指向链表第一个节点
  • 方便在无指针高级语言中使用链式结构
  • 为确定未使用的数组分量,可以将未被使用的、删除的分量用 游标链成备用边表

Circular Linked List

循环链表:表中最后一个节点指针域指向头节点,整个链表形成环

  • 循环链表和线性链表操作基本一致
    • 仅循环条件不再是指针域为空,而是是否等于头指针

Double Linked List

双向链表:链表中有两个指针域,分别指向直接后继、直接前驱

1
2
3
4
5
typedef struct DuLNode{
ElemType data;
Struct DulNode * prior;
Struct DulNode * next;
}DuLNode, * DuLinkList;
  • 双向链表克服线性链表寻找直接前驱时间$O(n)$的缺点
  • 双向链表大部分操作和线性链表相同,指示有些操作需要同时 修改两个指针
  • 字符串:数组实现的一种数据结构
    • 字符串常见操作不同于其他数组
      • 计算字符串长度
      • 按照字典序确定字符串排序时位置
      • 连接字符串构

图算法

总述

  • 图的遍历算法:如何一次访问到网络中所有节点
  • 最短路线算法:两个城市间最佳路线
  • 有向图拓扑排序:课程、预备课程是否有矛盾
  • All-Pairs Shortest-Paths Problem:完全最短路径问题,找到 每个顶点到其他所有顶点的距离

遍历算法

深度优先查找(DFS)

算法

  • 任意顶点开始访问图顶点,然后标记为已访问

  • 每次迭代时,紧接着处理与当前顶点邻接的未访问顶点, 直到遇到终点,该顶点所有邻接顶点均已访问过

  • 在终点上,算法沿着来路后退一条边,继续从那里访问未 访问顶点

  • 后退到起始点,且起始点也是终点时,算法停止,这样 起始点所在的连通分量的所有顶点均已访问过

  • 若存在未访问顶点,则必须从其中任一顶点开始重复上述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
count = 0
// 全局变量:访问次序(次数)
DFS(G)
// 对给定图的深度优先查找遍历
// 输入:图G=<V, E>
// 输出:图G顶点按照DFS遍历第一次访问到的先后次序,
// 未访问到标记未0
for each vertex v in V do
if v is marked with 0
dfs(v)
dfs(v)
// 递归访问所有和v相连接的未访问顶点,赋予count值
count = count+1
mark v with count
for each vertex w in V adjecnt to v do
if w is marked with 0
dfs(w)

特点

  • 算法效率非常高效,消耗时间和表示图的数据结构规模成正比

    • 邻接矩阵:遍历时间效率$\in \Theta(|V|^2)$
    • 邻接链表:遍历时间效率$\in \Theta(|V|+|E|)$
  • 可以方便地用栈跟踪深度优先查找

    • 首次访问顶点,将顶点入栈
    • 当顶点成为终点时,将其出栈
    • 运行时就是实际上就是栈,所以深度优先可以直接利用递归 实现
  • Depth-First Search Foreat:参见 algorithm/data_structure/graph

  • DFS产生两种节点排列顺序性质不同,有不同应用

    • 入栈(首次访问顶点)次序
    • 出栈(顶点成为终点)次序

应用

  • 检查图连通性:算法第一次停止后,是否所有顶点已经访问
  • 检查图无环性:DFS是否包含回边
  • 拓扑排序:见键值法
    • DFS节点出栈逆序就是拓扑排序的一个解(图中无回边, 即为有向无环图)
    • DAG中顶点v出栈前,不存在顶点u拥有到v的边,否则存在 回边,图不是DAG

广度优先查找(BFS)

算法

  • 首先访问所有和初始顶点邻接的顶点

  • 然后是离它两条边的所有未访问顶点

  • 以此类推,直到所有与初始顶点在同一连通分类顶点均已访问

  • 若存在未访问顶点,从图其他连通分量任意顶点开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
count = 0
// 全局变量:访问次序(次数)
BFS(G)
// 给定图广度优先查找变量
// 输入:图G=<V, E>
// 输出:图G的顶点按照被BFS遍历第一次访问到次序,
// 未访问顶点标记未0
for each vertax v in V do
if v is marked with 0
bfs(v)
bfs(v)
// 访问所有和v相连接的顶点,赋count值
count = count+1
whilte queue is not empty do
for each vertex w in V adjcent to the front vertex do
if w is marked with 0
count = count+1
mark w with count
add w to the queue
remove the front vertex from the queue

特点

  • 算法效率同DFS

    • 邻接矩阵:遍历时间效率$\in \Theta(|V|^2)$
    • 邻接链表:遍历时间效率$\in \Theta(|V|+|E|)$
  • 使用队列可以方便地跟踪广度优先查找操作

    • 从遍历初始顶点开始,标记、入队
    • 每次迭代时,算法查找所有和队头顶点邻接未访问,标记 、入队、将队头顶点出队
  • Breadth-First Search Forest:参见 algorithm/data_struture/graph

  • BFS只产生顶点的一种排序,因为队列时FIFO结构,顶点入队、 出队次序相同

应用

  • 和DFS一样可以检查图的连通性、无环性,但是无法用于比较 复杂的应用

  • 求给定两个顶点间最短路径:从一顶点开始BFS遍历,访问到 另一节点结束(难以证明?)

有向图强连通分量

Kosaraju算法

考虑有向图中强连通分量之间不连通的情况

  • 强连通分量之间没有边

    • 在任意连通分量中任意结点开始深度优先遍历

    • 访问完所有结点需要DFS次数就是强连通分量数量,每轮 DFS访问的点就是强连通分量中的顶点

  • 强连通分量之间只有单向边

    • 将强连通分量视为单个结点,则整个图可以视为一个 靠连通分量间单向边连接的有向无环图

    • 从最底层强连通分量中任选结点开始进行DFS,则此轮DFS 只能访问当前连通分量中结点

    • 逆序依次在各强连通分量中选择结点进行DFS,则每轮DFS只 访问当前连通分量中结点(其下层连通分量已访问)

    • 直至所有结点访问完毕,则得到所有强连通分量,即每轮 进行DFS访问的结点

    • 以下图为例,从图中连通分量B中任意结点开始进行DFS, 则经过两轮DFS即能找所有强连通分量

      strongly_connected_two_components

由以上分析

  • 只需要保证底层强连通分量进行DFS优先搜索

  • 也即在结点搜索优先级中,底层强连通分量中至少有一个结点 在其上层连通分量所有结点之前

  • 可以利用原图的反向的DFS逆后序排列得到满足条件 的结点优先级序列

    strongly_connected_two_components_reversed

    • 若从反向图中最底层强连通分量某结点开始,则只能遍历 自身,反向图中其余连通分量位于其所有结点之前

    • 若从反向图中非最底层强连通分量某结点开始,则能依次 遍历其底层所有强连通分量中结点,且至少该结点位于其余 连通分量所有结点之前

  • 逆后序排列参见algorithm/data_structure/graph

算法

  • 对原图G每条路径求反,得到反向图$G^R$
  • 对反向图$G^R$求解逆后序序列
  • 按照逆序序列优先级,对原图G进行DFS,每棵DFS生成树就是 一个强连通分量

特点

  • 算法效率
    • 时间效率$\in O(|V| + |E|)$
    • 算法需要对图进行两次DFS,速度较Tanjar算法更慢

Tarjan算法

Tarjan算法:基于图深度优先搜索的算法

  • 为每个结点维护两个标记,通过此标记确定是否存在回路

    • DFN:深度优先搜索中搜索到次序
    • Low:通过回边能访问到的前驱被搜索到的次序
    • 还可以维护一个Flag,判断结点是否仍在DFS栈中
  • 对图进行深度优先搜索

    • 未处理结点入栈,设置其DFNLow被搜索到的次序

    • 对已处理结点,考虑到深度优先的搜索、退栈方式

      • 仍然在栈中,则肯定是栈顶元素前驱,连接边为回边, 存在栈顶节点到该前驱结点的回路

      • 不在栈中,该节点不是祖先结点,连接边为交叉边, 该结点已经在其他连通分量中出栈

    • 使用栈中前驱结点Low/DFN次序更新当前(栈顶)结点, 并递归更新,即使用子节点访问先驱次序更新父节点

  • DFS回溯、退栈,考虑栈中每个结点DFNLow

    • DFN[u] > Low[u]:结点u和其前驱之间有回路, 即其属于同一个强连通分量

    • DFN[u] == Low[u]:结点u和其前驱之间没有通路, 没有更多结点属于其所属强连通分量,以结点u为根子树 是一个强连通分量

      • 则从栈顶元素开始退栈直至结点u退栈,退栈的所有 元素构成强连通分量
  • 每个强连通分量为深度优先搜索树中一个子树

  • $w, (w, v) \in E$:顶点v的直接前驱
  • $k, (v, k) \in E$:顶点v的祖先(即栈中结点)

算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
S = initStack()
DFN[MAX_VERTAX], Low[MAX_VERTEX]
index = 0
QList = InitList(Queue())

tarjan(u, E):
// 比较DFS搜索次序、回边到达次序判断强连通分量
// 输入:结点u,边集合E
// 输出:强连通分量队列列表
DFN[u] = Low[u] = ++ index
S.push(u)
for each (u, v) in E:
if (v is not visited):
tarjan(v)
Low[u] = min(Low[u], Low[v])
// 使用v找到的前驱更新u能找到前驱,递归更新
else if (v in S):
// 判断边是否为回边
Low[u] = min(Low[u], DFN[v])
// Low[u] = min(Low[u], Low[v])
// 应该也行
if(DFN[u] == Low[u]):
Q = QList.next()
repeat
v = S.pop()
Q.push(v)
until (u == v)

特点

  • 算法效率
    • 时间效率$\in O(|V|+|E|)$

关节点

类Tarjan算法

  • 类似Tarjan算法为每个节点维护DFNLow两个次序
    • 对非根结点v,存在其直接后继w有Low[w] >= DFN[v] ,则v为关节点
    • 对根节点,有两棵以上子树则为关节点

算法

此算法具体实现和Tarjan算法细节有差异

  • 此算法中不需要使用栈保存访问过顶点中是前驱者
    • 连通无向图DFS只会有回边,已访问点必然是前驱结点
  • 需要对根结点额外判断是否为关节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
DFN[MAX_VERTAX], Low[MAX_VERTEX]
index = 0
Q = InitQueue()

FindArticul(G):
// 输入:无向连通图G
// 输出:关节点队列
vroot = G.V.pop()
TarjanArticul(vroot, G)
for(v in G.V if v not visited)
// 根节点有两棵及以上子树
TarjanAricul(vroot, G)
Q.push(vroot)
// 根节点也是关节点
return Q

TarjanArticul(u, G):
// 比较DFS搜索次序、回边到达次序判断关节点
// 输入:结点u,无向连通图G
// 输出:关节点队列
DFN[u] = Low[u] = ++ index
for each (u, v) in G.E:
if (v is not visited):
tarjan(v)
Low[u] = min(Low[u], Low[v])
// 使用v找到的前驱更新u能找到前驱,递归更新
else:
Low[u] = min(Low[u], DFN[v])
// Low[u] = min(Low[u], Low[v])
// 应该也行
for(v connected by u)
if(Low[v] <= DFN[u])
Q.push(u)
return Q

无权路径

路径数量

图中顶点i到顶点j之间长度为k的不同路径数量为$A^k[i, j]$

  • A为图的邻接矩阵
  • 可以使用数学归纳法证明
  • 对无向、有向图均适用

Warshall算法

Warshall算法:生成有向图传递闭包

  • 构造n+1个n阶布尔矩阵$R^{(k)}, k=0,1,\cdots, n$

    • $R^{(k)}_{ij}=1$:顶点i、j直接存在中间顶点编号 不大于k的有效路径

    • $R^{(0)}$:邻接矩阵,顶点直接连接

    • $R^{(k)}, 0<k<n$:路径中间顶点编号最大为k

    • $R^{(n)}$:传递闭包,允许所有类型路径

    • 后继矩阵相对前趋,允许作为路径上顶点增加,可能包含 1数量更多

  • 考虑$R^{(k)}$通过直接前趋$R^{(k-1)}$计算得到

    • $R^{(k-1)}$中已有路径在$R^{(k)}$保留

    • 考虑$R^{(k)}$相较于$R^{(k-1)}$新增$r_{ij}=1$

      • 表示顶点i、j之间存在包含k的路径

      • 若k在路径中出现多次,则将删除回路,得到新路径

      • 则存在ik和kj之间路径满足中间顶点编号小于k,即在 $R^{(k-1)}$中有$r{ik}=1, r{kj}=1$

算法

  • 若元素$r_{ij}$在$R^{(k-1)}$中为1,则在$R^{(k)}$也是1

  • 若元素$r{ij}$在$R^{(k-1)}$中为0,当且仅当存在v使得 $R^{(k-1)}$中$r{iv}=1, r_{vj}=1$

1
2
3
4
5
6
7
8
9
10
11
12
Warshall(A[1..n, 1..n])
// 计算传递闭包的Warshall算法
// 输入:A[1..n, 1..n]包含n个顶点的有向图的邻接矩阵
// 输出:A的传递闭包
R^0 = A
for i = 1 to n do
for i = 1 to n do
for j = 1 to n do
if R^(k-1)[i, j] == 1 or
(R^(k-1)[i, k] == 0 and R^(k-1)[k, j] == 0)
R^k[i, j] = 1
return R^n

算法特点

  • 算法效率

    • 时间效率$\in \Theta(n^3)$
      • 重新构造最内层循环,可以提高对某些输入的处理速度
      • 将矩阵行视为位串,使用或运算也可以加速
    • 空间效率取决于如何处理布尔矩阵
  • 蛮力法:所有点分别作为起点作一次搜索,记录能够访问的顶点

    • 对有向图遍历多次
    • 使用邻接链表表示稀疏图,蛮力法渐进效率好于Warshall算法

最小生成树

Prim算法

Prim算法:求解最小图最小生成树算法

  • 每次添加距离当前树距离最近顶点进树
  • 不断迭代构造最小生成树

算法

  • 从图顶点集V中任选单顶点作为序列中初始子树

  • 对图中顶点维护两个标记:树中最近顶点、相应距离

    • 与树不直接相连顶点置:NULL、$\infty$
    • 每次添加新节点更新两个标记
    • 可使用优先队列维护提高效率
  • 以贪婪的方式扩张当前生成树,添加不在树中的最近顶点

  • 更新顶点和树距离最近的顶点、相应距离

    • 只需考察与新添加顶点直接相连顶点即可
  • 不断迭代直到所有点都在树中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Prim(G):
// 构造最小生成树Prim算法
// 输入:加权连通图G=<V, E>
// 输出:E_T, 组成G最小生成树的边集合
V_T = {v_0}
// 使用任意顶点初始化树顶点集合
E_T = NULL
// 初始化生成树边为空集

for i = 1 to |V|
if i connect V_T
connect_V[i] = 0
connect_D[i] = e(0, i)
else
connect_V[i] = NULL
connect_D[i] = \infty
// 初始化节点和树最近节点列表、节点与树距离列表

for i = 1 to |V|-1 do
// 重复n-1次,直到树包含所有顶点
edge = min(connect_D)
// 寻找距离树最近的点
v = vertex(edge)

V_T = V_T union {v}
E_T = E_T union {edge}

connect_V[v] = NULL
connect_D[v] = \infty
更新和v相连的顶点两个标记值
return E_T

算法特点

  • 算法时间效率依赖实现优先队列、存储图数据结构

    • 图权重矩阵、优先队列无序数组$\in \Theta(|V|^2)$
    • 图邻接链表、二叉最小堆$\in O(|E|log|V|)$
    • 图邻接链表、Fibonacci Heap $\in O(|E| + |V|log|V|)$
  • 对树进行扩展时用到的边的集合表示算法生成树

  • 穷举查找构造生成树,生成树数量呈指数增长,且构造生成树 不容易

Kruskal算法

Kruskal算法:把最小生成树看作是具有$|V|-1$条边、且边权重最小 的无环子图,通过对子图不断扩展构造最小生成树

算法

  • 按照权重非递减顺序对图中边排序

  • 从空子图开始扫描有序列表,试图把列表中下条边加到当前子图 中,如果添加边导致回路则跳过

  • 不断添加边直到达到$|V|-1$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Kruskal(G)
// 构造最小生成树的Kruskal算法
// 输入:G=<V, E>加权连通图
// 输出:E_T,组成G的最小生成树边集
reverse_sort([w(e_i)])
// 按照边权非递减顺序对边集排序
E_T = NULL
ecounter = 0
k = 0
while ecounter < |V|-1 do
k += 1
if E_T union {e_k} 无回路
// 常用并查算法检查`e_k`连接的两个顶点是否在
// 同一棵树(并查集)中
E_T = E_T union {e_k}
ecounter += 1
return E_T

算法特点

  • Kruskal每次迭代都需要检查添加新边是否会导致回路,其实 效率不一定比Prim算法高

  • Kruskal算法中间阶段会生成一系列无环子图(树)

    • 子图不总是联通的
    • 可以看作是对包含给定图所有顶点、部分边的森林所作的 连续动作
    • 初始森林由|V|棵普通树构成,包含单独顶点
    • 最终森林为单棵树,包含图中所有顶点
    • 每次迭代从图的边有序列表中取出下条边,找到包含其端点 的树,若不是同一棵树,则加入边生成一棵更大的树
  • 算法效率

    • 如果检查顶点是否位于同一棵树算法高效,则算法运行时间 取决于排序算法,时间效率$\in O(|E|log|E|)$

Sollin算法

Sollin算法:Prim算法、Kruskal算法的结合,将图每个顶点视为 子树,每次添加多条边合并子树直至得到最小生成树

算法

  • 将图中每个顶点视为一棵树,整个图表示森林$F^{(0)}$
  • 为森林$F$中每棵树选择最小代价边合并两棵树
  • 重复以上,直至所有树合并为一棵树
1
2
3
4
5
6
7
8
9
10
11
12
Sollin(G):
// 无向图最小生成树Sollin算法
// 输入:无向图G
// 输出:最小生成树边集
MST_E = NULL
Forest = G.V
while |MST_E| < |G.V|:
for tree in Forest:
e = find_min(G.E)
tree_b = get_tree(e)
MET_E.add(e)
tree.union(tree_b)

todo

算法特点

  • 算法效率
    • 每轮子树数量减少一半,则最多重复log|V|轮算法终止
    • 时间效率$\in O(|E|log|V|)$

最短路径

Dijkstra算法

Dijkstra算法:求解单起点、权值非负最短路径算法

  • 按照从给定起点到图中各顶点的距离,顺序求出离起始点 最近的顶点、相应最短路径

  • 第i次迭代前,算法已经确定了i-1条连接起点、离起点前i-1近 顶点的最短路径

    • 这些构成了给定图的一棵子树$T_i$
    • 可以在同$T_i$顶点邻接的顶点中找到和起点最接近的顶点 (边权非负)
  • 算法类似于Prim算法,两个对代价评价标准不同

    • Dijkstra算法是各条路径长度:有重复边,考虑整个路径
    • Prim算法是评价各边总和:无重复边,只考虑一条边

算法

  • 对顶点维护两个标记:起点到该顶点最短路径长度d、路径上 前个顶点pre_v

    • 一般使用优先队列维护最短路径长度
    • 对所有顶点维护:$\infty$、NULL标记不在树中、不与树 邻接顶点
    • 仅对生成树中顶点、邻接顶点维护:每次迭代更新列表
  • 根据标记选择邻接顶点中和起始点距离d最小顶点,添加进树

  • 更新顶点标记

    • 因为生成树只新添加一个顶点,只需要考虑与新添加顶点 直接相连、未在树中顶点
    • 比较与起始点距离是否改变
  • 不断迭代直至所有点均在树中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Dijkstra(G, s)
// 单起点最短路径Dijkstra算法
// 输入:G=<V, E>非负权重加权连通图,顶点s起始点
// 输出:对V中每个顶点,从s到v的最短路径长度d_v
Initialize(Q)
// 将顶点优先队列初始化为空
for v in V
d_v = \infty
p_v = NULL
Insert(Q, s, d_s)
// 初始化有限队列中顶点优先级
d_s = 0
Decrease(Q, s, d_s)
// 更新s优先级为d_s
V_T = NULL

for i = 0 to |V|-1 do
u* = DeleteMin(Q)
// 删除优先级最小元素
V_T = V_T \union {u*}
for v in V-V_T 中与u*邻接顶点 do
if d_u* + w(u*, u) < d_u
d_u = d_u* + w(u*, u)
p_u = u*
Decrease(Q, u, d_u)

算法特点

  • 算法时间效率同Prim算法

Bellman-Ford算法

Bellman-Ford算法:求解单节点、权值正负无限制最短距离

  • 权值正负无限制意味着贪心策略不再有效
  • 要求路径中不存在负权值回路
  • 对n个顶点图,路径最长为n-1,否则删除回路路径长度不增加

算法

考虑使用动态规划算法

  • 令$dist^{(l)}[u]$表示从起点v到节点u边数不超过l的最短 路径长度

    • 在不允许出现负权值回路的前提下,构造最短路算法过程 最多只需要考虑n-1条边

    • 即$dist^{(n-1)}$是从v到u不限制路径中边数目的最短路径 长度

Floyd算法

Floyd算法:求解完全最短路径问题,有向、无向、加权图均适用 (边距离不为负,否则距离可以任意小)

  • 构造n+1个距离矩阵$D^{(k)}, k=0,1,\cdots,n$

    • $D^{(k)}$中元素$d_{ij}$表示顶点i、j之间由编号小于k的 顶点作为中间顶点的距离

    • $D^{(0)}$:初始权重矩阵

    • $D^{(k)}, 0<i<n$:路径中顶点编号最大为k

    • $D^{(n)}$:目标距离矩阵

    • 后继矩阵相对前趋,允许作为路径上顶点增加,各顶点间 距离可能缩短

  • 考虑$D^{(k)}$通过直接前趋$D^{(k-1)}$计算得到,其中距离 (路径)分为两类

    • $d^{(k)}{ij} = d^{(k-1)}{ij}$:不包含顶点k作为中间 节点的路径

    • $d^{(k)}{ij} = d^{(k-1)}{ik} + d^{(k-1)}{kj} < d^{(k-1)}{ij}$: 包含顶点k作为中间节点的路径

算法

1
2
3
4
5
6
7
8
9
10
Floyd(W[1..n, 1..n])
// 计算完全最短路径的Floyd算法
// 输入:W不包含负距离的距离矩阵
// 输出:包含最短距离的距离矩阵
D^0 = W
for k = 1 to n do
for i = 1 to n do
for j = 1 to n do
D[i, j] = min(D[i, j], D[i, k] + D[k, j])
return D

算法特点

  • 算法效率

    • 时间效率同Warshall算法为立方级
    • 如上伪码的空间效率为平方级(没有创建n+1距离矩阵)
  • Floyd算法类似于Warshall算法

  • Floyd算法利用最优性原理,即最短路径中子路径也是最短

最大流量问题

Augmenting-Path Method

Shortest-Augmented-Path算法

最短增益路径法(first-labeled first-scanned algorithm

  • 对网络中顶点维护两个标记

    • 从源点到被标记顶点能增加流量数
    • 路径中前个顶点名称
      • +:从前向边访问到当前顶点
      • -:从后向边访问到当前顶点
  • 对网络的每条边$(i, j)$,初始化流量为$x_{ij}=0$

  • 从源点开始同时沿着前向边、后向边进行广度优先搜索

    • 先更新前向边
    • 只有有增益空间边(顶点)才能被访问
    • 更新搜索到顶点标记
  • 源点被标记表明得到一条增量路径,沿着标记反向更新边流量

  • 若广度优先搜索无法达到源点,表明不存在流量增益路径,当前 流量值作为最大值返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
ShortestAugmentingPath(G)
// 最短增量路径算法
// 输入:流量网络G
// 输出:最大流量x
对网络中每条边,设置x[i, j] = 0
把源点标记为(\infty, -),加入空队列Q中
// 使用队列实现广度优先搜索
while not Empty(Q) do
i = Front(Q)
Dequeue(Q)
for 从i到j的每条边 do
// 遍历从i出发的边,前向边
if j未被标记
r[i, j] = = u[i, j] - x[i, j]
if r[i, j] > 0
l[j] = min{l[i], r[i, j]}
/// 更新从源点到顶点j能增加的流量数
用l[j], i+标记j
Enqueue(Q, j)
for 从j到i的每条边 do
// 遍历到达i的边,后向边
if j未被标记
if x[j, i] > 0
l[j] = min{l[i], x[j, i]}
// 更新源点到顶点j能增加的流量数
用l[j], i-标记j
Enqueue(Q, j)
if 汇点被标记
// 沿着找的增益路径进行增益
j = n
while j != 1
// 反向更新到源点为止
if 顶点j前个节点为i+
// 通过前向边访问到顶点j
x[i, j] = x[i, j] + l[n]
else
x[j, i] -= l[n]
j = i
去除除源点外所有顶点标记
重新初始化化队列Q

算法特点

  • 算法正确性可以(联合)最大流-最小割定理证明

  • 算法时间效率

    • 可以证明最短增益路径算法用到的增益路径数量不超过 $|V||E|/2$
    • 对使用邻接列表表示的网络,用广度优先查找找到一条增益 路径的时间$\int O(|V|+|E|)$
    • 所有算法时间效率$\in O(|V||E|^2)$
  • 迭代算法

Preflow推进算法

预流:满足容量约束,但是不满足流量守恒约束

  • 把过剩流量向汇点处移动,直到网络所有中间顶点都满足流量 守恒约束为止

算法特点

  • 算法时间效率
    • 这类算法中较快者最差效率可以接近$O(|V||E|)$

Dinitz算法

Karzanov算法

Malhotra-Kamar-Maheshweari算法

Goldberg-Tarjan算法

单纯形法

此问题仍然是线性规划问题,可以使用单纯形法等通用解法求解

最大匹配(二分图)

匈牙利算法

算法

  • 对U中每个顶点维护一个标记:与其匹配的对偶顶点

  • 从V中的一个自由顶点v出发,按广度优先搜索找到U中自由 顶点u,寻找增益路径,搜索过程中

    • V中顶点:按照广度优先搜索,得到不在匹配M中的边

      • 搜索到U中自由顶点,则停止得到增益路径
      • 搜索到U中被标记顶点,则连接上已有匹配
    • U中顶点:直接找到其在V中的对偶顶点,得到在M中边

  • 得到一个增益路径,沿着增益路径回溯,奇数边加入匹配

  • 未找到自由顶点时,则无法得到增益路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
MaximumBipartiteMatching(G)
// 用类似广度优先算法遍历求二分图的一个最大匹配
// 输入:二分图G=<V, U, E>
// 输出:输入图中一个最大基数匹配
初始边集合M包含某些合法的匹配(例如空集合)
初始队列Q包含V的所有自由顶点(任意序)
while not Empty(Q) do
w = Front(Q)
Dequeue(Q)
if w \in V
for 邻接w的每个顶点u do
// 二分图性质保证u一定在U中
if u是自由顶点
// 增益
M = M \union (w, u)
// 首边进匹配
v = w
while v已经被标记 do
// 从增益路径回溯生成匹配
u = 以v标记的点
M -= (v, u)
// 偶数边出匹配
v = 以u标记的点
M += (v, u)
// 奇数边进匹配
删除所以顶点标记
用V中所有自由顶点重新初始化Q
break
// 增益后,重新搜索
else
// u已经匹配
if (w, u) not \in M and u未标记
用w标记u
Enqueue(Q, u)
else
// w \in U,此时w必然已经匹配
用w标记w的对偶v
// 将已有匹配添加进增益路径中
Enqueue(Q, v)
return M
// 当前匹配已经是最大匹配

算法特点

  • 注意:从自由顶点开始寻求匹配时,无论是否找到增益路径, 路径中中U中节点标记已经更新,匹配仅在得到增益路径才更新

  • 算法时间效率

    • 每次迭代花费时间$\in O(|E|+|V|)$,迭代次数 $\in O(|V|/2 + 1)$
    • 若每个顶点的信息(自由、匹配、对偶)能在常数时间内 得到(如存储在数组中)
    • 则算法时间效率$\in O(|V|(|V| + |E|))$
  • 算法正确性参见图graph_undirected关于增益路径-最大匹配

  • 迭代算法

霍普克罗夫-卡普算法

算法特点

  • 对匈牙利算法的改进,把多次迭代在一个阶段完成,然后用一次 查找把最大数量边添加到匹配中

  • 算法时间效率:$\in O(\sqrt {|V|}(|V| + |E|))$

稳定婚姻问题

婚姻稳定算法

存在自由男士,任选求婚回应之一执行,直至不存在自由男士

  • 求婚:自由男士m向女士w求婚,w为其优先级最大、之前未拒绝 过其女士(可以是已匹配)

  • 回应:若女士w自由则接受男士m求婚,与之配对;女士w不自由 则把m同当前配偶匹配,选择其中优先级较高者

算法

算法特点

  • 算法会在$n^2$次迭代内终止:至多每位男士向所有女士求婚

  • 性别倾向:总是生成man-optimal的稳定匹配,优先满足 男士偏好

    • 在任何稳定婚姻中,总是尽可能把优先级最高的女士分配给 男性
    • 使用女士进行求婚也只会把性别偏见反向,而不能消除
  • 对给定的参与者优先选择集合而言,男士(女士)最优匹配唯一

    • 由性别性别倾向容易证明
    • 所以算法的输出不取决于自由男士(女士)求婚顺序,可以 使用任何数据结构表示参与者集合而不影响结果
  • 算法最终输出匹配M为稳定婚姻匹配证明参见graph

分配问题(二分图)

n个任务分配给n个人执行(一人一个),将任务j分配个人i的成本为 $C_{ijd}$,求最小成本分配方案

  • 类似问题:最大权重匹配问题

蛮力算法

算法

  • 生成整数n的全部排列
  • 根据成本矩阵计算每个分配方案总成本
  • 选择和最小的方案

特点

  • 算法排列次数为$n!$

分支界限法

  • 第i层节点下界可取:$lb = c + \sum_{k=i+1}^n min{c_k}$

    • $c$:当前成本
    • $min{c_k}$:成本矩阵第k行最小值

算法

特点

匈牙利算法

算法

特点

Topological Sorting

拓扑排序:按照次序列出有向图的顶点,使得对图中每条边,其 起始顶点总在结束顶点之前

删点法

算法

  • 在有向图中求出源(没有输出边的顶点),然后把删除其和所有 从它出发的边
  • 不断重复,直到不存在源,如果此时图中还有顶点,则图中存在 环,无解
  • 则删除节点顺序即为拓扑排序可行解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
TopologicalSort(G):
// 从有向图中不断删除入度为0的点、入栈,判断有向图G是否
// 为DAG,并给出拓扑排序栈
// 输入:有向图G
// 输出:拓扑排序栈T
InitStack(S)
indgree = [v.indegree for v in G]
for v in G:
if v.indgree == 0
S.push(v)
// 存储0入度结点栈
count = 0
// 删除结点计数
while(!S.empty())
v = S.pop()
T.push(v)
count++
for(u connected to v)
if --indgree.u == 0
S.push(u)
if count < |G.V|
// 结点未删除完毕,但无0入度结点
// G中有回路,报错
return ERROR
return T

特点

  • 算法效率
    • 时间效率$\in O(|V|+|E|)$
  • 减常数法

DFS逆后序遍历

图中无环时,由某点出发进行DFS

  • 最先退出DFS的为出度为0的点,即拓扑有序序列中最后顶点

  • 按照DFS退出先后次序得到序列即为逆向拓扑有序序列

    • 使用逆后序方式存储DFS访问顶点,判断是否有环、出栈 次序即为正向拓扑有序序列

应用

  • 判断庞大项目中相互关联任务不矛盾,然后才能合理安排,使得 项目整体完成时间最短(需要CPM、PERT算法支持)

Cirtical Path问题

找出使用AOE网表示的工程的中关键路径

  • 关键路径由关键活动构成
  • 即耗费时间变动对工程整体完成时间有影响的活动

拓扑排序求解

  • 最早、最晚开始时间检查是否为关键活动
  • 建立活动(边)、事件(顶点)发生事时间关系
  • 拓扑排序求解事件发生最早、最晚时间
  • 具体参见algorithm/data_structure/graphdi_specials

算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
TopologicalOrder(G):
// 从有向图中不断删除入度为0的点、入栈,判断有向图G是否
// 为DAG,并给出拓扑排序栈
// 输入:有向图G
// 输出:拓扑排序栈T、顶点事件最早发生事件ve
InitStack(S)
indgree = [v.indegree for v in G]
ve[0..|G.V|] = 0
for v in G:
if v.indgree == 0
S.push(v)
// 存储0入度结点栈
count = 0
// 删除结点计数
while(!S.empty())
v = S.pop()
T.push(v)
count++
for(u connected by v)
ve[u] = max{ve[u], ve[v] + len(v, u)}
// 若有更长路径,更新
if --indgree[u] == 0
S.push(u)
if count < |G.V|
// 结点未删除完毕,但无0入度结点
// G中有回路,报错
return ERROR
return T, ve

CriticalPath(G, T, ve):
// 逆序求顶点事件最晚发生时间,求出关键活动
// 输入:有向无环图G,G拓扑排序
// 输出:关键活动队列Q
vl[0..|G.V|] = ve
Q = InitQueue()
while(!T.empty())
v = T.pop()
for (u connect to v)
vl[u] = min{vl[u], vl[v] - len(u, v)}
for(v in G.V)
for (u connected by v)
ee = ve[v]
el = vl[u] - len(v)
if(el == ee)
Q.push(G.edge(v, u))
return Q
  • 以上算法中在生成拓扑排序栈时同时得到各顶点事件最早发生 时间

  • 可以只获取拓扑排序栈,然后处理其获得顶点事件最早发生时间 ,将两个功能分离,只是处理一遍顶点而已

  • 也可以使用其他算法获得拓扑排序栈

    • DFS遍历甚至可以遍历顶点一遍,同时获得顶点事件最早、 最晚发生时间

特点

  • 算法效率
    • 时间效率$\in O(|V|+|E|)$

哈密顿回路问题

确定给定图中是否在包含一条哈密顿回路

回溯算法

算法

  • 对所有节点维护标记:是否位于当前路径中

  • 选择某节点a作为哈密顿回路起点顶点,即回溯状态空间树根

  • 从根节点开始处理

    • 若节点周围还有未标记节点,选择下个加入路径、标记
    • 若节点周围没有未标记节点,回溯到之前节点重新处理
  • 直到所有节点都被标记,且当前节点和根节点相邻

特点

旅商问题

Traveling Salesman Problem:对相互之间距离已知为正整数的n座 城市,求最短漫游路径,使得在回到出发城市之前,对每个城市只 访问一次

  • 即:对权重为正整数的无向完全图寻找最短哈密顿回路

蛮力算法

算法

  • 生成n-1个中间城市的组合得到所有旅行线路
  • 计算线路长度,求得最短路径

特点

  • 算法排序次数为$(n-1)!/2$

改进

  • 线路成对出现,只是方向相反,可考虑任意两个相邻顶点,只 考虑包含其某个排序的线路

分支界限法

  • 第i层下界可取$lb = \sum{k=i+1}^n d{k1}$

  • 更紧密、也不复杂的下界 $lb = \lceil \frac {\sum{k=i+1}^n (d{k1} + d_{k2})} 2 \rceil$

    • $d{k1}, d{k2}$:城市$i+1$到最近的两个城市距离

    • 最短路径为两个端点共享,至多只能有一个端点能够成为 该边起点

    • 若要求所有哈密顿回路中必须包括某些边,则在考虑相应 边端点城市时,使用必须边(若不是节点最短边)替换其中 次短边

  • 只需要生成某对节点有序的路径:可以消去状态空间树中部分 分支

算法

特点

旅商问题非精确算法

以下均是讨论TSP问题的欧几里得实例,不对称实例等已经证明更难 解决,对精确算法、启发式算法都是如此

贪婪算法

Nearest-Neighbor算法

  • 任意选择城市开始
  • 每次访问和当前城市k最接近的城市,直到访问完所有城市
  • 回到开始城市

Multifregment-Heuristic算法

求给定加权完全图的最小权重边集合,且每个顶点连通度均为2

  • 将边按权重升序排列,将要构造的旅途边集合开始时空集合

  • 不断尝试将排序列表中下条边加入旅途边集合

    • 边加入不会使得某节点连通度大于2
    • 不会产生长度小于n的回路
    • 否则忽略这条边
  • 返回旅途集合

算法特点

对属于欧几里得类型的旅商问题实例(大部分)

  • 此时虽然两个算法的精确性能比无法界定,但是满足 $\frac {f(s_a)} {f(s^{*})} \leqslant \frac 1 2 (\lceil log_2 n \rceil + 1)$

Minimum-Spaning-Tree-Based Algorithm

基于最小生成树的算法

  • 哈密顿回路中去掉一条边就能得到一棵生成树$T_h$
  • 可以先构造一棵最小生成数$T^{*}$,然后在其基础上构造近似 最短路径

Twice-Around-The-Tree算法

  • 对给定实例构造最小生成树$T^{}$(Prim, Kruskal*)
  • 从任意顶点开始,(利用深度优先遍历)绕树散步一周,记录 经过顶点
  • 扫描顶点列表,消去重复出现顶点(走捷径,直接去新城市), 除列表尾部重复起点,得到一条哈密顿回路
  • 可能是考虑到最小生成树能够选出部分最短路径???

特点

对属于欧几里得类型的旅商问题

  • 绕树两周算法是2近似算法:$2f(s^{*}) > f(s_a)$

    • $f(s^{} > w(T_h) \geqslant w(T^{})$:最优哈密顿 回路去掉一条边后长度大于等于最小生成树长度
    • $f(s^{}) < 2w(T^{})$:第二次扫描走捷径距离小于绕树 一周距离
    • 这里限定了特点类型实例,并没有找到对所有旅商问题的 优先近似算法

Christofides算法

同样利用问题与最小生成树的出关系,但更复杂

算法

  • 对给定实例构造最小生成树$T^{*}$
  • 构造包含包含$T^{*}$的欧拉回路
    • 找出最小生成树中所有连通度为奇数的顶点
    • 求出这些顶点的最小权重匹配(匈牙利算法)
    • 将最小权重匹配边加入树中得到多重图欧拉回路
  • 使用走捷径方法将欧拉回路转换为哈密顿回路

特点

对属于欧几里得类型的旅商问题

  • 绕最小生成树一周得到的路径是多重图的一条欧拉回路,其中 多重图为将当前图每条边重复一遍得到

    • 绕树两周算法:直接原始欧拉回路上走捷径
    • Christofides算法:重新构建更短的欧拉回路,在此基础 上走捷径
  • Christofides算法是1.5近似算法

    • 实际应用中,Christofides算法近似解明显好于绕树两周
    • 可以对连通度大于2顶点尝试不同访问次序,即将回路 中邻接顶点分别两两组合,找到访问其的最佳路径

迭代改进算法

Local Search Heuristics:本地查找启发法

  • 这类算法从某个初始旅途(随机或简单近似算法生成)开始

  • 每次迭代把当前旅途一些边用其他边代替,试图得到和当前旅途 稍有差别的旅途

    • 若能得到优于当前旅途的新旅途,则替换当前旅途,继续 寻找
    • 否则,返回当前旅途,停止

2选算法

删除旅途中2条非临边,把两条边端点用另一对边重新连接

  • 此操作称为2改变
  • 为保证重连后得到合法哈密顿回路,重连方法只有一种

3选算法

删除3条非临边后重连

  • 重连方法有3种
  • 事实上可以推广到k选,但是只有3改变被证明有意义

Lin-Kernighan算法

变选算法算法的一种

  • 可以视为在3选操作后进行一系列2选操作

算法特点

  • 迭代改进算法求得的近似解效果质量非常好
  • Lin-Kernighan算法是公认的求解高质量近似解的最佳算法

Held-Karp Bound

Held-Karp下界

  • 将TSP描述为线性规划问题求解(忽略整数约束)得到,计算 速度快
  • 一般和最优旅途长度非常接近,误差不超过1%
  • 可使用其代替最短旅途估计近似算法的精确度

模拟

  • 10000个随机点:坐标、距离取整
  • Comqaq ES40:500MHz的Alpha处理器、2GB内存
启发式算法 超过Held-Karp下界的% 运行时间
最近邻居 24.79 0.28
多片段 16.42 0.20
Christofides 9.81 1.04
2选 4.70 1.41
3选 2.88 1.50
Lin-Kernighan 2.00 2.06

分类

图$G=$:由一些称为顶点的点构成的集合,其中某些顶点由 称为边的线段相连

  • 有限集合V:元素为顶点vertex
  • 有限集合E:元素为顶点二元组,称为边edge/弧arc

边方向

Undigraph/Undirected Graph

  • 无向图:所有边都是无向边的图
  • 无向边:若有序对$(u, v) \in E$,必有$(v, u) \in E$, 即E是对称的,顶点对$(u, v)$等同于$(v, u)$,没有顺序

对无向边$(u, v)$

  • 则顶点u、v相互adjcent
  • 其通过undirected edge$(u, v)$相连接
  • 顶点u、v称为边$(u, v)$的endpoint
  • u、v和该边incident(相依附)

Digraph

  • 有向图:所有边都是有向边的图
  • 有向边:若有序对对$(u, v) \in E$无法得到$(v, u) \in E$, 即两者不等同,有顺序

对有向边$(u, v)$

  • 边$(u, v)$的方向是从顶点u到顶点v
  • u称为tail,v称为head

边权重

Ordered Graph

有序图:各边地位有序

Weighted Graph/Network

加权图/网络:给边赋值的图

  • 值称为weightcost
  • 有大量的现实应用

边数量

  • complete graph:任意两个顶点直接都有的边相连的图
    • 使用$K_{|v|}$表示有|V|个顶点的完全图
  • dense graph:图中所缺的边数量相对较少
  • sparse:图中边相对顶点数量较少

特点

  • 不考虑loop(顶点连接自身边),则含有|V|个顶点无向图 包含边的数量|E|满足: $ 0 \leq |E| \leq \frac {|V|(|V| - 1)} {2} $

  • 对于稀疏图、稠密图可能会影响图的表示方法,影响设计、使用 算法的运行时间

图表示方法(存储结构)

Adjacency Matrix

邻接矩阵:两个数组分别存储数据元素(顶点)信息、数据元素之间 关系(边)的信息

  • n个顶点的图对应n * n的bool矩阵
    • 图中每个顶点使用一行和一列表示
    • i、j节点间有边,则矩阵第i行、j列元素为1,否则为0
  • 无向图邻接矩阵一定是对称的,有向图邻接矩阵不一定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef enum{DG, DN< UDG, UDN} GraphKind;
typedef struct ArcCell{
VRType adj;
// 顶点关系类型:无权图0、1是否相邻,加权图权值类型
InfoType * info;
// 弧相关信息
}ArcCell, AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
typedef struct{
VertexType vexs[MAX_VERTEX_NUM];
// 顶点向量
AdjMatrix arcs;
int vexnum, arcnum;
// 图当前顶点弧数
GraphKind kind;
}

Adjacency List

邻接表:n条单链表代替邻接矩阵的n行,对应图G的n个顶点

  • 每个顶点用一个邻接表表示

    • 线性表的header表示对应的顶点
    • 链表中结点表示依附于顶点的边
  • 无向图一条边在邻接链表中对应两条链,有向图对应一条

    • 有向图出度计算简单
    • 计算入度则比较复杂,如果需要频繁计算入度,可以再存储 一个反向邻接表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct ArcNode{
int adjvex;
// 弧指向顶点信息
struct ArcNode *nextarc;
// 指向下条弧指针
InfoType * info;
// 弧相关信息
}ArcNode;
typedef struct VNode{
VertexType data;
// 顶点信息
ArcNode * firstarc;
// 首条依附该顶点的弧指针
}VNode, AdjList[MAX_VERTEX_NUM];
typedef struct{
AdjList vertices;
int vexnum, arcnum;
int kind;
}

Orthogonal List

十字链表:将有向图的邻接表、逆邻接表结合起来

  • 有向图中每条弧、每个顶点对应一个结点
  • 弧结点所在链表为非循环链表,
    • 结点之间相对位置自然形成,不一定按照顶点序号有序
  • 表头结点即顶点结点,顺序存储

orthogonal_list_structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct ArcBox{
int tailvex, headvex;
// 头、尾顶点链域
struct ArcBox *hlink, *tlink;
// 头相同、尾相同弧链域
InfoType *info;
// 弧相关信息
}ArcBox;
typedef struct VexNode{
VertexType data;
// 顶点信息
ArcBox *firstin, *firstout;
// 顶点第一条出、入弧
}VexNode;
typedef struct{
VexNode xlist[MAX_VERTEX _NUM];
// 表头
int vexnum, arcnum;
}OLGraph;

Adjacency Multilist

邻接多重表:无向图的另一种存储形式

  • 一条边对应唯一结点
    • 所有依附于同一顶点的串联在同一链表中
    • 每个边界点同时链接在两个链表中
    • 避免无向图邻接表中一条边两次出现
  • 类似十字链表,仅无需区分头尾结点

adjacency_multilist_structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct EBox{
VisitIf mark;
// 访问标记
int ivex, jvex;
// 边依附的两个顶点
struct EBox *ilink, *jlink;
// 依附两个顶点的下条边
InfoType *info;
// 边信息指针
}EBox;
typedef struct VexBox{
VertexType data;
EBox *firstedge;
// 第一条依附该顶点的边
}VexBox;
typedef struct{
VexBox adjmulist[MAX_VERTEX_NUM];
int vexnum, edgenum;
}AMLGraph;

说明

  • 稀疏图:尽管链表指针会占用额外存储器,但是相对于邻接矩阵 占用空间依然较少

  • 稠密图:邻接矩阵占用空间较少

  • 邻接矩阵、邻接链表都可以方便的表示加权图

    • 邻接矩阵元素A[i, j]设置为有限值表示存在的边的权重, 设置为$\infty$表示不存在边,此时矩阵也称 weighted matrixcost matrix
    • 邻接链表在节点中型跨同时包含节点名称和相应边权重
  • 任何可以存储顶点、边的数据结构(如集合)都可以表示图, 只是存储效率低、无法高效操作

概念

路径

有向图

  • directed path:有向图中顶点的一个序列,序列中每对连续 顶点都被边连接,边方向为一个顶点指向下一个顶点

  • directed cycle:有向图中节点序列,起点、终点相同,每个 节点和其直接前趋之间,都有一条从前趋指向后继的边

  • directed acyclic graph:DAG,有向无环图

无向图

  • path:(无向)图G中始于u而止于v的邻接顶点序列,即为顶点u 到顶点v的路径

  • simple path:路径上所有点互不相同

  • cycle:特殊的path

    • 起点、终点都是同一顶点
    • 长度大于0
    • 不包含同一条边两次
  • acyclic:不包含回路的图

长度

  • length:路径中代表顶点序列中的顶点数目减1,等于路径中 包含的边数目
  • distance:顶点间距离,顶点之间最短路径长度

连通性

无向图

  • connected:顶点u、v连通,当且仅当存在u到v的路径

  • connected graph:连通图,对图中的每对顶点$(u, v)$, 都有从u到v的路径

  • connected component:连通分量,给定图的极大连通子图, 即没有可以添加的连通子图用于扩充

    • 非连通图中包含的几个自我连通的部分
  • articulation point:关节点,如果从连通图$G$中删除顶点 $v$、极其邻接边各点之后的的图$G^{‘}$至少有两个连通分量, 则称顶点$v$为关节点

  • biconnected graph:重连通图,没有关节点的连通图

  • biconnected component:重连通分量,连通图的最大重连通 子图,即不是其他重连通分量的真子图

    • 两个重连通分量不可能共享一个以上节点,即图的一条边 不可能同时出现在两个重连通分量中 (否则两个“重连通分量”可以合并)
    • 所以可以说重连通分量是对原图的边的划分

有向图

  • 强连通图:对所有不同顶点u、v,都存在u到v、v到u的路径
  • strongly connected component:强连通分量,有向图的 极大连通子图
源点、汇点
  • soruce:源,没有输入边的顶点
  • sink:汇点,没有输出边顶点

图遍历

遍历图:实质是对每个顶点查找其邻接顶点的过程

  • 具体算法参见algorithm/problem/graph

遍历方式

  • BFS:Breadth First Search:广度优先搜索,类似树的先序 遍历
  • DFS:Depth First Search:深度优先搜索,类似树的按层次 遍历

  • 具体算法参见algorithm/problem/graph

结点处理顺序

  • post-order:后序,先处理子节点,再处理当前结点
  • pre-order:前序,先处理当前结点,再处理子节点
  • 搜索森林中仅有一棵树

    • 前序、后序均满足处理顺序(后序为逆处理顺序)
    • 前序、后序处理仅是处理顺序刚好相反
  • 搜索森林中有多棵树时,将每棵树视为一个结点考虑

    • 每个树内前序、后序顶点处理顺序相反
    • 不同树整体前序、后序处理顺序相反
    • 此时前序不再满足处理顺序,后序仍为逆处理顺序,所以 前序不常用
  • 节点处理记录方式

    • 栈:出栈顺序同记录反向
    • 队列:出队列顺序同记录顺序

总结

  • Pre-Order:正前序,先当前顶点、后子顶点,队列存储

  • Reverse Pre-Order:逆前序,先子顶点、后当前顶点, 栈存储

  • Post-Order:正后序,先当前顶点、后子顶点,队列存储

  • Reversed Post-Order:逆后序,先子顶点、后当前顶点, 栈存储,也称为伪拓扑排序

    • 可以用于得到DAG的拓扑有序序列
    • 也可以用于得到有环有向图的伪拓扑序列
      • 强连通分量整体看作结点,组成DAG
      • 各强连通分量必然可以选出某个顶点,满足在伪拓扑 序列中次序先于DAG中先驱强连通分量所有顶点
    • 用途最广
      • 有向图拓扑排序
      • Kosaraju算法中用到

BFS、DFS森林

广度优先、深度优先生成森林

  • 遍历的初始顶点可以作为森林中树的根
  • 遇到新未访问的顶点,将其附加为直接前趋子女
  • 其实仅有树向边才是符合森林定义(其他边都只是搜索过程 中遇到)
  • DFS森林中边是从左到右逐渐生成
  • BFS森林中边是从上到下逐渐生成

边类型

  • tree edge:树向边,连接父母、子女的边
  • back edge:回边,连接非直接前驱的边
    • 对有向图包括连接父母的边
    • 对无向图不存在连接父母边
  • cross edge:交叉边,连接非前驱、后继的边
  • forward edge:前向边,连接非直接后代的边

边存在性

  • 无向图
    • DFS森林:只可能有回边
    • BFS森林:只可能有交叉边
  • 有向图
    • DFS森林:可以都有
    • BFS森林:只可能有回边、交叉边

Spanning Tree

生成树/极小连通子图:包含图中所有顶点的连通无环子图(树)

  • n个顶点的生成树有且仅有n-1条边,反之不成立

  • 在生成树添加一条边必定构成一个环,因为其实使得其依附的 两个顶点之间生成了第二条路径

Minimum Cost Spanning Tree

最小生成树:图的一棵权重最小的生成树(权重指所有边权重之和)

MST性质
  • 若$N=(V, {E})$是连通网,U是顶点集V的非空子集,若 $(u, v), u \in U, v \in V-U$是一条具有最小权值(代价)边 ,则图中存在最小生成树包含该边
  • 假设网N中最小生成树T不包含边$(u, v)$,将其加入T中

  • 因为T为生成树,所以必存在边 $(u^{‘}, v^{‘}), u^{‘} \in U, v^{‘} \in V-U$,且 $u, u^{‘}$、$v, v^{‘}$之间均有路径连通

  • 则删除$(u^{‘}, v^{‘})$可以消去上述回路,得到新生成树 $T^{‘}$,且代价不大于$T$,矛盾

  • 存在是考虑到存在其他同权值边,若权值严格最小,则所有 最小生成树必然包含
  • 依此性质生成算法参见algorithm/problems/graph

连通性

  • 对图进行遍历是判断图连通性、求解连通分量的好方法

有向图

  • 连通图:从图中任意顶点出发,进行深度、广度优先搜索即可 访问到图中所有顶点

    • 利用DFS生成树可以找出图的重连通分量
  • 非连通图:需要从多个顶点起始进行搜索

    • 每次从新的起始点出发进行搜索过程中得到顶点访问 序列就是各个连通分量的顶点集

无向图

  • 深度优先搜索是求有向图强连通分量的有效方法
  • 具体算法参见algorithm/problem/graph

有向图衍生

Directed Acycline Graph

DAG:有向无环图

  • 描述含有公共子式的表达式的有效工具
    • 可以实现对相同子式的共享,节省存储空间
  • 描述一项工程、系统进行过程的有效工具
    • 拓扑排序:工程能否顺利进行
    • 关键活动:整个工程完成必须的最短时间

Dependence Analysis

依赖性分析:由有向图判断、构造可行任务执行序列

Topological Sort

  • 拓扑有序:由偏序得到的全序
  • 拓扑排序:由偏序定义得到拓扑有序的操作
  • 构造有向图中顶点的拓扑有序序列

    • 得到可行任务执行顺序
  • 判断有向图AOV-网中是否存在环,即是否为DAG

    • 若图中所有顶点都在拓扑有序序列中,则有向图中不存在环
  • 参见algorithm/problems/graph
  • AOV网:activity on vertex network,顶点表示活动,弧 表示活动间优先关系的有向图

关键活动优化

Critical Path

关键路径:路径长度最长的路径

  • 对整个AOE网,开始点到结束点的关键路径长度即为完成工程 的最短时间

  • 对事件$v_i$,从起始点$v_1$到其的关键路径长度即为,以 $v_i$为尾的活动的最早开始时间

  • 关键路径上的所有活动均为关键活动

    • 分析关键路径目的就是找出关键活动
    • 提高关键活动工效缩短工期
  • AOE网:activity on edge network,顶点表示事件,边表示 活动持续事件的带权有向无环图
    • 一般网络中只有一个源点、汇点,表示工程开始、完成
    • 以上假设AOE网中活动可以并行进行

关键活动

  • 关键活动:记$ee{ij}$为活动$a{ij}$最早开始时间、 $el{ij}$为不推迟整个工程完成前提下$a{ij}$最迟开始时间 ,称$ee{ij} = el{ij}$称为关键活动
  • $el{ij} - ee{ij}$表示完成活动$a_{ij}$的时间余量

    • 提前完成非关键活动不能加快工程进度
    • 关键活动耗时一定范围变化影响工程整体完成时间
  • 考虑如下事件和活动发生关系有

    • $ve_i, vl_i$:事件(顶点)i最早、最晚发生的时间
    • $a{ij}$:活动$a{ij}$需要持续时间
  • 对$ve_i, vl_i$,存在如下递推关系

    则依拓扑排序可计算得$ve_i$,逆拓扑排序可计算得$vl_i$

  • 参见algorithm/problems/graph
  • 为求解AOE网中活动$e{ij}, l{ij}$,应该首先求出事件

Flow Network

流量网络

  • 包含一个源点:物质流唯一出发点
  • 包含一个汇点:物质流唯一汇聚点
  • 每条有向边权重为边capacity的正整数$u_{ij}$
    • 事实上,有理数总可以通过变换变为整数,所以只要容量为 有理数就可以
    • 计算机无法真正处理无理数,无理数边权只具有理论意义

流量约束

Capacity Constraits

容量约束:通过边的流量不大于边容量$x{ij} \leqslant u{ij}$

Flow-Conservation Requirement

流量守恒要求:除源点、汇点外其余顶点只能改变流方向,进入、 流出物质总量不变,不能消耗、添加物质

  • 其中$x_{ij}$表示通过边$(i,j)$的流量(传输量)
  • 等式左右为进入、离开顶点i的输入、输出流总和

并且

  • 流的值 = 源点输出流 = 汇点输入流
  • 可通过流量守恒要求推出

最大流

最大流:分配流量网络中边权(实际流量),使得网络在满足流量 守恒、容量束情况下,最大的流的值(边容量都为正整数)

cut

割:$C(X,\bar X)$=所有头在$X$、尾在$\bar X$边集合

  • $X$;包含源点、不包含汇点的顶点V的子集
  • $\bar X$:$X$的补集,包含汇点
  • 删除割中所有边,则不存在从源点的汇点的路径

Augmenting-Path Method

Augmenting-Path

流量增益路径:当前流情况下,可以传输更多流量、从源点到 汇点路径,考虑路径$i \rightarrow j \leftarrow k$中顶点j边

  • forward edge:前向边$(i, j)$,同流量网络中边$(i, j)$ 方向相同

    • 具有正的未使用容量$r{ij} = u{ij} - x_{ij}$
    • 可以把该边流量增加最多$r_{ij}$
  • backward edge:后向边$(j, k)$,同流量网络中边$(k, j)$ 方向相反

    • 具有正流量$x_{kj}$
    • 可以把该边流量减少最多$x_{kj}$

增益路径法

  • 寻找网络中增益路径,设$r$为路径中各前向边未使用容量 $r{ij}$、后向边流量$x{jk}$最小值

  • 每条前向边流量加r、后向边流量减r,可以得到新的可行流量, 且流量值增加r,不断迭代

  • 基于边容量、流量都为正整数假设

    • r也为正整数,每次迭代流量值至少增加1

    • 流量最大值有上界为源点为起点边容量和,则增益路径法 在有限步迭代后会停止

  • 联合最大流-最小割定律可以证明

    • 最终流量一定是最大化的,等于最小割容量
    • 和增量路径选择无关
  • 最后一步迭代中

    • 所有已标记顶点和未标记顶点之间边就构成最小割
    • 这样边流量要么满(标记到未标记)、要么空(反)
  • 增益路径法具体实现参见graph
  • 算法主要问题在于如何寻找增益路径,生成路径的顺序对算法 有巨大影响

Max-Flow Min-Cut Theorem

最大流-最小割定理:网络中最大流量值等于其最小割容量

Theorem1

网络中最大流量值小于任意割容量

  • 设可行流量x值为v,割$C(X, \bar_X)$容量为c

  • 通过该割的流量为:$X$到$\bar_X$的流量之和$\bar_X$到$X$的流量之和的差值(可由流量守恒推导)

  • 由流量非负可得

    即网络上任何可行流值不能超过网络上任意割容量

Theorem2

网络中最大流量等于最小割容量,可以使用增益路径法证明

  • 设$v^{}$为通过路径增益法得到的最终流$x^{}$的值

  • 对增益路径法最终流量情况下,考虑顶点集合$X^{*}$

    • 包含源点
    • 其他顶点可由源点出发,经未使用的容量大于0的前向边、 流量大于0的后向边组成路径到达
  • 则汇点不属于$X^{*}$,否则存在一条流量增益路径,不是流量 增益法最终流情况

  • 考虑割$C(X^{}, \bar_{X^{}})$

    • $X^{}, \bar_{X^{}}$之间任意边$(i,j)$: $x{ij} = u{ij}$,没有未使用容量

    • $\bar{X^{}}, X^{}$之间任意边$(j,i)$:$x{ji}=0$, 没有流量

    • 否则顶点j$\in X^{*}$

  • 则有

    即存在某个割容量等于流量增益法得到最终流量值

SparkSQL2.4中启用CBO时JoinReorder分析

背景

Spark Join方式

SparkSQL目前支持三种join方式

  • broadcast hash join:将小表广播分发到大表所在的结点上 ,并行在各节点上进行hash join

    • 仅适合内表非常小的场合
  • shuffle hash join:按照join key分区,每个结点独立并行 进行hash join

    • 类似分布式GHJ,不同块位于不同结点
  • sort merge join:按照join key分区,在各节点独立并行SMJ

    • spark当前shuffle算法使用sort-based shuffle算法
    • 理论上shuffle过后各分区数据已经排序完毕,无需再次 sort,效率很高

Join类型

SparkSQL支持的Join类型可以分为以下两类

  • 顺序结果无关Join

    • inner join
    • (full)outer join
  • 顺序结果相关Join

    • left(outer) join
    • right(outer) join
    • left semi join
    • right semi join

考虑到JoinReorder的结果

  • 仅支持连接重排序的连接类型只可能是inner join outer join

  • outer join重排序虽然不影响结果,但是处理不方便,所以 联接重排序一般仅限于inner join???

    • 有些情况下RBO可以将外联接等价转换为内联接
    • SparkSQL2.4中支持的连接重排序仅限于内连接

Cost-Based Opitimization/Optimizer

CBO:基于成本的优化(器)

  • 根据SQL的执行成本制定、优化查询作业执行计划,生成可能 的执行计划中代价最小的计划

    • 数据表统计数据
      • 基/势
      • 唯一值数量
      • 空值数量
      • 平均、最大长度
    • SQL执行路径I/O
    • 网络资源
    • CPU使用情况
  • 在SparkSQL Hash Join中可以用于

    • 选择正确hash建表方
    • 选择正确join类型:广播hash、全洗牌hash
    • join reorder:调整多路join顺序
  • CBO本身需要耗费一定资源,需要平衡CBO和查询计划优化程度

    • 数据表的数据统计资源耗费
    • 优化查询计划即时资源耗费
  • CBO是相较于Rule-Based Optimization的概念

CBO中的独特概念

  • cardinality:集的势,结果集的行数

    • 表示SQL执行成本值
    • SQL执行返回的结果集包含的行数越多,成本越大
  • selectivity:可选择率,施加指定谓语条件后返回结果集的 记录数占未施加任何谓语条件的原始结果记录数的比率

    • 值越小,说明可选择性越好
    • 值越大,说明可选择性越差,成本值越大

Join Reorder

Join Reorder:基于CBO的多表连接顺序重排

  • 用统计信息预估的基修正join顺序

  • 主要涉及到以下两个方面

    • 查询代价估算
    • 多表连接顺序搜索算法

查询代价估计

代价模型

  • 单个join操作成本

    • carinality:对应CPU成本
    • size:对应IO成本
  • join树的成本是所有中间join成本总和

Filter Selectivity估计

过滤选择率:估计应用谓词表达式过滤的选择率

逻辑运算符
  • AND:左侧过滤条件选择率、右侧过滤条件选择率之积

  • OR:左侧、右侧过滤条件选择率之和,减去其乘积

  • NOT:1减去原始过滤条件选择率

比较运算符
  • =:等于条件

    • 若常数取值在当前列取值范围之外,则过滤选择率为0
    • 否则根据柱状图、均匀分布得到过滤选择率
  • <:小于条件

    • 若常数取值小于当前列最小值,则过滤选择率为0
    • 否则根据柱状图、均匀分数得到过滤选择率

Join Carinality估计

联接基:估计联接操作结果的基

  • inner:其他基估计值可由inner join计算

    • num(A):join操作前表A的有效记录数
    • distinct(A.k):表A中列k唯一值数量
  • left-outer:取inner join、左表中基较大者

  • right-outer:取inner join、右表中基较大者

  • full-outer

多表连接顺序搜索算法

SparkSQL2.4中使用动态规划算法对可能联接顺序进行搜索,从中 选择最优的联接顺序作为执行计划

  • 最优子结构:一旦前k个表联接顺序确定,则联接前中间表和 第k+1个表方案和前k个表的联接顺序无关

  • 动态规划表:从单表代价开始,逐层向上计算各层多表联接代价 ,直到求得所有表联接最小代价

  • 减少搜索空间启发式想法:尽可能优先有谓词限制的内连接、 中间表

评价

  • 优势:动态规划算法能够求得整个搜索空间中最优解
  • 缺陷:当联接表数量增加时,算法需要搜索的空间增加的非常快 ,计算最优联接顺序代价很高

PostgreSQL

代价模型

Postgres的查询代价估计模型基于CPU开销、IO开销,另外还增加 了启动代价

动态规划算法

类似SparkSQL2.4多表连接算法(假设联接n个表)

  1. 构造第一层关系:每个关系的最优路径就是关系的最优单表扫描 方式

  2. 迭代依次构造之后n-1层关系联接最优解

    • 左深联接树方式:将第k-1层每个关系同第1层关系联接
    • 紧密树联接方式:将第m(m > 2)层每个关系同第k-m层关系 联接

    left_deep_tree_bushy_tree

遗传算法

遗传算法:模拟自然界生物进化过程,采用人工进化的方式对目标 空间进行搜索

  • 本质是高效、并行、全局搜索方法
  • 能在搜索过程中自动获取、积累有关搜索空间的知识,并自适应 的控制搜索过程以求的最佳解

思想

  • 将问题域中可能解看作是染色体,将其编码为符号串的形式
  • 对染色体群体反复进行基于遗传学的操作:选择、交叉、变异
  • 根据预定目标适应度函数对每个个体进行评价,不断得到更优 群体,从中全局并行搜索得到优化群体中最优个体

MySQL

代价模型

  • 因为多表联接顺序采用贪心算法,多个表已经按照一定规则排序 (可访问元组数量升序排序)
  • 所以MySQL认为,找到每个表的最小花费就是最终联接最小代价

贪心算法

贪心算法:认为每次连接表的连接方式都是最优的,即从未联接表中 选择使得下次联接代价最小者

  • 多表排序一般为

    • 常量表最前
    • 其他表按可访问元组数量升序排序
  • 贪心算法得到的联接方式都是最优的

    • 则每次联接主要求解要联接表对象的最佳访问方式
    • 即每次代价估计的重点在于单表扫描的代价
  • 求解结束后,局部最优查询计划生成

    • 得到左深树
    • 最初始表位于最左下端叶子节点处

优化方案

以下分别从查询代价估计、多表连接顺序搜索算法给出方案

查询代价估计

  • 考虑在现有代价模型上增加网络通信开销

  • 在现有直方图估计选择率基础上,增加选择率估计方法

    • Parametric Method:参数方法,使用预先估计分布函数 逼近真实分布

    • Curve Fitting:曲线拟合法,使用多项式函数、最小 标准差逼近属性值分布

多表连接顺序搜索算法

考虑到动态规划算法随着联接表数量增加时,计算代价过于庞大, 可以考虑引入其他算法优化多表连接顺序

  • 遗传算法
  • 退火算法
  • 贪心算法
  • 遗传算法
  • 退火算法
  • 贪心算法