Algorithm Design

Algorithm DesignChapter 0. DefinitionsChapter 1. Algorithms of Numbers1.1 数字基本算数1.1.1 加法1.1.2 乘法 & 除法1.2 模运算1.2.1 上的加法、乘法1.2.2 上的幂1.2.3 Euclid's Greatest Common Divisor1.2.4 Euclid GCD 算法的延伸1.2.5 上的乘法逆元1.3 素性测试1.3.1 Definitions1.3.2 随机生成素数1.4 密码学1.4.1 对称加密1.4.2 非对称加密1.4.3 证书Chapter 2. 分治算法2.1 更快的乘法2.2 更一般的分治递推分析2.3 应用:比较排序的时间下界2.4 应用:中位数算法再讨论2.5 应用:矩阵乘法2.6 应用:计数逆序Chapter 3. The Algorithms using Fast Fourier Transform3.1 Revisit Complex Number3.2 Begin with The Production of PolynomialsChapter 4. Graph4.1 Revisit: DFS in Graph4.2 无向图的连通分量4.3 有向图的连通分量4.4 有向无环图(DAG)4.5 Revisit: 有向图强连通分量4.6 BFS in Graph4.7 正权图中两点间的单源最短路径4.8 含负权无负环图中两点间的单源最短路径补充:负环问题 & 最简最短路4.9 含负权的 DAGs 中两点间的单源最短路径Chapter 5. Greedy Algorithms5.1 Minimal Spanning Tree (MST)5.2 Set Cover(集合覆盖问题)5.3 贪婪算法何时失效:Coins ChargingChapter 6. 动态规划6.1 最长子序列问题 I:最长递增子序列6.2 编辑距离6.3 背包问题6.3.1 01-背包问题6.3.2 完全背包问题6.3.3 多重背包问题6.3.4 问题变种:Optimal Solution for Coins Charging6.4 最长子序列问题 II6.4.1 最长公共子序列6.4.2 最长公共子串6.4.3 Quiz: 最大连续子序列和6.5 3-Partition Problem6.6 跳石子6.7 Shortest Reliable Path (Lite) & Revisit Bellman-Ford Algorithm6.8 Revisit Shortest Paths Problem: Floyd-Warshall Algorithm6.9 Traveling Salesman Problem (TSP)6.10 Independent Sets in TreesChapter 7. Linear Programming7.1 Definitions7.3 Integer Linear Programming7.4 Dual Program7.4 Application: Shortest Path7.5 Simplex Algorithm7.5.1 算法内容7.5.2 Corner Cases7.5.3 运行时间7.6 最大流与最小割Chapter 8. NP Problems如何应对 NP 问题?梯度下降Hopfield Netural Network

Chapter 0. Definitions

介绍算法的时间复杂度(应该在数据结构中有所接触):

Prerequisitesf(n)=Θ(g(n))c1>0,c2>0,n0>0. s.t. n>n0, 0c1g(n)f(n)c2g(n)n(n0,+), n0>0, 0f(n)c1g(n)(c2c1)g(n)n(n0,+), n0>0, M>0, |f(n)g(n)c1|M⟹̸c>0,limn+f(n)g(n)=c

 

Chapter 1. Algorithms of Numbers

看两个问题:

事实上,第一个问题相当复杂,第二个问题却相当简单。

 

b 进制下 k 位的最大数是 bk1;所以 b 进制下表示 N,需要 logb(N+1) 位数;

由于在 O 表示法下常数倍可以被忽略(注意 b>1),因此 N 的表示数(表示符号)的位数的复杂度为 O(logN)

1.1 数字基本算数

1.1.1 加法

引理:任意 3 个单数位数字之和(不论进制),结果最多只有 2 个数位长。

由引理,我们讨论加法运算的时间复杂度:

假设 x,y 位数均为 n,单独计算某位的时间是固定的,因此基本操作数为 c0+c1n,时间复杂度:O(n)(读取就需要 n 次,因此是 optimal 的);

1.1.2 乘法 & 除法

对于乘法一般性的讨论:

假设 x,y 位数均为 n,那么按乘法式可知,中间结果共 n 行,错位累加后长度 2n;累加的复杂度 O(n)×(n1)O(n2)

Al Khwarizmi 算法:ab 计算时,a 截断除 2、b 乘 2,直至 a 为 1,去掉 a 为偶数的行,将剩下行的 b 累加即为 ab 的值;

使用另一种算法:

xy={2(xy2), y is even,x+2(xy2), y is odd

使用了 2 进制移位运算的思想,递归调用次数为 n(或者说 logy,这里以数位 n 来衡量)次(将 y 折半的次数),每次递归中:一次右移、一次奇偶测试、一次左移、可能的一次加法,因此时间复杂度 O(n2)

考虑除法?

1.2 模运算

本节需要线性代数、抽象代数相关知识。

xymodNN divides (xy)

几种理解方式:

  • xy 能被 N 整除;

  • x 除以 N 会余下 y,或者说,xN 等于 y

  • xyN 同余(一个等价类);

我们讨论一个 mod N 的系统,按照 mod N 运算能将整数集划分为 N 个等价类(数学上可以证明:自反性、对称性、传递性),于是在讨论 mod N 的代数系统中,我们总是选取 0N1N 个对象作为这个代数系统的元素,因为有定义:

对于定义在集合 G 的一个二元运算符 及集合自身构成的代数系统,满足:

这个代数系统就被称为 “群”(Group)。

特别地,当 G={0,1,,N1},运算符 为 mod 时,这个系统符合上述定义,并且具备一些良好性质,数学上一般还称之为:

最小非负完全剩余系(Minimal Non-negative Complete Residue System),记作 ZN

我们接下来定义在这个系统 ZN 中的基本四则运算、幂运算、GCD 运算,充分讨论其算法和性质,为后面的计算机算法研究提供理论基础。

 

我们先在整数集上观察模运算的性质:

上面的性质说明:在一系列基本算数运算过程中,模运算可以在任意阶段进行:

2345(25)693269169mod31

也就是说在整数集上:

不讨论除法,而是讨论乘法逆元!

 

于是我们定义在 ZN 上的加法运算、乘法运算、乘法逆运算、负运算(可以由加法运算表示)、幂运算(可以由乘法运算表示):

其中注意乘法逆运算的定义:

aZN, s.t. ax1modNx1amodN

含义就是,如果在 ZN 上的元素 x,存在另一个元素 a 使得二者的乘法运算结果为单位元,则称 ax 的乘法逆元,注意没有说明乘法逆元和元素是一一对应的,也就是说可能存在一个元素没有乘法逆元。我们后面具体讨论这个问题。

注:ZN 上的等式一般写在 mod N 标记的左边,并且用等价符号表示。也就是说,在一般整数域上 axmodN=1 可以写成 ax1modN

1.2.1 ZN 上的加法、乘法

x+ymodN 的值是两个 [0,N1] 的数之和,最长不过 2(N1)

整个计算过程包括:一次加法、一次可能的减法,因此时间复杂度 O(n)nN 的位数;

xymodN 的积最大是 (N1)2,因此最大占用 2n 位长。两个 2n 位数字之积,按照前面的分析,时间复杂度仍然 O(n2)

模的除法会涉及 0 的情况,比较复杂,后面推导;

1.2.2 ZN 上的幂

xymodN=(xmodN)y,因此由替换准则考虑重复乘以 xmodN 进行运算,可以减小运算位数:

xmodN 然后算出 x2modN=(xmodN)2<N,再算出 x3modN=x2modNxmodN<N(两个小于 N 的数相乘),依此类推,需要乘 y 次;

也就是说,xymodN 可以通过计算 y2n+1(位数 - 数大小之间的指数近似)次,最多 N 位的乘法来解决,其中 ny 的位数。

这是指数时间复杂度的算法,不能这么解决问题。

我们想到可以每次计算之后不止乘一次,而是平方一次,这样能大大加快计算速度:

xmodN 然后 x2modN=(xmodN)2<N 然后 x4modN=(x2modN)2<N,直至 x2logymodN;每一步是模之积的运算 O(n2),但这次只需要乘 logN 次即可,所以时间复杂度 O(n3)

这样可以列出公式:

xy={(xy2)2, y is even,x(xy2)2, y is odd

1.2.3 Euclid's Greatest Common Divisor

定义 gcd(x,y)x,y 的最大公因数;

特别地,gcd(x,0)=x

Euclid 发现,对于正整数 x,y,如果 xy,那么 gcd(x,y)=gcd(xmody,y)

两个正整数的最大公因数,为其中一个较大的数模另一个数的余数,与另一个数的最大公因数。

要证明上式,只需说明 gcd(x,y)=gcd(xy,y),然后重复将 x 减去 y 即可;

证明等式,将其分解为两个不等式:

如何估计这个算法的时间复杂度?

引理:

abamodb<a2

证明引理分 ba2b>a2 两种情况:

ba2 时,显然有 amodb<ba2

b>a2 时,则显然有 amodb=ab<a2

所以可以知道,gcd(a,b)=gcd(amodb,b) 每次迭代会减少较大的一个数一半以上的值,因此迭代次数 O(logN)=O(n),每轮迭代计算一次取余 O(n2)

综上,GCD 算法时间复杂度 O(n3)

1.2.4 Euclid GCD 算法的延伸

我们知道如何求两个正整数的最大公因数。考虑新的问题:如何验证两个正整数 a,b 的最大公因数为 d

仅验证整除性肯定是不够的,所以我们再引入一个引理

如果 d 整除 ab(公因数的定义),且同时存在整数 x,y(不都是正数)使得 d=ax+by 成立,那么一定有 d=gcd(a,b)

证明引理,将等式拆成不等式:

显然由公因数的条件得:dgcd(a,b)

另一方面,由于 gcd(a,b)a,b 的公因数,又由于 d=ax+by,因此 gcd(a,b) 一定能整除 d,即 gcd(a,b)d 的因数,故 gcd(a,b)d

得证;

现在讨论 “扩展 Euclid 算法”:

再引入一个引理

对任意正整数 a,b,利用扩展 Euclid 算法可以求得 d,x,y,使得 d=ax+by=gcd(a,b) 成立;

证明上面的引理,观察到这是个递归算法,因此可以考虑从递归终止条件归纳证明。

借助 b=0 的信息,使用归纳法证明:

  • (奠基)当 b=0 时,显然(?)

  • (假设)假设当 b=k 时(kN+),算法是正确的;

  • (归纳推理)则注意到 amodbb,因此可知调用返回的 gcd(b,amodb)=bx+(amodb)yx,y 是正确的;

    amodb=aa/bb,则 d=gcd(a,b)=gcd(b,amodb)=bx+(amodb)y=bx+(aa/bb)y=ay+b(xa/by)

    这说明 d=ax+by 其中 x=y, y=xa/by

人手工应该如何计算 “扩展 Euclid 算法” 的答案呢?

为了找到 x,y 使得 ax+by=d

  1. 用 Euclid 计算出 gcd(a,b)=d,保留过程数组 (d,0),(kd,d),,(b,amodb),(a,b)

  2. 先用 (d,0) 表示 dd=d+0,然后用 (kd,d) 表示 dd=d+(kdkd),核心思想是,用 (an,bn) 反复替代向量 (an1,bn1) 中较小的 bn1 来线性表示 d

  3. 最终会得到 d=xa+yb

值得注意的是,扩展 Euclid 算法涉及数论中的重要定理:

裴蜀定理:设 a,b 为正整数,则关于 x,y 的方程 ax+by=c 有整数解当且仅当是 gcd(a,b) 的倍数。

此外,扩展 Euclid 算法可以应用于:

注,由裴蜀定理可以证明以下结论:

方程 ax+by=gcd(a,b) 的特解 (x0,y0) 可以由扩展 Euclid 得到(或者瞪眼法),通解可以写成:

{x=x0+bgcd(a,b)ky=y0agcd(a,b)k,kZ

 

有了这个方法,我们可以讨论 ZN 的乘法逆元,及其算法时间复杂度了。

1.2.5 ZN 上的乘法逆元

回顾在 1.2 开头对于 ZN 系统的乘法逆运算的定义,有:

x 是关于 amodN 的一个乘法逆元 ax1modN

理解为,在整数域上,对于一个整数 a,可能存在另一个数 x,使得 axN 除后余 1;

或者说,在 ZN 系统中,两个元素的乘法结果为 1;

引出可以被数学证明的定理

 

 

总结:

总结两个定理:

 

1.3 素性测试

1.3.1 Definitions

引入 费马小定理

If p is a prime  a[1,p), ap11modp

证明:

S 为模 p 的所有非零整数的集合,即 S={1,2,,p1}

可以证明,有序集 {a|aS}{iamodp|iS} 形成一个简单排列变换。

也就是说,iiamodp 一定不相同,并且不重复、都位于 S 内;

因此可以将两边的等式相乘:(p1)!=(p1)!ap1modp

再由 p 为素数可知 (p1)!p 互素,因此消去 (p1)! 得:

ap11modp

我们用费马小定理就能判断素数了吗?可惜不行,因为这不是充分必要条件。

事实上存在一些数 p 是合数,但是能通过费马测试。但是我们认为这种情况出现的概率比较小(但这些数是无限个的);

也就是说,对所有与 p 互素的 a,合数 p 是能通过费马测试的(存在一个 a 满足)!这种情况被称为 Carmichael 数,它可以被其他算法剔除。

如果 ap 不互素,则 ap10modp,肯定无法通过费马测试。因此这里只需要看与 p 互素的 a

因此,在不考虑 Carmichael 合数的情况下,所有其他合数 N 都满足:

对与 a 互素的 NaN11modN,那么对 a<N 至少一半的可能取值,N 将无法通过费马测试(有一半及以上的例子会破坏任意性);

证明:

假设某个 a 使得 aN11modN,那么对于任意通过了费马测试的 bbN11modN)都有 ab 无法通过费马测试:

(ab)N1aN1bN1aN11modN

并且:对所有给定的 a,当 b 不同时,ab 对应的 abmodN 总不会相同(证明同费马定理,如果 ai=ajaimodp=ajmodp,则可知 ijmodp,其中 i,j0,与 b 的假设矛盾)。

所以存在 bab 一一映射(能通过 -> 不能通过),又多出来一个 a 不能通过,因此:非 Carmichael 合数 N 一定有至少一半无法通过费马测试的 a

所以:

我们只要重复 100 次,整个算法对于所有的 N 只有不超过 12100 的概率出错。也就是说能保证绝大多数情况的正确性。

补充:Monte-Carlo Algorithms & Las Vegas Algorithms

  • Monte-Carlo Algorithms:时间复杂度确定,但算法正确性是个概率(例如这里的 prime test);

  • Las Vegas Algorithms:时间复杂度不确定,但算法正确性确定(例如 quicksort);

 

1.3.2 随机生成素数

引入 Lagrange 素数定理

π(x) 为不大于 x 的素数个数,则:limx+π(x)x/lnx=1

这说明:

  • 素数有无限多个,只不过越大越稀疏而已;

  • 随机的 n 位数字 N(回忆 logNn),是素数的概率为 P=1ln2n1n

我们根据上面的理论依据设计一个生成 n 位素数的算法:

  1. 随机生成一个 n 位数 N

  2. N 进行素数测试,如果成功则输出;失败则重复上述过程;

由于对于随机整数 N 中的素数概率为 1n,因此平均情况算法会在循环 O(n) 次停止,时间复杂度 O(n)

 

1.4 密码学

基本设定(roles):Alice & Bob 传递信息,不希望 Eve(偷听者,仅尝试在不被发现的情况下获取 Alice 和 Bob 交换的信息)和 Ida(中间人,它可以打破通信的规则,例如伪造身份等等,也可以做 Eve 的窃听行为)知道消息;

如果 Alice 直接向 Bob 发送原封不动的消息(称为 “明文”),那么 Eve 可以直接监听到、Ida 还可以直接进行内容篡改。

1.4.1 对称加密

所以可以采用一种简单的做法来尝试防止 Eve 和 Ida 的行为:

形象理解就是一把暴露在外的锁(e(x),d(x))和钥匙 r

Sidebar:按位异或 与 AES 算法

按位异或(bitwise exclusive-or)就是一种选择。

dr(x)=er(x)=xr

dr(er(x))=(xr)r=x0=x

因为它满足一些优秀性质:

  • 是一个双射函数,其逆函数就是自身;

  • 能保证 er(x)x 的关系不大。只要密钥 r 是随机的,那么 x 的每个二进制位有 50% 概率不变,有 50% 概率相反;

  • 硬件计算很快,性能好;

但这个函数的 r 参数不能重复使用相同的随机数。为什么?因为重复的 r 会让窃听者获取和明文相关的消息:窃听者第一次拿到 xr,第二次拿到 yr,那么就知道 xy=xr(yr);这个 xy 就会间接地泄漏信息。

假设通信传递的是某个重要选举的票选信息,而每一位代表一个票选意见(比如是 1 就选 A,是 0 就选 B)。那么 xy 中的 0 越多,说明选举群众的共识度越高,有更多的人选择类似。

为了解决这个问题,人们基于按位异或设计了一种更强的对称加密算法:AES(Advance Encryption Standard),它的好处是:

  • 虽然算法公开,但可以重复使用密钥;

  • 计算效率合适;

  • 目前没有比 brute-force 更好的办法来破解(但也没有证明绝对不可破解);

这种方式被称为 “对称加密”(Symmetric Cryptography),即加密、解密用的密钥相同。

er(x)dr(x)r 参数就是加密用的密钥,而 erdr 本身是公开的算法

对称加密特点

但对称加密也有问题:在 Alice 和 Bob 第一次交流的时候,肯定需要约定一个密钥 r,这样 Eve 直接在信道上监听就能拿到解密函数。

也就是说,对称加密使用的密钥目前还没有办法保证不被网络上不可信任的设备截获。

因此需要更强大的措施来防止窃听者、中间人获取密钥。

1.4.2 非对称加密

假设我们通过某种方法获得了一个神奇的算法:

对,就这一个更改就让 Eve 没办法了:

因为虽然 b 解密密钥是公开的,但是,一旦有中间人 / 窃听者要用 b 解密后,由于 a 永远不在网络上传播(从生成开始一直保管在 Alice 那),所以他们将无法把消息加密还原回去,这样 Bob 发现消息不对就可以及时发现问题。

这个公开的 b 就被称为公钥(Public Key),私有的 a 就被称为私钥(Private Key),这种加密方式被称为 “非对称加密”(Asymmetric Cryptography);

而私钥加密、公钥解密的过程被称为 签名(signing)

Sidebar: RSA 算法

那么上面的 “神奇算法” 是如何实现的呢?数学家想出一个方法:

  1. 任取两个相当大的素数 p,q,计算 N=pq

  2. 找到一个和 (p1)(q1) 互素的数 e,则 f:xxemodNZN 上的双射,作为我们的加密函数;

  3. 解密相当简单:找到 e 关于 mod(p1)(q1) 的乘法逆元,记为 d,则对 ZN 上的任一元素 x,都有:(xe)dxmodN

证明:由于第 3 条蕴含第 2 条,所以下面仅证明第 2 条:

ed1mod(p1)(q1) ed=1+k(p1)(q1) (xe)dx=xedx=x(xk(p1)(q1)1)

注意到 xk(p1)(q1)1 可以进行因式分解:xk(p1)(q1)1=(xp11)(xq11)Pk

再由费马小定理:xp11modpp 是素数),因此对于素数 p,qxk(p1)(q1)1modpq=0;即:xedxmodN=0,或者说 xedxmodN 得证;

也就是说,(e,N) 是公钥,d=e1mod(p1)(q1) 是私钥,f(x)=er,N(x)=dr,N(x)=xrmodN 是加/解密函数。

由我们前面讨论的知识,随机生成两个素数时间复杂度 O(n),加密 / 解密是 ZN 指数 O(n3)

这个算法安全的前提是,没有人能快速地从 N 获得 p,q(即:素因数分解是难问题)

但是非对称加密也有缺陷:无论是生成公私钥,还是加密数据,计算都相当复杂、性能不佳,不适合用来传递大规模数据。

于是,一般可以采用这种措施:非对称加密 先用来传递对称加密的密钥,然后之后的通讯就使用对称加密来通信。这样既利用了对称加密的特性(计算量小、不知道密钥的情况下基本没法破解),又利用了非对称加密的强大安全性。

1.4.3 证书

现在 Eve(窃听者)彻底没法窃听到任何有效数据了,但是 Ida 还有办法。

Ida 可以拦截 Alice 和 Bob 间的所有流量,然后在 Alice 向 Bob 第一次发送密钥时:

这下,Alice 和 Bob 在不知情的情况下,把消息全都发给了 Ida,Ida 既掌握解密的公钥、所有消息,还能不被 Alice 和 Bob 发现。

Ida 在这里被称为 “中间人”,而这种行为被称为 “中间人攻击”(Middle-in-the-man Attack,MITM);

这样是不行的。不过往好处想,还是之前的说法,只要我们保证第一次交换对称加密密钥的过程是安全的不就行了?这里的问题在于 Ida 可能会篡改公钥

那么有什么办法是在暴露公钥的情况下,还能防止公钥被篡改的?

这理论上肯定没法只由 Bob 和 Alice 解决,还需要外界的帮助。于是人们引入了 “第三方公证” 的机制:

人们需要设立一个第三方机构,确保它不会与中间人勾结。第三方机构本身事先生成一对公私钥,然后:

由于 Ida 无法伪造第三方机构,因此最外层的密钥没法突破,因此也没法得到里面的数据进行中间人攻击了。

这里,第三方机构(证书签发机构)提供的公钥被称为 “证书”(Certificate)。Alice 使用第三方公钥加密、Bob 将加密信息给第三方解密的过程,就称为 “证书签名”;

目前,这套机制能够完全防御 Eve(窃听者)、Ida(中间人 / 攻击者)对信息的窃听和篡改。当然,证书 + 非对称加密 + 对称加密的整套机制被应用在了 SSL/TLS 当中,为 HTTPS、SSH 等协议重要通信场合提供全面的保护。

 

Chapter 2. 分治算法

2.1 更快的乘法

引理:对任意正整数 n,总存在一个 n 使得:nn2n,其中 n 是 2 的非负次幂;

高斯发现,一个复数乘法:(a+bi)(c+di) 可以只用 3 次乘法就能完成,因为 bc+ad=(a+b)(c+d)acbd

也就是说乘法次数从 4 次变为 3 次;这对于单次计算来说没什么(常数优化),但在递归的用法时会获得极大地提升:

我们把将两个数 x,y 按照 2 进制数位对半拆开:

x=2n/2xL+xR,y=2n/2yL+yR

那有人会说,这不还是这么算吗:xy=2nxLyL+2n/2(xRyL+xLyR)+xRyR,这样递归的时间复杂度:

T(n)=4T(n2)+O(n)n2 位数乘法 T(n2)n2 位数加法 O(n))可以解出 T(n)=O(n2),不是和原来的算法一样吗?

不过你再考虑一下之前高斯提出的优化呢?计算 xLyL,xLyR,xRyL,xRyR 看似要 4 次乘法,实则 3 次乘法:

xRyL+xLyR=(xR+xL)(yL+yR)xLyLxRyR

这样递归时间复杂度就变为:

T(n)=3T(n2)+O(n)

注意,(xR+xL)(yR+yL) 的位数可能达到 n2+1 位,但是在推导中无关紧要,所以写成 T(n2)

想象成一棵三叉树(因为 3 次乘法),每层中结点的 n 规模比上一层小 12,所以层数大约 O(log2n)

在第 k 层,共 3k 个结点、每个结点的规模 n2k,也就是说每层的时间复杂度约 O(n)(32)k,总时间复杂度:

T(n)=k=1log3nO(n)(32)k=O(n)(321(32)log2n132)=O(n)(3(32)log2n3)O(n)((32)log2n1)O(nlog23)

注:指数换底 3log2n=eln3log2n=eln3lnn/ln2=elnnlog23=nlog23

所以 时间复杂度为 O(nlog23)O(n1.59)

事实上还能做得更好(快速傅里叶变换),以后介绍。

2.2 更一般的分治递推分析

在解决 O(n) 规模的问题时,总是先递归分解为 a 个规模为 n/b 的子问题,然后需要 O(nd) 的时间将 a 个这样的子问题合并;

a 为分支因子,b 为规模缩放因子;

因此这样一般的问题的时间复杂度计算法为 T(n)=aT(nb)+O(nd),由数学推导可知大师定理(master theorem):

T(n)={O(nd),if d>logbaO(ndlogn),if d=logbaO(nlogba),if d<logba

推导过程可以直接假设 nb 的幂,因为无论 b 的某个整数幂与 n 相差再大,总不会超过 nb(也就是 minkN|bkn|nb),

因此我们可以直接忽略 nb 造成的舍入影响。

证明:

  • 树高 logbn,第 k 层每个子问题规模 nbk,第 k 层共 ak 个子问题,第 k 层总体规模 O((nbk)d)

  • 问题总体规模:T(n)=m=1logbnamO(ndbmd)=O(nd)m=1logbn(abd)m

注意到 abd 和 1 的大小需要分类讨论后才能代入等比级数求和:

  • abd>1,即 d<logba,此时 T(n)=O(nd)q1qs1q=1q1O(nd)((abd)logbn1)O(nlogba)

  • abd=1,即 d=logba,此时 T(n)=O(ndlogbn)O(ndlogn)

  • abd<1,即 d>logba,此时 T(n)=O(nd)q1qs1q=q1qO(nd)(1(abd)logbn)O(nd)

记忆方法:dlogba 谁大以谁为幂;一样大则 ndlogbn 都有;

2.3 应用:比较排序的时间下界

我们首先讨论归并排序。

算法描述:对于长度为 n 的数组排序,n=1 为终止条件(直接返回);n>1 时,将数组大致平分,对每一部分递归,对二者的结果进行 merge(),返回 merge 的结果;

显然这里 a=2,b=2,这里重要的是 merge(也是子问题合并)算法的时间复杂度;

merge 两个有序数组的方法是,用两个指针放在数组第一元素位置并比较数,较小(如果从小到大排)的先放到最终数组中,并将指针后移;直至一方的指针遍历结束,另一方直接将剩余部分复制到结果数组中,我们认为复杂度 O(n)

这里看到 d=1=logba,因此由 master theorem,时间复杂度 O(nlogn)

在上升到更一般的层面:所有的基于比较的排序都可以看成是一棵比较树,这一定是一棵二叉树,因为一次只能比两个数(以一个 3 个数间最少次数比较的排序为例):

所以,这一定是一棵有 n! 个叶结点的二叉树。树高就是比较次数,所以我们希望树高越矮,比较次数越少,基于比较的排序时间复杂度更低;

以相同结点数量下最矮的二叉树——满二叉树为例(事实上达不到),树高 log2(n!),所以基于比较的排序时间复杂度 O(logn!),可以由数学证明 O(logn!)O(nlogn),两种方法:

  1. 证明 nn/2n!nn

  2. 使用斯特林公式证明:limn+n!2πn(ne)n=1

2.4 应用:中位数算法再讨论

我们讨论过 Top-K Problem:理论最小时间复杂度 O(n)

一开始是小顶堆出队: O(n+klogn),但在中位数方面退化为 O(nlogn)

本章我们讨论分治策略。说到分治策略解决 Top-K Problem,就不得不提快选算法。

随机 pivot 的快选算法:O(n) 平均、O(n2) 最坏,但最坏可能性很小。

我们再来详细分析它的时间复杂度。

假设我们认为,如果选到 25% ~ 75% 分位的数是好的选择。因此随机选 pivot “是好的” 的概率为 50%;

另外,概率学认为平均 2 次随机选择会选到 “好的” pivot。证明:

设 “选到好的 pivot” 作出选择次数的期望是 E,因此:

E=12+12(12×2+12(12×3+12()))=k=1k2k;故 12E=k=1k2k+1E12E=k=112k=1,得出 E=2

所以在选择好的 pivot 的情况下,我们每两次最少能排除掉 14 长度的数组内容,因此:T(n)T(34n)+O(n)

根据 master theorem:其中 b=43,a=1,d=1d>logba,故 T(n)=O(n)

2.5 应用:矩阵乘法

20 世纪,有人发现 2 x 2 的矩阵乘法计算只需要 7 次乘法(原来需要 8 次)。

这看起来没什么,不就是常数时间的优化吗?你错了!因为矩阵乘法可以分块,这意味着对于 n×n 矩阵的乘法,我们可以通过分块(n2×n2),也利用递归算法,列式:

T(n)=7T(n2)+O(n2)

b=2,a=7,d=2d<logba,因此由主定理得:T(n)=O(nlog27)O(n2.81),相较于原来的 O(n3) 降低了不少。

2.6 应用:计数逆序

考虑一个问题:

为了方便讨论,我们以一个顺序序列作为基准(例如 1~N),给定一个输入序列,问题转换为判断这个输入序列和基准序列间的计数逆序。

普通思路是直接先排序再计数,时间复杂度 O(n2)(要知道所有元素的逆序次数),但是多做了很多事,必然可以优化。

现在发现,计数逆序的求解也可以通过分治法完成。考虑分治法如何 combine two subproblems:

对两个子问题序列 A,B,如果 aA, bB,那么如何计算 (a,b) 间的计数逆序?

如果 A,B 都无序,那么就情况就比较头疼:遍历的时间复杂度甚至可能不如不用分治法。

因此考虑如果 AB 各自有序,那么问题就比较简单,因为只需要把 bA 中作二分查找、aB 中二分查找就行。得到的逆序数累加到数组总的计数逆序中;

但子问题合并的时间复杂度是 O(nlogn)T(n)=2T(n2)+O(nlogn)

能不能更简单?可以。我们可以在 A,B 归并的时候,边归并、边计算逆序。过程类似 merge sort,其方法如下:

由于这是从头到尾读一遍而已,和 merge sort 的情形一致,因此新的合并复杂度 O(n)

所以,问题整体可以用分治的排序来顺便计算,时间复杂度 T(n)=2T(n2)+O(n),即 O(nlogn)

这种算法的名称就称为 sort-and-count algorithm;

 

Chapter 3. The Algorithms using Fast Fourier Transform

3.1 Revisit Complex Number

复数的基本运算

3.2 Begin with The Production of Polynomials

考虑一个问题,现在我们希望计算两个两个同次多项式之积:

Ad(x)=a0+a1x++adxdBd(x)=b0+b1x++bdxd

答案会是这种形式:C(x)=c0+c1x++c2dx2d,所以我们一般可以由相关系数(coefficients)计算:

ck=aobk+a1bk1++akb0=i=1kaibki

假设乘积和加法数字不大(和前一章讨论以数位为 n 不同,我们认为参与运算的数字不大),常数时间完成,因此计算 ck 需要 O(k) 步,计算所有 ck2d+1 个)则需要 Θ(d2) 的时间复杂度;

能否简化这个时间复杂度?

再注意到数学上的事实:d+1 个点就能唯一确定一个 d 次的多项式,因此我们有两种多项式表示法:

Ad(x)Bd(x) 的乘积式 C(x) 通常需要 2d+1 个点(即 2d+1 个值),注意到 C(z)=A(z)×B(z),因此,多项式的乘法在值表示法上只需要线性时间就能计算

 

所以我们想,更简单的算法应该是这样的:

  1. (选取 & 计算)分别为 A,B 选取 d 个点,并计算它们的值:Θ(n2)

  2. (相乘)利用选取的点,计算在 C(x) 上的点(A(xi)B(xi)):O(n)

  3. 插值)将这些 C(x) 上的点恢复到相关系数表示法:O(?)

现在我们不清楚插值的时间复杂度能到多少,先放在一边,不过我们可以使用特殊的方法来优化第一步 Θ(n2)O(nlogn)。这个特殊的方法就是分治 + 快速傅里叶变换(Fast Fourier Transform,FFT)。

为了加快从系数表示法到值表示法的计算速度,我们可以选取一些特殊的点:正负点对(±x0,±x1,,±xn/21),因为这么选 A(xi)A(xi) 计算就会有很多重复:

这么优化是常数时间的,还远远不够。

我们还要借鉴分治法的思想,我们发现:

A(x)=Ae(x2)+xAo(x2)

其中 Ae 包括了 A 偶数系数的多项式、Ao 是奇数项的系数的多项式,二者的次数不超过 n/21

也就是说,通过把奇数次项提出一个 x,就将计算 n 次多项式的值转换为计算两个次数不大于 n21 的多项式的值:

现在给定正负点对 ±xi

A(xi)=Ae(xi2)+xiAo(xi2)A(xi)=Ae(xi2)xiAo(xi2)

这样 Ae(xi2)Ao(xi2) 结果可以复用。

假设我们能一直这么递归下去,那么:

讨论 ±xin 个点的 A(x) 取值,就可以分解成两个 Ae(xi2),Ao(xi2) 次数不大于 n21 的多项式在 xi2 只有 n2 个点的子问题。

xi2 常数时间、剩余的相乘、相加都是常数时间,然后对每个 i 重复一次(共 n2 次)。也就是说,拼接子问题的时间复杂度为 O(n),计算的时间复杂度为:

T(n)=2T(n2)+O(n)

由 master theorem 可知 T(n)=O(nlogn)

可惜的是,我们并没有办法一直这样持续下去,因为在实数范围内 xi2 一定是正数,所以在递归到第二层的时候,就没法找到两个非零的数的平方还是相反数的例子(也就是找 xi2=xj20);

这个时候就需要用到复数了:

我们取的 xi 需要是 zn=1 解集中的点,而且 n 满足 n2d+1 、是 2 的整数次幂(n=2k, kZk 需要尽可能小)。为什么需要这么做?我们先顺着向下看。

无论如何,现在我们只需要输入一个满足上面条件的 n,也就是 ω=e2πi/n,它代表的解集 1,ω,ω2,,ωn1 就能给 A 提供 2k2k2d+1)个可供递归的点;它们在递归时,由于 zn=1 的解的特性,ωn/2+j=ωj,因此在 1,ω,ω2,,ωn1 中,互为相反的复数成对出现;

而在 1,w2,ω4,,ωn/21 中,相反复数仍然成对出现,如图:

现在我们描述这个 FFT 算法:

注意到,在某层递归中,计算 ωnkωnk+n/2 就能实现计算量的节省:

A(ωnk)=Ae(ωn2k)+ωnkAo(ωn2k)

和计算:A(ωnk+n/2)=Ae(ωn2k+n)+ωnk+n/2Ao(ωn2k+n)=Ae(ωn2k)ωnkAo(ωn2k)

可以成对处理。

在递归下去的 Ae(ωn2k)Ao(ωn2k) 也可以遇到同样的情况,直至 ωnn=1 的输入,递归终止。

这样,第一步的 “选取计算” 环节已经被我们成功降到了 O(nlogn)。现在我们来讨论相反的过程:插值。

我们注意到,如果得到了结果多项式的值表示,要反过来求系数,就是在解这个线性方程组:

[A(x0)A(x1)A(xn1)]=[1x0x02xon11x1x12x1n11xn1xn12xn1n1][a0a1an1]

注意到中间的矩阵是范特蒙德矩阵,记为 M,可以通过线性代数的知识了解到:只要 x0xn1 是互异的数,那么 M 就是可逆的。又由于我们取的复数点肯定是不重合的,因此下面的步骤就转换为了求 M1

为什么我们能大胆求逆?因为一般矩阵高斯消元是 O(n3),但对范特蒙德矩阵而言只需 O(n2),并且再次通过快速傅里叶变换,还能简化成 O(nlogn)

为什么这么说?回想我们之前代入的 1,ω,ω2,,ωn1,现在代入 M

[11111ωω2ωn11ωjω2jω(n1)j1ωn1ω2(n1)ω(n1)(n1)]n×n

这个矩阵中 aij=ωij,注意到它有个相当优秀的性质:

而这个矩阵正是快速傅里叶变换,在变换向量 A(多项式系数向量)时的变换矩阵。这个矩阵对应的线性变换被称为傅里叶变换(Fourier Transformation),人们将其列向量称为傅里叶基(Fourier Basis)

这样,由 MHM=nI 可知:Mn1=1nMH=1nM,我们惊喜地发现,计算:

M[A(x0)A(x1)A(xn1)]

和前面的 evaluation 的计算方法不是几乎一致吗!前面给 ω 生成点,A 向量则作为系数,然后计算 n 个点;现在反过来 ω 输入是为了生成系数矩阵、A(xi) 向量则是给定的点;

在代入前面的算法之后在除以 n(乘 1n)就得到了插值的结果,理所当然时间复杂度也是 O(nlogn)

 

Chapter 4. Graph

4.1 Revisit: DFS in Graph

首先定义一些本章会用到的方法:

 

4.2 无向图的连通分量

注意,每次 DFS 后 pre 和 post number 不会重置,各个顶点的 pre/post number 只会增大;

 

4.3 有向图的连通分量

我们已经了解,对有向图 DFS 会生成深度优先森林(DFS Forest)。

于是在有向图、DFS Forest 的定义的基础上,引入新的定义(定义有向图 G):

我们将结点生命周期考虑进来,针对一条有向边 (u,v),可以这么分类:

能否证明有向图 G 中的边只有这些分类?

由上一节的证明可知,如果要有其他类别,则一定是 [u]u[v]v

假设生命周期表示为 [u]u[v]v,那么由于 ]u 先于 [v,并且 u,v 间存在有向边 (u,v),因此 (u,v) 不是树边;再由 u,v 的生命周期可知,u 既不是 v 的子孙结点,又不是 v 的祖先结点;而这与存在有向边 (u,v) 矛盾,因此假设不成立,有向图 G 中只有可能有以上几类情况。

 

4.4 有向无环图(DAG)

给出引理:

一个有向图 G 存在自环,当且仅当EiG,使得 Ei 在 DFS 森林中是回边;

证明:

因此我们知道,在 DAG 中不存在 back edge。而只有回边 (v,u) 满足 post(v) < post(u),并引出另一条引理:

在 DAG 中,每一条边总是指向 post number 减小的方向

至此我们发现:一个有向图:无自环、可线性化、无回边实际上相互等价(是同一件事);

进而:DAG 中,post number 最小的点一定是汇点(出度为 0),post number 最大的点一定是源点(入度为 0);

由于正整数集上的可比性,DAG 上所有结点中必定有 post number 最大的,也必定有最小的,即证明了另一条引理:

每个 DAG 中,至少有一个源点和一个汇点

因此,我们找到一种线性化 DAG 的方案:找到源点(post number 最大点)删除并输出,重复这个过程直至图为空;

 

4.5 Revisit: 有向图强连通分量

我们已经知道:

  1. 两结点间存在一条从 vivj 的有向道路 存在另一条从 vjvi 的有向道路,则称 vivj 强连通

  2. 两结点间存在一条从 vivj 的有向道路 存在另一条从 vjvi 的有向道路,则称称 vivj 单向连通

  3. 两结点间 不考虑所有道路的方向(称为“有向图的底图”),若这两个结点连通,则称 vivj 弱连通

而且:

另外,我们在有向图中讨论 “连通”,一般都指 “强连通”,除非额外说明。

现在,我们将一个有向图按强连通分量划分为若干个不相交集,如果:

则一定会得到一个 DAG,我们称这种形成的 DAG 为 embedding graph(嵌图)

这就是引理:任意一个有向图的 embedding graph 一定是 DAG

引入定义:源/汇点强连通分量:一个有向图中,它的 embedding graph 中源/汇点对应的强连通分量;

现在考虑如何找出任意一个有向图的强连通分量。

现在已知已经存在了有效的求解强连通分量的算法,线性时间复杂度,它主要基于一些性质:

  1. DFS 的 explore 子过程如果从结点 u 开始,则该子过程恰好在 u 及其所有可达结点已访问时终止

    这个性质告诉我们:如果在一个汇点强连通分量的任一顶点调用 explore 子过程,则恰好能获得这个强连通分量

    为什么必须是汇点?不然这个超结点出度不为 0,调用 explore 会出这个超结点,或者说可达结点在这个超结点之外也有;

    问题是:

    1. 如何确定一个有向图的汇点强连通分量中的一个顶点?

    2. 通过这个性质获得了汇点强连通分量后,下一步应该怎么办?

    针对第一个问题,我们率先给出了下面的性质:

  2. 在 DFS 中 post number 最大的点一定位于其中一个源点强连通分量内

    注意,post number 最小的点不一定在汇点强连通分量内,因为原图不是 DAG。这个 “post number 最大的点一定位于源点强连通分量” 的规律是比较特殊的。

    由于证明它需要另外一个性质,因此先介绍:

  3. CC 为有向图 G 的两个不同的强连通分量,并且存在一个从 C 的内部结点到 C 的内部结点中的有向边,则 C 所有结点的 post number 的最大值大于 C 的 post number 最大值(不是所有结点都大于)

    现在先证明这条性质:

    设顶点 uC,vC,由于 CC 是两个不同的强连通分量,因此不存在从 CC 的有向边(否则 CC 的顶点相互强连通,它们就不是强连通图了),因此 vC 中任意顶点 u 不单向连通。分情况:

    • 如果 DFS 遍历从 C 开始,则根据第一条性质,在一次 explore 结束后必然遍历所有可达结点,因此从 C 任意结点出发的 explore 一定不会到达 C,也就不会有 C 的 post number 大于等于 C 的情况;

    • 如果 DFS 遍历从 C 开始,那么在一次从 C 任意结点 u 出发的 explore 子过程必然通过了所有可达结点,又由于 C 中任意子结点到 C 中任意子结点单向连通,因此 explore 子过程结束后,CC 的所有顶点都会被遍历完成,这时显然出发点 uCC 中 post number 最大的结点;

    综上,这条性质得证;

    再证明上一条性质:

    由第三条性质可知,一个有向图的 embedding graph 中的任意一条有向边 (u,v),如果 uC,vC,则 C 的 post number 最大值大于 C 的post number 最大值;由于 embedding graph 是一个 DAG,因此对于所有不是 embedding graph 的源点的超结点 E0,总存在一个指向它的结点 E,使得 E 的 post number 最大值更大。因此最大 post number 的结点一定位于其中一个源点强连通分量中,性质 2 得证;

有了 2、3 两个性质,我们能解决:“如何确定一个有向图的汇点强连通分量中的一个顶点” 的问题。

我们找到一个有向图 G 的反转图 GR(即 GR 中所有边都与 G 反向),则 GRG 的强连通分支中的结点情况相同。

证明:

这样,我们借助第 2 条性质,在 GR 中做一次 DFS 找到 post number 最大的点(在 GR 源点强连通分量中),那么这个点也在 G 的汇点的强连通分量中!我们于是能成功找到整个汇点强连通分量。

接下来我们如何做来继续找图 G 中的其他强连通分量?其实还是由性质 3 保证:将汇点强连通分量,连同这个超结点指入的边一同删除,则 embedding graph 会出现新的汇点,它的出度一定是执行之前的汇点强连通分量。所以我们可以利用 GR 中对 post number 排序,不需要删除来决定新的汇点强连通分量是谁。总结:

  1. GR 上执行 DFS 按照 post number 标记各个结点并降序排序 Ee,,E2,E1,并获取 post number 最大的结点 EeEe 位于 G 的汇点强连通分量中);

  2. Ee 出发,在 G 中执行无向图连通算法(就是 explore,PREVISIT 中设置 ccnum[])找到汇点连通分量中的所有顶点,在一轮 DFS 结束后,找到剩余结点中 post number 最大的 Ek,按 post number 降序遍历,重复本步,直至所有点都已划归为一个强连通分量为止;

这个算法的时间复杂度是线性时间的,但是在很大的图中,这也是不可行的,因此目前有学者已经提出了 O(logn) 的算法。

 

考虑习题:

Give an efficient algorithm which takes as input a directed graph G=(V,E), and determines whether or not there is a vertex sV from which all other vertices are reachable.

  • 如果这个点存在,那么它一定在源点强连通部件中(反证法);

  • 找到任意一个源点强连通部件,做一次 DFS,如果没有遍历完所有顶点,则不存在;反之存在,并且这个强连通部件中的所有点都符合题意。

 

4.6 BFS in Graph

我们知道 DFS 可以找到图两点间的连通性,但是对于两个点间的最短距离却不清楚。我们可以利用广度优先搜索(BFS)来确定两点间的距离)。

我们本章认为 BFS 是先入队、更新距离,之后再出队。

并且和 DFS 不同,我们只关心同一个强连通分量中的距离情况。

有个 lemma。BFS 过程中,对每个 dN,都总存在一个时刻,使得

可以证明:当前队列中的所有结点总小于下入队一元素的距离;

这个 lemma 形象地说明 BFS 是“按层输出”的。

4.7 正权图中两点间的单源最短路径

我们知道 BFS 只能知道等边权情况下的两点距离。如果我们需要将边长(边权)考虑进去,则就没法适用。

一个改进方法是,按边权数量大小增加该边上的结点(对 le 边长的边 (u,v),在 u,v 间增加 le1 个顶点),相当于转换成了等边权图。

但这种方法的时间复杂度 O(|V|+eEle),也就是时间复杂度任意的坏(不知道边长的情况)。

再改进一些,就是 Uniform Cost Search;算法的思路是:

 

Dijkstra Algorithm 就是在这个算法的基础上,先将所有结点构建优先级队列(比逐个插入更快),然后通过手动出队、手动减小更新后的元素优先级、通知优先级队列调整被减小键值的元素(就是 decreaseKey),达成上面的目标。

上面的 pre[] 用到了两个性质:

注意到我们可以用相似的思路,让我们把所有顶点都先放到优先级队列中(减小出入队的开销),然后遍历点是动态地更新点的最短距(也就是改变优先级队列中某元素的优先级),当我们确定下某个点的优先级后,直接将它出队,这就是 Dijkstra Algorithm;

现在我们关心这个 “优先级队列” 的实现,以及它对于各种操作的复杂度。

众所周知,优先级队列通常使用堆来实现。我们比较常用的数据结构在 Dijkstra 算法中的使用的操作的耗时情况:

Type \ Aspectdelete mininsert/decreaseKey|V|×deletemin+(|V|+|E|)×insert
ArrayO(|V|)O(1)O(|V|2)
Binary HeapO(log|V|)O(log|V|)O((|V|+|E|)log|V|)
d-ary HeapO(dlog|V|logd)O(log|V|logd)O((d|V|+|E|)log|V|logd)
Fibonacci HeapO(log|V|)O(1) (amortized)O(|V|log|V|+|E|)

因此有这几个事实:

d 叉堆实现和二叉堆类似。不过每个结点有 d 个后继结点。位置为 j 的结点父结点位置为 j1d,子结点位置 [(j1)d+2,min{n,(j1)d+d+1}]

 

4.8 含负权无负环图中两点间的单源最短路径

如果一个图中存在负值的边权,那么为什么 Dijkstra Algorithm 不再适用了?

我们回顾 pre 用到的两个性质,子路径最短性、子路径距离可加性 都没有违反

那究竟是哪里出问题了?实际上是这个假设:从起点 s 到任意顶点 v 的的最短路径一定经过比 v 距离 s 更近的顶点

也就是 Dijkstra Algorithm 总是从当前最近到远的方法依次更新最短距,但是如果远处有个负值会降低路径总长度,那么需要重新更新这条路上所有已经确定最短路径的顶点。

更严谨地说,我们应该从如何证明 Dijkstra Algorithm 的角度下手,看看究竟哪个证明步骤不再成立。

原先我们使用归纳假设证明 Dijkstra Algorithm:

作出假设。在每次更新一个结点 s 的所有相邻结点位置后(每一轮 while 循环后),有下列条件成立:

条件 1. 存在一个值 d,使得从 s 到全部已出队结点集 R 中的所有顶点的距离都小于等于 d,同时 sR所有顶点的距离都大于等于 d

条件 2. 对每个已遍历的顶点 rR,从 sr 的最短路径上的所有点都在 R 中;

奠基。d=0 时(起点),上述假设显然成立;

归纳推理。下一轮更新时,将出队 uu 加入 R)并更新 u 的所有相邻结点 vi。接下来需要:

证明条件 1 仍然成立:由于 uvi 中的最短距离一定是正值,因此 d+mini{|uvi|}>d 满足条件 1;

证明条件 2 仍然成立:我们设从 su 最短路径上的上一个顶点 p,由子路径最短性(显然成立,否则可以找到更短的,没有借助边是正值的条件)可知,这条路径上的 sp 的子路径一定是 sp 的最短路径。由上一轮假设的条件 2,从 sp 路径上的所有点都在 R 中。

由于 u 刚入队,即 uR,因此边 (p,u) 上的两个顶点都在 R 上,所以 su 最短路上所有点也在 R 上。条件 2 仍然成立。

综上,我们证明了在 Dijkstra Algorithm 的每轮 while 循环上,都有条件 1、条件 2 成立。

现在如果我们要知道 sv(目标结点)的最短路,那么我们一定可以通过 Dijkstra Algorithm 逐步找到最短路上的各个顶点,最终达到 v;这就是 Dijkstra Algorithm 的正确性。

我们发现,如果边不再是正数,那么归纳假设中就没法证明条件 1 了

也就是说,可能存在一个在未出队(R 以外)的结点 q,其值也小于 d,这就说明我们没法保证最短路 ABCB 一定比 CA 更近(也就是 B 会普遍比其他结点“更难”出队、更后面出队),又由于 Dijkstra Algorithm 没法更新已经出队的结点的最短距,因此没法找到这条最短路径!

 

sidebar:那么能不能同时加上负值的绝对值最大值来转换成非负权的问题?不行。因为最短路中每条边被使用的次数不一样,因此这样转换没法等价为原问题。

 

所以本质上,Dijkstra Algorithm 无法求单源含负权图的最短路,就是因为没法更新出队后的结点最短距,因为它认为先出队的结点一定能更近地到达目标结点!

所以 “乱拳打死老师傅”,我们只要更新最短距的次数足够多、足够充分(没有 side effect 的话),那一定能知道真正的最短路。事实上也确实是这样,这就是 Bellman-Ford 算法:

这个 for all e in E 的遍历是有说法的,这不仅仅是 Dijkstra Algorithm 的一次 while 循环(确定已知顶点的相邻顶点的最短距),而是:

于是重复 |V|1 次后,一定能获得各个顶点的最短距,并且大多数情况下顶点早于这个轮数就遍历全了。

 

补充:负环问题 & 最简最短路

显然,如果一个图含有负环,那么就别想求最短路了,因为最短距是负无穷。

但是如果我要求的是最简道路(不含重边和环的道路)中的最短路呢?那又可以求了!

 

4.9 含负权的 DAGs 中两点间的单源最短路径

如果是 DAG,那么借助这个好的性质,单源最短路径的算法甚至可以优化到线性时间。

我们知道 DAG 可以线性化,那么很显然,按照线性序的顺序访问各个顶点,直接按照沿途的边来更新结点最短距,那一定可以获得各个结点最终的最短距。

算法很简单,就是先做一次拓扑排序,标记顺序后,沿顺序遍历点,更新点的所有相邻结点的目前最短距(不继续展开,而是跟从外层循环给定的顺序),时间是线性的。

如果要求 DAGs 最长路,那么把所有边权全部转为相反数,使用求 DAGs 最短路的方法即可。

 

考虑问题:

 

Chapter 5. Greedy Algorithms

5.1 Minimal Spanning Tree (MST)

对无向图,我们有引理 1:移除一个环上的边,不会改变这个图的连通性。

由引理 1,结合树的定义,我们可以得知:任何一个无向图,都有一个最小生成树,在保持原图连通性的同时边权和最小。

证明引理 2:对于一个树的数据结构,n 个结点一定对应 n1 条边。证明过程对顶点数学归纳。

对边数学归纳:

证明引理 3(引理 2 的逆命题):一个无向连通图如果 |E|=|V|1,那么它一定是一棵树。反证法。

证明引理 4:一个无向图是一棵树 当且仅当 任意两结点间有一条唯一路径。分开 左推右 和 右推左:

 

Kruskal 算法求 MST

初始只有顶点构成的森林,从小到大向顶点中加入边;如果不存在环则继续;如果存在环则移除这个边并尝试下一个边。最终直至加入 |V|1 个边结束。

这本质上就是一个 greedy approach;

割定理

如果边集 X 是图 G=(V,E) 的最小生成树的一部分,并且存在一个任意的结点子集 S,满足 X 不会连接 SVS 的顶点,我们称这个 SVSG 的一个割(cut set,图按顶点分割)。

那么在连接 SVS 间顶点的最短边 e 一定有 X{e} 也是 G 的最小生成树的一部分。

分类讨论 + 反证法证明 割定理

G 的最小生成树为 T

假设 eT,那么显然 X{e}T

如果 eT,那么由树的定义,必然存在一个边 e 连接 SVS 使得 eT。这时我们一定能构造一个新的树 T=T{e}+{e} 使得 w(T)<w(T),(T 是树的原因:T+{e}e 是环边,因此由 lemma 1,T 是连通的;再由 lemma2、3 得到是树)也就是这个树的边权和小于原来的 MST,这是和 MST 定义矛盾的。因此这种情况不存在。

 

Prim 算法求 MST

从一个顶点组成的集合 S={v0}开始,和其他顶点构成一个割(cut),那么每次向 S 中加入 VS 间的最短边,及其连接的顶点,组成新的顶点集 S,重复上述步骤直至加入了 |V|1 条边。

正确性由割定理保证。

 

考虑新的上色算法

先指定两条上色规则:

  1. (破圈)如果 C 是一个没有红色边的环,在 C 中选择一条最长的未着色边,并将其染成红色;

    利用了树的定义,这步的转换肯定是正确的;

  2. (闭圈)如果 D 是跨越一个割的、其中所有割边都不是蓝色边的集合,在 D 中选择一条最短的未着色边,将其着色为蓝色。

    利用了割定理,这步的转换也肯定是正确的;

在图上随便使用上述规则上色,直到所有边都着色为止。

最后所有的蓝色边就是这个图的 MST;

这个算法是正确的吗?我们已经知道了这两条规则是必要的,但是它能做到最后吗?

也就是说,如何证明这种上色方法能够一直做到全部上色完成而不产生冲突?

 

 

5.2 Set Cover(集合覆盖问题)

对一个分散的点集 S,要求放置最少的特殊点,使得 S 中的所有点到这个特殊点集 V 中的某个点的距离不大于 d

这个问题等价于:

对一个点集 B,有一些确定的 点集 S1,S2,,SmB,问:如何从 S1,S2,,Sm 中选取一些集合,使得 kSk=B,并且选取的集合最少。

遗憾的是,这个问题暂时没有多项式时间算法(NP Hard)。不过我们考虑 Greedy Algorithm 作为近似算法。

如果我们用 Greedy Idea 思考:每次选能覆盖未被覆盖点最多的 Sk,直至最终覆盖完全。

理论上,我们能保证这种 Greedy Idea 最坏情况只会达到最优情况(optimal)的 lnn 倍(局部最优和全局最优的误差的上界)。

 

证明:

nt 是贪婪算法在第 t 次迭代后仍然没有覆盖的顶点数(易知 n0=n),设最优的选取方案只需要 OPT 个集合;

由于剩下的顶点一定能被最优的 OPT 个集合覆盖,因此由鸽巢原理(抽屉原理),在这 OPT 个最优集合中,一定存在其中一个集合至少包含 ntOPT 个剩下的这些元素(反证;如果都包含少于 ntOPT 个元素,那么最后一定覆盖不完,这与 “最优集合能覆盖完所有顶点” 的前提矛盾)。

与此同时,贪心法会确保:nt+1ntntOPT(因为贪心法会选出每步的最优解),所以数学上:

ntn0(11OPT)t<n0(e1OPT)t=n0etOPT

注意到 t=lnnOPT 时,nt<nelnn=1,说明此时贪婪算法一定覆盖完全!因为是严格小于,所以贪婪算法取集合数量的最差不会比 OPTlnn 倍更差。

 

5.3 贪婪算法何时失效:Coins Charging

如果有种货币面值:{1,5,10,25,100}(单位略),如何用最少的货币数量凑出指定的值?

Cashier's Algorithm 就是一个 Greedy Algorithm:每次选取

但是这种最优是因为特殊的面值组合。

如果换一种面值组合就不是最优(近似解),例如 {1,10,21,34,70,100,350,1225,1500} 要凑出 140;

甚至有时候贪婪算法没法得出解(任意坏):{7,8,9} 凑 15;

嗯,贪婪算法之所以失效,就是因为局部最优解会逐渐偏离全局最优解。接下来我们将介绍一种能够解决这种问题的算法。

 

Chapter 6. 动态规划

我们在解决 DAGs 中最短路径的问题(线性化后逐结点解决)的时候,使用的就是动态规划。

动态规划的解决方案,就是解决子问题的途中记录下子问题的解,最后利用这些记录的解得出最终问题的解。

分治法和动态规划的区别?

  • 动归:小问题和大问题大小差不多(两者都是包含关系);

  • 动归:小问题不会 overrlap(很重要,充分利用了各个子问题的解);

6.1 最长子序列问题 I:最长递增子序列

给定一个数列 a1,a2,,,an,要求找到这样一个最长的子序列 ai1,ai2,,aik 使得 ai1<ai2<<aik,其中只要求 i1<i2<ik(不要求连续)。

这个问题的思路可以是用图来解决:

这样所有的序列都可以转换为一个 DAG 图,那么原问题转换为求出这个 DAG 的最长路径

 

6.2 编辑距离

编辑器中需要有 spell checking,通常会检查 typos;而 typos 的修改建议就是借助了单词间的编辑距离。

编辑距离描述了一个单词最相近的另一个(改变次数最小,使得一个单词变成另一个单词);

我们使用一个二维表格(含有空白)来描述编辑距离:

这被称为动态规划的 “状态转移方程”。

代码实现层面,看起来需要遍历全表,于是时间、空间复杂度 O(mn)m,n 分别为两个词的长度);

比较有意思的是,这里时间复杂度和空间复杂度都能优化:

在表 ED 中相关 entry 间看成连接的一条条路径,可以:

 

6.3 背包问题

6.3.1 01-背包问题

考虑一个问题题干:

假设有 N 件物品、容量为 V 的背包,放入第 i 个物品的 cost(这里指需要占据背包的容量)是 Ci,但得到的价值是 Wi

求解在不超过容量限制的情况下,如何装入背包以获得最大价值。

在 01 背包的假设下,我们有前提:每个物品只有 1 件,只可以选择放或者不放

这种做法比较简单,和之前我们讨论的编辑距离一样,都可以利用动态规划 “放一步看一步”。

定义状态表格 F[i,v] 表示i 个物品放入剩下容量 v背包后,可以获得的最大价值;

为什么是 “前 i 个”?因为动态规划需要子问题总是被包含在大的问题内;

很容易地得到状态转移方程:

求解方法也很简单,就是填表,伪代码如下:

注意到这个 O(NV) 时间和空间可以优化。我们以空间优化为例:

因为每轮 i 求解只涉及前一轮 i1 的内容,因此我们可以只保留 2 行,更巧妙地,1 行的数据(转换成一维列表):

如果只保存 F[v] 的数据,那么根据 F[i1,v]F[i1,vCi],可能要用到上一轮 v 更小的数据,因此应该从 v 大的位置向 v 小的位置遍历,例如:

问题变种:值得注意的是,这里并没有要求背包完全装满。如果要求背包必须恰好装满,应该怎么改?

答案是改变 F 初始化的方式就行。我们定 F[0]=0,但 F[1...V]=,就相当指定了这个背包最终状态只能空间为 0;

为什么?因为我们每次动态规划都是在找第 i 轮各个不同容量的背包的最大价值,相当于把每一轮都看作算法结束(因为是 “前 i 个物品”),因此这么设定最终会使得所有 “不能规约到剩余空间为 0” 的放置方法的价值都是

此外,上述解法还有常数时间优化:在每轮 i 的循环时,只从 Vmax{ViNWi, Ci} 就行;

 

6.3.2 完全背包问题

题干仍然不变,但在完全背包的假设下,我们有前提:每个物品有无穷件,可以选择放 [0,+)

思路一

思路和 01 背包相似,不过在遍历每种 i 物品时,要考虑放 0、1、……、VCi 件;

即:

F[i,v]=max{F[i1,vkCi]+kWi | 0kCiv}

这下时间 O(NViVCi),空间 O(NV),我们也不得不想办法优化了。

注意到一个引理:

在完全背包的前提下,如果两个物品 Ci>Cj,反而 Wi<Wj,则物品 i 可以完全抛弃,只把 j 纳入考虑范围。

在 01 背包中不能这么做,考虑一种情况:最后可能就只有 i,j 两个物品且背包不满。

这个引理可以完成常数时间的优化:

  1. 去除所有 cost 大于 V 的物品;

  2. 找出所有 cost 相同但 value 最大的物品,作为这个 cost 下的唯一选择(不再考虑剩下的物品);

这个优化过程总体 O(V+N) 时间;

最终我们需要落实解决并优化这个问题,可以把它规约到 01 背包问题上:

现在我们对拆分的过程优化一下:

也就是说,把第 i 个物品的件数使用二进制表示,转换成 01 问题,这样每种物品要拿几件就可以在 O(logVCi) 时间内讨论出。

这是一种算法。时间复杂度 O(NilogVCi)

 

思路二

还有一种思路:重写状态转移方程。注意到一个物品可以选不止一次,因此新的状态转移方程是:

F[i,v]=max{F[i1,v], F[i,vCi]+Wi}

注意到,这时我们考虑选择第 i 件物品时,用的是 F[i,vCi]+Wi,而非 F[i1,vCi]+Wi,就是因为考虑了第 i 个物品可以选很多次,所以不用 i1 次来转移,用 i 本身转移就行;

而且我们发现这个时候由于不依赖 i 小的情况下的 vCi 的单元格的值,因此在遍历 v 的时候,不需要像 01 背包一样倒序了(不如说倒序现在是错的了)!顺序就能解决问题。

时间复杂度 O(NV)

 

6.3.3 多重背包问题

题干仍然不变,但在完全背包的假设下,我们有前提:每个物品有有穷件,可以选择放 [0,Mi]

其实就是完全背包的特例,只不过相当于限制了 VCi 为指定的数 Mi

这个时候我们只有一种思路:

F[i,v]=max{F[i1,vkCi]+kWi | 0kMi}

在遍历的循环内层加一层遍历 k(该物品个数),都和同样的 entry F[v](这时它不仅仅指第 i 轮了)比较就行。

然后优化方法同 “完全背包”,使用二进制表示法来优化。时间复杂度 O(VilogMi)(其实是任意坏,因为 Mi 不定),如下:

我们还可以结合在完全背包中介绍过的常数时间优化,来应对这个问题。

可惜的是,在这种情况下没有 O(NV) 时间的解决方案。

拓展一下

如果多重背包问题需要恰好填满背包,并且别管最大价值了,那么这个时候就有 O(NV) 的解决方案了:

这个时候 F[i,v] 的含义改一改:不再是指 “前 i 件物品放入剩余容量 v 的背包中的最大价值” 了,而是指 “前 i 件物品填满容量 v 的背包后,最多还剩几个第 i 件物品可用”;并且定义 0F[i,v]Mi 才是可行的状态。这样就有伪代码:

 

6.3.4 问题变种:Optimal Solution for Coins Charging

现在我们再来看上一章用贪婪算法解决不了的 “硬币找零” 问题,发现其实它就是另一种形式的背包问题:

现在硬币找零,就是一个 “要求将物品恰好填满背包、并且价值最小(不是最大)” 的完全背包问题。

直接写出状态转移方程:

F[i,v]=min{F[i1,v], F[i,vCi]+1}

好,问题解决。我们只需要注意:

  1. 同理可以只用一维列表解决这个问题;

  2. 对于恰好填满背包的,记得初始情况 F[0]=0F[]=+,理由见 “01 背包问题”,(不是 )希望读者举一反三;

具体选取的方案可以在每层循环决定时记录在额外的数组中(如果需要的话),不影响时间和空间复杂度。

如果每种硬币数量有限,那么就是需要恰好填满的“最少”多重背包问题。这里不再赘述。

 

6.4 最长子序列问题 II

6.4.1 最长公共子序列

对于可比较序列 a1,a2,,amb1,b2,,bn,求出它们最长的公共子序列(不要求连续)。

这个问题其实就是之前编辑距离的特例。我们同样引入 “匹配距离” 的说法:

最后最优解就在 M[m,n] 中;

6.4.2 最长公共子串

对于可比较序列 a1,a2,,amb1,b2,,bn,求出它们最长的公共子串,要求子串是连续的。

就是 “最长公共子序列” 的特殊情况:

在状态转移方程中,如果 same(i,j)=0,那么就不是比大小的问题了,需要把 M[i,j] 直接置为 0 即可。

并且最长公共子串的最优解在表的任意位置,需要遍历得到。

当然,上述只是最基础的方案,你能在网上/论文里找到更快的算法,不过它们大多数都是基于这个算法改进得到的。

6.4.3 Quiz: 最大连续子序列和

对于一个整数集 Z 上的子序列 a1,a2,,an,请问:

 

DP 填表 General Method:

  1. DP 填表,决定表格的维数和含义;

  2. 出口是什么?(最容易的 entries);

  3. 填表的顺序是将大问题拆解为小问题的思路;

  4. 最优解在表的什么部分;

 

6.5 3-Partition Problem

三分问题(3-Partition problem):给定整数集合 a1,a2,,an,判断是否可以将其分割为三个相互不相交的子集 I,J,K,使得:

iIai=jJaj=kKak=13m=1nam

定义一个四维数组 F[i,x,y,z],将集合中前 i 个元素分为和的值分别为 x,y,z 的 3 组子集,entry 是 boolean,表示能否将前 i 个元素按 x,y,z 的分法分为 3 个集合。

因此有递推式:F[i,x,y,z]=F[i1,xai,y,z]F[i1,x,yai,z]F[i1,x,y,zai]

出口 F[1,a1,0,0]=True, F[1,0,a1,0]=True, F[1,0,0,a1]=True

答案是 F[n,S3,S3,S3],其中 S=m=1nam

6.6 跳石子

在一条河上有一座独木桥,长度为 L,上面分布着一些石子,为了简单起见,我们假设桥为 0-L 的一段线段,而石子都分布在整数坐标上,也就是有一个函数 stone(x),表示在坐标 x 上是否有石子,比如 stone(0)= 1 表示在桥头有一个石子,stone(2) = 0 表示在坐标为 2的位置没有石子。 现在有一个小朋友站在桥头的位置想要过桥(站在桥尾或者跨过桥尾均为过了桥),但他不想踩到石子,他每跨出一步的步长是 [S,T] 区间中的任何整数(包括 S 和 T)。设计算法求小朋友要过河,必须踩到的最少的石子数。

设一维表 F[i] 表示到坐标为 i 的位置上时踩到的最少石子数。因此有状态转移方程:

F[i]={stone(0),i=0stone(i)+min{F[iS...iT]},otherwise

显然,时间复杂度 O((TS)n)

6.7 Shortest Reliable Path (Lite) & Revisit Bellman-Ford Algorithm

在网络拓扑图中,如果用边长表示传输时延,并且需要考虑到每一跳的丢包问题,因此需要尽可能避免边数过大。

定义问题:给定图 G,以及其边上所有权值。要求其中两点 s,t 间的最短路,并且至多只能经过 k 条边。

s 为源点,建立一个二维数组 D[v,i] 表示对任意终点 v,经过最多 i 条边的最短路径长。

出口 D[v,0]=, D[s,i]=0;其中状态转移方程:

D[x,i]=min(x,y)E{D[y,i1]+l(x,y)}

答案是 mini{D[t,i]},如果是 “恰好经过 k 条边”,那么甚至可以优化空间复杂度(只保留 i=k 的一列)。

这里可以用动态规划的思想理解 Bellman-Ford Algorithm。对单源填一个表的时间 O(|V||E|)

那么如果想知道每两个顶点间的最短路径,用上述算法的时间 O(|V|2|E|),不过这里我们还有一种替代算法 Floyd-Warshall Algorithm,时间复杂度 O(|V|3),如何选择取决于图的疏密。

6.8 Revisit Shortest Paths Problem: Floyd-Warshall Algorithm

对图 G 中的结点编号 V={1,2,,n},我们定义三维数组 D[i,j,k],认为从 ij 的顶点只经过(不包含 i,j)前 k 个顶点({1,2,,k})的最短路的长度。

出口:D[i,j,0]=l(i,j);状态转移方程如下:

D[i,j,k]=min{D[i,j,k1], D[i,k,k1]+D[k,j,k1]}

状态转移式在 “从 ij 的经过前 k 个结点的最短路” 的前提下,分为两类情况:

  • 这个从 ij 的最短路不经过 k,那么一定经过前 k1 个顶点,最短路的数据保存在前 k1 的表格中;

  • 这个从 𝑖 到 𝑗 的最短路经过 k,那么一定可以从 k 拆成两条路径,一个以 k 为起点,另一个以 k 为终点;

那么从 uv 的最短距离即为 minwD[u,v,w]

6.9 Traveling Salesman Problem (TSP)

给定一个无向连通图 G,指定起点 s,能否找到一条初级回路(顶点和边都不重复的、首尾相连的路径),通过这个这个图的所有顶点(这种初级回路定义为 哈密顿回路,或者 H 回路),并且长度最小。

一般讨论完全图。因为如果两个城市间不存在路径,则添加一条远大于其余边权的边,这样不影响计算最优回路;

简单来说,如何找无向连通图 G 中最短的 H 回路。

Brute-Force 算法:遍历从 s 出发的所有可能路径,共 (n1)! 种,因此时间 O(n!)

使用动态规划:为每个结点标记序列 V={1,2,,n},其中起点定义为 1,我们记 SVS 必包含起点 1)为 subproblem,对 jS,定义 D[S,j] 为 “在结点子集 S 以及 G 中对应的边作为图,从 1 出发、到 j 的初级回路的最短长度”。

这样对 j1,jS,有状态转移方程:

D[S,j]=miniS, ij{D[S{j}, i]+l(i,j)}

含义解释:在子问题对应的点集 S 中,从 1 开始到 j 结束的最短初级回路,一定可以转换为求从 1 到 j 前一点 i 的最短初级回路再到 j 的情况。这个 “子路径最短性” 我们在 Dijkstra Algorithm 证明时已经阐述过了。

由于子集 S 情况有 2n 种(外层循环 O(2n)),内层对 j 遍历 O(n),对 i 遍历计算一次最小值 O(n),因此时间复杂度 O(n22n)

出口 S={1}, D[S,1]=0|S|>1, D[S,1]=

可惜的是 TSP 问题即便使用动态规划也无法在多项式时间内解决。它是个 NP Complete 问题(因为等价于求子集问题)。

 

6.10 Independent Sets in Trees

独立集(Independent Sets)问题是指,对于一个图 G=(V,E),找出一个顶点子集 SV,使得 S 内的所有顶点两两不相连。

这个问题也是一个 NP Complete 问题(也和集合的子集数量有关)。

但是如果这个图 G 是一棵树,那么可以用动态规划在线性时间内解决。

我们定义 I(u) 为以 u 为根所在的子树最大独立集的大小。那么有状态转移方程:

I(u)=max{1+grandchildren w of uI(w), children w of uI(w)}

解释一下,树中的两个结点不相连的情况很多,但是如果我们从共同的根结点开始看,那么就只需要讨论两类情况:

最终此法时间复杂度 O(n)n 为结点个数。

 

Chapter 7. Linear Programming

7.1 Definitions

定义:给定一组变量,要求它们的一组取值使得:

举个例子:

maxx1+6x2x1200x2300x1+x2400x1,x20

对于两个变量:

对于三个变量:

 

于是更一般的形式上,我们可以定义线性规划的规范形式(Canonical Form):

maximizecTxsubject toaiTxbi,i[1,m]xj0,j[1,n]

松弛型(Slack Form):

min cTxaiTx+biTs=bixj,sj0

在 Computer Algebra System 中非常有用。

所有线性规划问题都能通过以下方法转换为规范型或松弛型:

例如:

minimize3x1+x2subject tox1>x2+5,x1+3x2=10

可以被转换为:

maximize3x1x2subject tox1+x2<5,x1+3x210x13x210

 

解决上述线性规划问题的算法之一就是 simplex(单纯形法),它描述:

从可行空间代表的凸多边形的任意一个顶点出发,按任意方向遍历凸多边形的相邻顶点,找到使 objective function 的局部最优值(没有任何相邻结点能使得 objective function 的取值更优),即为线性规划的解。

但实践上有几个问题:

我们将在讨论完 “对偶问题” 的概念后再回来解决。

 

另一个算法是椭球法,由苏联科学家发现。我们这里不作详细描述,只需要知道它是多项式时间复杂度的算法即可。

还有一个算法是内点法,也是时间复杂度。

 

7.3 Integer Linear Programming

整数规划并不是说直接可以通过一般线性规划直接得到的,因为解可能是小数,仅仅做舍入运算是不一定能得到最优解的(舍入可能造成偏离目标,但有可能其他原先不是最优的点在舍入后最优了),可能是近似解。

整数规划就是一种 NP Problem,除非使用普通线性规划的解恰好是整数,否则不能很快地求出最优解。

常见方法有剪枝、动归等等。我们后面讨论。

 

7.4 Dual Program

同样考虑这个问题:

maxx1+6x2x1200x2300x1+x2400x1,x20

如何验证这个线性规划的最优解是 (x1,x2)=(100,300),目标最大 1900?

可以通过线性约束的线性叠加,得到关于目标函数的最紧的约束(x1+6x21900),这个最紧约束一定是能达到的最优值。

因此我们进行配凑:

y1x1200y2x2300y3x1+x2400

(y1+y3)x1+(y2+y3)x2200y1+300y2+400y3

我们希望 200y1+300y2+400y3 能取得的最小值(因为需要 “最紧约束”),一定是原问题的最大值。

也就是说,原问题转换为了:

min200y1+300y2+400y3y1+y31y2+y36y1,y2,y30

我们总结一下转换的方法:

另外,这种对偶问题还有 Complementary Slackness(互补松弛)来描述这种 “相互为最紧约束” 的性质:

对线性规划问题 P 的解 x,和对应对偶问题 D 的解 y

以上面的问题以及它的对偶问题为例,我们发现原问题解 (x1,x2)=(100,300) 和对偶问题解 (y1,y2,y3)=(0,5,1) 符合上述互补松弛条件:

7.4 Application: Shortest Path

给定一个有向正权图 G=(V,E),其权重函数 w:EQ+(映射到正实数)。现在给定一个源点 s 以及一个终点 t,求从 st 的最短路径权值和。

我们可以用线性规划表示这个问题!

maxdtdvdu+w(u,v), (u,v)Eds=0di0, iV

这个线性规划实际上是在描述 Dijkstra Algorithm。dx 表示 xs 的最短距,那么上面的线性规划 dvdu+w(u,v) 应该这么理解:

假设图上有一些橡皮筋,现在 s 点钉住(ds=0),然后拉动 t 点使 dt 增长(maxdt),率先被拉直的边的组合就是最短路。

注意到如果 (u,v) 在最短路中,那么就有 dv=du+w(u,v) 成立,而如果不在,那么 dv<du+w(u,v)

我们回想,Dijkstra Alogrithm 的步骤是不断更新 dist(v)>dist(u)+l(u,v)dist(v) 的值,也就是让期望值和真实值接近;而这里相反,则是讨论真实值(dxx 真实最短路径长)的约束关系。

这恰好对应该约束条件紧绷(取等)的状态,我们可以从数学角度证明这点。

 

再看另一个角度:

CS={SV|sS,tS} 为正权图 G=(V,E) 中的 st 间的所有割集。

我们可以将 st 间的最短路,转换成整数规划问题:

mineEwexeeδ(S)xe1,SCSxe{0,1},eE

其中 δ(S) 表示所有 “一个顶点在 S,另一个不在 S” 的边的集合(被割集 S 割的边的集合)。

上面的线性规划可以理解为,在 st 间的任意一个割中,必须要有至少一条边被选中(因此约束 st 是连通的),并且让这些边权和最小。

那么我们能将 xe relax 到 [0,1] 的范围吗?因为整数规划没有多项式时间解。答案是可以,因为 xe 一定会在整数点取得最优解。xe 是小数的情况只可能是一条路取 x%,另一条路取 (1x)%(两条路都最短),这种情况 x 一定是任意的,否则不满足条件。这种情况解所代表的超直线一定平行于可行域边缘超平面,那么取一个整数顶点也是最优解。

更进一步地,xe 能不能 relax 到 xe0(因为需要同时处理 e 个变量)?可以,因为约束是 min,而且是正权。

我们注意到新问题的对偶问题:

maxcCSyccCS, eδ(c)ycwe,eEyc0,cCS

注意借助对偶变量的个数来列式。对偶变量 y 共有原问题约束变量的个数(CS 个);

这个对偶问题是有含义的。注意到 c 与跨越 st 间任意一个割的边有关,因此 yc 引申为:对于割集 c 来看,st 的护城河宽度(moat),这个目标函数就是让护城河总体宽度尽可能长,与此同时任意边 e 的长度要容纳下 st 间所有经过边 e 的护城河总长度。

 

 

7.5 Simplex Algorithm

7.5.1 算法内容

算法内容:我们需要可行域中的任意一个顶点 v,要求一旦发现邻居 v 的目标值更好,则设置 v=v,重复至没有邻居更好(局部最优),就一定是全局最优解;

我们定义单纯形:

对每一次迭代,Simplex 需要:

  1. check 当前结点间是不是 optimal 的点(要比较所有邻居,不好做);

  2. 确定个下一步需要到达的点(用不等式找顶点,不好找);

我们知道,如果当前顶点是原点,上面两个操作会非常简单。因此我们可以每轮迭代都进行一次坐标变换,把下一次要处理的点变成原点。

为什么顶点是原点就简单?

首先 task 1 判断是否为最优点会比较简单:

当原点是顶点,线性规划必定满足(x=(x1,x2,,xn)):

maxcTxAxbx0

因为原点恰好满足 x0n 个不等式的取等(紧绷)且在可行域内,因此原点是顶点。

假设原点是最优解,即 x=(x1,x2,,xn)=(0,0,,0),当且仅当 i, ci0

证明:因为一旦存在一个 ci>0,那么一定有更大的 xi>0 能取到更优的值;

其次 task 2 判断下一个需要到达的点也会比较简单:

如果此时原点不是最优点,那么对于这个 ci>0 对应的 xi,控制其他的 xj 不变,增大这个 xi(使 xi0 不再紧绷)使得第一次在 Axb 的不等式中的某个不等式取等(loose 变为 tight),记为 Akxbk

此时 {x0|x}{xi0}{Akxbk} 恰好此时 (0,0,,Δxi,0,,0)n 个不等式紧绷的、且在可行域的解。因此这个点就是下一个可以去的顶点。

那么如何在这个点的基础上继续重复呢?我们需要变换坐标系,让上面的更优点为原点,手工解法是,让 yj=xj, jiyi=bkAkx(碰撞到的新的 n 个约束条件能构成下一个相邻顶点),联立(欧几里得刚性变换,或者称正交变换),将原来的线性规划全部换成 y 即可变换坐标系。

 

7.5.2 Corner Cases

考虑新的情况,如果原点不是顶点怎么办?这个时候说明变量不全是大于等于 0 的(原点可能不在可行域内)。我们只要转为规范型就行了!

这里我们换一种思路,转成松弛型。并且确保所有等式约束条件右边常量都是正数(不是就乘以 1);

然后,我们构造一个新的线性规划:

我们注意到,这个新的线性规划有很好的性质,zi=bi,且其他变量都是 0 可以作为起始顶点很好找。并且如果:

 

再考虑一个可能会使单纯形法失效的情况:退化。也就是说一个顶点可能由多个超平面相交得到,多于确定一个点所需的个数。

以 3 个变量的退化的线性规划为例,可能一个顶点会满足 4 个不等式,这会导致无论选哪三个约束条件并松弛另外一个,都会超出可行域(也就是当前顶点的所有邻居和这个点一样好),进而造成死循环。

破局的方法是,向约束条件的常数添加一个极小的扰动(加或减 0.00001),这不会对最优解的获得造成影响,但能够区分各个约束条件,以跳出这个怪圈。

 

另外,单纯形法能应对 unbounded 吗?可以,只要出现一种情况:在松弛一个不等式时,无论如何都碰不到下一个不等式取等了。这个应对方法是设定一个最大/最小阈值,松弛到这个值还找不到就算 unbounded;

 

7.5.3 运行时间

对于一个规范型线性规划,假设它有 n 个变量、m 个约束条件(不包含 n 个变量大于等于 0 的条件),那么给定一个顶点有多少邻居?

我们知道,一共有 m+n 个不等式,又因为每个顶点 u 的邻居与其共享 n1 个不等式、n 个不等式同时取等组成一个顶点。

每轮单纯形算法总是从 n 个不等式(确定当前顶点的)中任意松弛一个,并任意换成其余 m 个不等式之一。因此相邻顶点最多可能的个数为:mn

每轮进行坐标变换的复杂度最多 O((m+n)n)(将联立出的 nyx 关系式代入每个不等式中,实际是操作矩阵);

同时,单纯形所有顶点的最多个数为 Cm+nn,意味着我们最多进行这么多轮遍历。

因此单纯形算法上界 O(n(m+n)Cm+nn),这是指数时间复杂度!

不过线性规划是多项式时间内能解决的问题,因为椭球法、内点法是多项式时间复杂度的。

但好笑的是,单纯形算法虽然是指数级算法,但是实践中要比椭球法、内点法在大部分情况下更快。

研究人员对这个问题使用了 Smoothed Analysis,获得了2008 年的哥德尔奖。研究证明了单纯形法只有在每一轮都取到一个特定的特殊的顶点时才会 fallback 到指数时间,否则有一点扰动就极大概率是多项式时间能完成的。

 

7.6 最大流与最小割

最大流问题:对于一个有向图,边权为该边的流量限阈。其中图中除了源点、汇点外,流体无法存储、无法产生。

在不超过每条边的容量前提下,我们希望尽可能多地从源点 s 向汇点 t 输送尽可能多的流体。

我们设每条边权(容量) ce,每条边的计划流量 fe 则:

如果我们从汇点到源点连一条有向边,这样取消了 s,t 的特殊性,便于在代数层面操作。

此时让 (t,s) 的容量 +,这样可以让每个顶点流入都等于流出。

注意到我们可以更改约束条件都为 为规范型:

在线性规划的思路中,我们怎么做?

先确定原点(fe=0),注意到,所有流都是 0 的情况不是最优的,因此增加一个 fi,由于约束限制,最终会同时提升一条路径上的流量(相当于找到了一条路径),最终达到这条路径整体的最大容量。

于是,在每次迭代中,找到的一条路径(不小于前一条路径的流量,且这条路径后一个边不能小于前一条边),因此该路径的每一条边都满足下面两种情况之一:

  1. (u,v) 包含在最初网络中,并且未达到最大流量;

  2. 其反向边 (v,u) 在最初网络中,并且存在一定流量;

对第一种情况,边 (u,v) 最多还能接受 cuvfuv 的流量;

对第二种情况,最多增加的流量就是 fvu(选中 (v,u) 后最大的承载量就会取消原先选中的流量);

因此我们可以定义一个新的图 Gf,用来描述选中当前路径后,网络中的 “增加流量的机会”:

cf={cuvfuv,if (u,v)E, fuv<cuvfvu,  if (v,u)E, fvu>0

这样,我们模仿单纯形法,获得计算最大流的算法:

从全空流开始,计算剩余网络 Gf,使用线性时间的 BFS 在 Gf 中找到一条从 st 的能提升当前流量的路径,然后将在流网络中插入这个路径(注意插入的路径的各边权重都等于该路径的最大流量)。

 

 

问题:给定一个有向图(含多个源点和汇点),判断它是否是流通的(circulate);

创建一个源点 s、汇点 t,让 s 指向各个源点

 

应用:二部图匹配。

创建一个源点 s 指向二部图中一个部分的所有结点,创建一个汇点 t 被二部图中另一部分的所有结点指向。让每条边的权重(容量)均为 1;

则原问题等价于 “在新的图中是否有最大流的流量等于最终配对数”。

 

注:最大流问题的整数性。

如果所有边的容量为整数,则算法得到最大流规模也是整数。因为算法每次迭代都增加的是一个整数值的流量。

如果边的容量不是整数(例如实数),那么最大流的很多算法没法使用,例如上面的剩余网络算法可能无法终止。

 

 

Chapter 8. NP Problems

本章几乎全部是概念。

首先,问题和算法是两个概念。

例如最小生成树(Kruskal/Prim)算法能在指数级 search space 中使用接近线性时间的复杂度完成任务。这就是一个简单问题(P);

再例如可满足问题(SAT)。如果每个 clause 的 word 数量都是 2(称为 2-SAT 问题),那么可以将 xixi 作为节点、¬xixj 可以看作 (xi, xj) 的一条有向边。问题就能转换成查找该有向图的强连通子图中,是否存在 xixi 在一个强连通部件中。如果存在则冲突(不可满足),否则可以找到满足的赋值;

那么如果是 3-SAT 问题呢?(人们发现 N-SAT, N3 和 3-SAT 问题的复杂度是一样的)

SAT 问题就是典型的 search problem。我们现在定义两个概念:

而一个 search problem 必然满足一个性质:给定一个 S,必然能快速地代入 I 来确定正确性,也就是说:

一个 search problem 的 I,S 存在一个算法 C 判断 S 的正确性:C(I,S) 其时间复杂度为 O(|I|n)多项式时间可验证);

 

我们再来看看旅行商问题(TSP):这个问题我们换一种表述方式。给定 n 个顶点以及它的 n(n1)2 条边的距离,以及一个预算距离 b,能否找到一个初级回路经过所有顶点,并且预算不大于 b(没有就报告)?这就是把 optimization problem 转换成了一个 search problem;

理论上,我们认为 optimization problem 和 search problem 的相互转换不会改变时间复杂度,因为:

又因为判定问题 和 搜索问题可以通过图灵机进行等价,因此:优化问题、判定问题、搜索问题可以相互转换(一个问题就有 3 个版本)。

通常,判定问题用作判断问题的复杂度(参数复杂性);优化问题版本通常用来求解近似算法;

 

最小割问题(移除最少边使得图不连通),可以通过执行 n1 次最大流算法,在多项式时间内解决;

而平衡割问题(balanced-cut,每个割集内结点数不少于 n3,并且其间最多 b 条边)不能在多项式时间内解决(N-Coloring 问题规约到它);

 

ZOE:一个线性方程组系数只能是 0/1,解的取值也只能是 0/1;它和整数规划一样,不存在多项式时间算法。

因此 3 维匹配问题可以转化为 ZOE 问题(一个匹配 triple 作为一个变量),所以 3D Matching 是不存在多项式时间算法的。

 

独立集问题(最大顶点子集互不相连)、顶点覆盖问题(所有边至少有一个顶点在当前顶点子集中)、团问题(最大顶点子集间两两有边)三者实际相互等价。

(顶点覆盖是集合覆盖的子集?)

 

子集和问题(取一个给定整数集的子集,使这个集合元素和为指定值),可以转为整数规划问题 a1x1+a2x2++anxn=W

 

 

 

NP:所有种类的 search problems。多项式时间内可验证的问题(判断标准);

co-NP:NP 问题的补问题。一般认为 co-NPNP 的交集包含 P

 

P:可以多项式时间内可以求得解的 search problems(PNP);

为什么说只要解决一个 NP 问题,就能解决所有 NP 问题(Solve one and solve all)?

因为可以规约(reduction):将问题 A 规约到 B,则说明 A 不难于 B,并且:

 

NPH:NP Hard 问题,可能没法在多项式时间内验证的问题。

NPC:NP 完全问题,其他所有 NP 问题都能规约到它(因此 NPC 不简单于 NP)。

 

证明:如果 NP != co-NP,则 P != NP

如何将 H 路径问题向 H 回路(顶点和边不重复的)规约?

f: 在原图 G 中添加一个结点 x 连接图中的两个顶点 u,v(由 H 回路的定义,H 回路必然经过这两个新边);在新图 G 中运行 H 回路算法;

g: 如果有 H 回路,则删除这两条边,记为原问题的解(H 道路);如果没有 H 回路,则原问题无解;

如何将 3-SAT 问题规约到独立集问题?

考虑任意一个 3-SAT:

这时形成一个图 G,考虑 G 的独立集:

我们知道 xx 只能有一个真,

 

如何将 SAT 问题规约到 3-SAT 问题?

注意任意 SAT 问题的任意子句 (a1a2an),可以转换成若干个 3 个文字组成的子句的合取:

(a1a2y1)(y1a3y2)(yk2akyk1)(yn3an1an)

解释:除了第一个、最后一个子句有两个 a,以及一个 y / y 以外,中间的所有子句都是由前一个子句正出现的 yk2 取反,以及下一个子句反出现 yk1 对应的正出现;

这两个子句不完全等价,但有个性质:它们同可满足性

先证明 。设 ai 为真(左边的子句为真):

再证明 ;反证

进一步,3-SAT 即便再简化都是难问题!例如,我们让每个变量最多出现 3 次、每个文字最多出现 2 次;

 

如何将 H 回路问题规约到 TSP 问题(H 回路的权值和最小)?

考虑两个问题的不同(以便构造 input f):

f:将 H 回路补上边使得它称为一个完全图(完全图一定存在 H 回路),但是旧的边权设置为 1,新补的边设置大于 1;

对新图执行一次 TSP 算法(假设有),如果边权和能小于等于预算 nn 为顶点数),则说明没有用到新补的边,因此原图存在 H 回路;反之不存在;

 

如何应对 NP 问题?

梯度下降

 

Hopfield Netural Network

给定一个无向图,图上每个顶点要么 +1,要么 -1(两种状态,记顶点状态 si);然后给边也赋权值;

我们定义 configuration:顶点状态的一个指派;

定义一个 good edge:该边如果负权,则顶点状态相同,正权则顶点状态相反;

也就是希望两个顶点边是负值顶点状态相同,边是正值顶点状态不同;

定义一个 satisfied node:该点连着的 good edges 权重高于 bad edges 的权重。用数学表示:v:e=(u,v)Ewesusv0

定义一个 stable configuration:一个对顶点状态的指派,使得所有顶点都是 satisfied nodes;

我们对任意一个连通的无向图,希望找到一个 stable configuration;

这样我们发现,只要把 unsatisfied nodes 的状态 flip 后,至少当前的顶点会变成 satisfied node!

我们让这样的 flip 变换的 configuration 为一个邻居,就有 local search: