算法设计与分析学习笔记

课程为山东大学软件学院人工智能专业2020年大二下学期的“算法设计与分析”课程
2020年2月25日~2020年5月8日,笔记更新结束
课程内容:《算法导论(第三版)》1、2、3、22、24、23、25、26、34章(按讲课顺序)
在讲完第22章后,讲了一下什么是优先队列(最大堆)以及相关操作及其时间复杂度,还讲了一点贪心算法的基本思想;在23章之后,讲了并查集,用于分析Kruscal算法

作业基本都是定理、引理或推论的证明
作业(按照博客的内容):二.4.(3);四.4.(2);四.4.(4);六.贪心算法基本思想英文翻译成中文;七.4.(1);八.4.(1);十.作业(有两次);十二.增广路径定理的证明

注:前四部分(含)和第七部分需要结合教材,其他部分可以不看教材,但最好还是看教材

最近一次修改:2020年9月3日,完善了部分证明和伪代码,修改了部分错误


本学习笔记主要是包含:
(1)注意事项
(2)算法伪代码
(3)时间复杂度说明
(4)定理、引理和推论的证明过程

定理证明等都是课上老师所讲的内容,笔记中出现的定理证明都是老师所讲过的,没有出现的即为没有讲过的,证明的内容根据老师所讲和书中证明过程


一.广度优先搜索(Breadth-first Search)

  1. 注意事项:infinity表示无穷大,emptySet表示空集,NIL表示空,u.color表示结点s的颜色,u.d表示结点s相对于起点的距离,u.pi表示结点s的前驱结点
  2. 伪代码
BFS(G, s)
for each vertex u in (G.V-{s})
	u.color = WHITE
	u.d = infinity
	u.pi = NIL
s.color = GRAY
s.d = 0
s.pi = NIL
Q = emptySet
ENQUEUE(Q, s)
while Q != emptySet
	u = DEQUEUE(Q)
	for each v in G.Adj[u]
		if v.color == WHITE
			v.color = GRAY
			v.d = u.d + 1
			v.pi = u
			ENQUEUE(Q, v)
	u.color = BLACK 
  1. 时间复杂度
    每个结点及其边都进行扫描且只扫描一次,因此为theta(|V|+|E|)

注:BFS算法生成的树是一棵最短路径树(shortest path tree)


二.深度优先搜索(Depth-first Search)

  1. 注意事项:u.d表示结点u刚被发现的时间,u.f表示结点u处理完的时间
  2. 伪代码
DFS(G)
for each vertex u in G.V
	u.color = WHITE
	u.pi = NIL
time = 0
for each vertex u in G.V
	if u.color == WHITE 
		DFS-VISITE(G, u)

DFS-VISITE(G, u)
time = time + 1
u.d = time
u.color = GRAY
for each vertex v in G:Adj[u]
	if v.color == WHITE
		v.pi = u
		DFS-VISITE(G, v)
u.color = BLACK
time = time + 1
u.f = time
  1. 时间复杂度
    DFS中的初始化theta(|V|)和对每个白色结点进行算法theta(|V|),DFS-VISITE中遍历结点的相邻的边为theta(|adj[u]|),所有的边总和为theta(|E|),因此总时间复杂度为theta(|V|+|E|)
  2. 定理、引理和推论证明
    (1)定理:括号化定理:在对有向或无向图G = (V, E)进行的任意深度优先搜索中,对于任意两个结点u和v来说,下面三种情况只有一种成立
    区间[u.d, u.f]和区间[v.d, v.f]完全分离,在深度优先森林中,结点u不是结点v的后代,结点v也不是结点u的后代
    区间[u.d, u.f]完全包含在区间[v.d, v.f]内,在深度优先森林中,结点u是结点v的后代
    区间[v.d, v.f]完全包含在区间[u.d, u.f]内,在深度优先森林中,结点v是结点u的后代
    证明:根据祖孙和后代之间的关系以及时间来分析
    (2)推论:后代区间的嵌套:在有向或无向图G的深度优先森林中,结点v是结点u的真后代当且仅当u.d < v.d < v.f < u.f
    证明:由上述括号化定理易证
    (3)定理:白色路径定理:在有向或无向图G = (V, E)的深度优先森林中,结点v是结点u的后代当且仅当在发现结点u的时间u.d,存在一条从结点u到结点v的全部由白色结点所构成的路径
    证明:
    =>:如果v是u的后代,且从u到v只有1个点,即u等于v,那么显然成立。如果v是u的真后代,那么根据后代区间的嵌套推论有u.d < v.d,即在发现u时,v还没发现,此时v是白色的,而且v可以是任意后代,因此从u到v路径上的每个后代都是白色的,即存在一条白色路径。
    <=:存在从u到v是一条白色路径,此时是在u刚被发现时的情况。那么,u.d < v.d。而此时u.f或在u.d和v.d之间或在v.d之后,u和v之间存在路径说明u和v存在祖先后代的关系,根据括号化定理,v.f在v.d和u.f之间,此时满足后代区间嵌套的推论,即v是u的后代。
    (4)定理:在对无向图G进行深度优先搜索时,每条边要么是树边,要么是后向边
    证明:假定任意一条边(u, v),有u.d < v.d,那么如果在扫描u的邻接结点时发现了v即(u, v),那么(u, v)是树边,如果在扫描v的邻接结点时发现的u即(u, v),那么(u, v)是后向边。

三.拓扑排序(Topological Sort)

  1. 注意事项:算法对象有向无环图
  2. 伪代码
TOPOLOGICAL-SORT(G)1)call DFS(G) to compute finishing times f[v] for each vertex v
(2)as each vertex is finished, insert it onto the front of a linked list
(3return the linked list of vertices
  1. 时间复杂度
    调用DFS为theta(|V|+|E|),生成链表为theta(|V|),返回链表为theta(1)
    总共为theta(|V|+|E|)
  2. 定理、引理和推论证明
    (1)引理:一个有向图G=(V, E)是无环的当且仅当对其进行的深度优先搜索不产生后向边
    证明:
    =>:假设有向图无环并产生了一条后向边(u, v),那么v是u的祖先,此时v是灰色的,根据白色路径定理,当发现v时,有一条从v到u的白色路径,因此出现了一个环路。矛盾,假设不成立。
    <=:假设不含后向边的图G含有环路,环路中有两个点u和v,假设v是第一个发现的,即v.d<u.d根据白色路径定理,在发现v时,环路中的点都是白色的,因此从v到u存在一条白色路径,因此u是v的后代。因为u和v在环路中,因此有一条从u指向v的边,且是后代指向祖先的后向边。矛盾,假设不成立。
    (2)定理:拓扑排序算法TOPOLOGICAL-SORT生成的是有向无环图的拓扑排序
    证明:(根据拓扑排序的定义,我们只要证明图中的任意一条边(u, v),有u.f > v.f,图中只有四种边,并且图是无环的,没有后向边,因此只需注意判断其他三种边即可)如果(u, v)是前向边,即u是v的祖先,那么u.f > v.f一定成立;如果(u, v)是树边,u是v的父亲节点,因此必有u.f > v.f;如果(u, v)是横向边,即u和v没有祖先和后代之间的关系,并且v一定已经先处理完,再发现u,因此,u.f > v.f。综上,得证。

四.强连通分量(Strongly Connected Components)

  1. 注意事项:算法对象有向图;算法中的第二次DFS时,转置图按结束时间从大到小排序;证明中的第二个引理是根据原图而不是转置图;G.T表示图G的转置
  2. 伪代码
STRONGLY-CONNECTED-COMPONENTS(G)1)call DFS(G) to compute finishing times u.f for each vertex u
(2)compute G.T
(3)call DFS(G.T), but in the main loop of DFS, consider the vertices in order of decreasing u.f (as computed in line 1)4)output the vertices of each tree in the depth-first forest formed in line 3 as a seperate strongly connected component
  1. 时间复杂度
    两次DFS为theta(|V|+|E|),求图的转置为theta(|V|+|E|)
    总共为theta(|V|+|E|)

  2. 定理、引理和推论证明
    (1)引理:设C和C’为有向图G = (V, E)的两个不同的强连通分量,设结点u, v 属于C,结点u’, v’ 属于C’,假定图G包含一条从结点u到结点u’的路径u~u’。 那么图G不可能包含一条从结点v’到结点v的路径v’~v。
    证明:如果图G包含一条从结点v’到结点v的路径v’~v, 则G也将包含路径u~ u’~ v’和v’~ v~u。因此,u和v’可以互相到达,从而与C和C’是不同的强连通分量的假设矛盾
    (2)引理:设C和C’为有向图G = (V, E)的两个不同的强连通分量。假如存在一条(u, v) 属于E,这里u属于C,v属于C’,则f(C ) > f(C’)。(d(U) = min{u.d}、f(U) = max{u.f} 其中U是V的子集,u属于U)
    证明:
    分为两种情况考虑:a. d(C ) < d(C’) b. d(C ) > d(C’)
    a.第一种情况:设x是C中第一个被发现的结点,那么d(C ) = x.d,C中其余结点在刚发现x时都是白色的,C’中的所有结点此时也是白色的,那么x就能从通过结点u和边(u, v)到达C’中。根据强连通分量的性质和白色路径定理,C和C’中的所有结点都是x的后代,那么f(C ) = x.f,因此自然有f(C ) > f(C’)
    b. 第二种情况:设y是C’中第一个被发现的结点,那么f(C’) = y.f,d(C’) = y.d,由于d(C ) > d(C’)并且C’中的结点无法到达C中,因此C’中的结点一定先被处理,因此d(C ) > f(C’),推出f(C ) > f(C’)
    (3)推论:设C和C’为有向图G = (V, E)的两个不同的强连通分量,假如存在一条边(u, v) 属于E.T,这里u属于C,v属于C’,则f(C ) < f(C’)
    证明:G.T中有一条从u到v的边,那么在G中这条边就是从v到u的,v属C’而u属于C,根据之前的引理,那么f(C ) < f(C’)
    (4)定理:算法STRONGLY-CONNECTED COMPONENTS能够正确计算出有向图G的强连通分量
    证明:(现在考虑的图是G.T,即第二次的深度优先搜索树和强连通分支对应)采用归纳假设,即第二次深度优先搜索时产生的前k棵树都是强连通分量。当k=0时,此时没有树显然成立。假定前k棵树都是强连通分量。现在考虑第(k+1)棵树,假设结点u是该树中第一个被发现的结点,且u在强连通分量C中,那么在u刚被发现时,C中所有结点都是白色的,根据白色路径定理,C中的所有结点都是结点u在搜索树中的后代,因此可以排除强连通分量的结点不在树中的情况。根据推论(设C和C’为有向图G = (V, E)的两个不同的强连通分量,假如存在一条边(u, v) 属于E.T,这里u属于C,v属于C’,则f(C ) < f(C’))我们能知道,如果存在一条从C到达其他未发现的强连通分量C’的路径,那么f(C ) < f(C’),而在第二次深度优先搜索中,我们访问结点的次序是按照完成时间的非递增顺序,有f(C ) > f(C’),也就是说不可能存在从C到C’的路径,但可能存在从C到已发现的强连通分量的路径并且由于已发现的强连通分量中的结点都是黑色的,不可能对其进行访问,因此可以排除树中的结点不在强连通分量中的情况。综上,以u为结点的树恰好构成强连通分量C,证明结束。


五.优先级队列(Priority Queue)

优先级队列是一种使用堆的数据结构,支持高效地插入元素和找到最大元素值。
堆序:对于每个结点的值v,其结点的父结点的值w,有key(w) > key(v)
leftChild(i) = 2i,rightChild(i) = 2i + 1,parent(i) = floor(i/2)
Sift-up:将一个元素向上浮动 O(log n)

Input: An array H[1...n] and an index i between 1 and n.

Output: H[i] is moved up, if necessary, so that it is not larger than its parent.1.done <- false2.if z = 1 then exit {node i is the root}3.repeat

	if key(H[i]) > key(H[lower(i/2)]) then interchange H[i] and H[lower(i/2)]else done <- true

	i <- lower(i/2)4.until i = 1 or done

Sift-down:将一个元素向下浮动 O(log n)

Input: An array H[1...n] and an index i between 1 and n.
Output: H[i] is percolated down, if necessary, so that it is not smaller than its children
1.done <- false
2.if 2i > n then exit {node is a leaf}
3.repeat
	i <- 2i
	if i+1 <= n and key(H[i+1]) > key(H[i]) then i <- i+1
	if key(H[lower(i/2)]) < key(H[i]) then interchange H[i] and H[lower(i/2)]
	else done <- true
	end if
4.until 2i > n or done

MakeHeap(n):建立一个含有n个元素的堆O(n)

Input: An array A[1...n] of n elements
Output: A[1...n] is transformed into a heap
1.for i <- lower(n/2) downto 1
	Sift-down(A, i)
2.end for

Insert(H, x):向堆H中插入值为x的元素O(log n)

Input: A heap H[1...n] and a heap element x.
Output: A new heap H[1...n+1] with x being one of its elements.
1.n <- n+1 {increase the size of H}
2.H[n] <- x
3.Sift-up(H, n)

FindMax(H):堆顶元素即为最大值O(1)

Delete(H, i):删除指定位置的元素O(log n)

Input: A nonempty heap H[1...] and an index i between 1 and n.
Output: A new heap H[1...n-1] after H[i] is removed.
1.x <- H[i]; y <- H[n]
2.n <- n-1 {decrease the size of H}
3.if i = n + 1 then exit {done}
4.H[i] <- y
5.if (key(y) >= key(x)) then Sift-up(H, i)
6.else Sift-down(H, i)
7.end if

ExtractMax(H):删除最大元素O(log n)

Input: A heap H[1...n]
Output: An element x of maximum key is returned and deleted from the heap
1.x <- H[1]
2.Delete(H, 1)
3.return x

六.贪心算法(Greedy Algorithms)

一个算法是贪婪的,如果它建立了通过每一个小步骤的解决方案,在每一小个步骤选择一个决定,以优化一些潜在的标准。
区间调度问题(interval scheduling):根据完成时间排序,从小到大选择
下面是“Review of Greedy Algorithms”,也是贪心算法的基本思想
Greedy algorithms are usually designed to solve optimization problems in which a quantity is to be minimized or maximized. It typically consists of an iterative procedure that tries to find a local optimal solution. In some instances, these local optimal solutions translate to global optimal solutions. In others, they fail to give optimal solutions. A greedy algorithm makes a choice on the basis of little calculation without worrying about the future. Thus, it builds a solution step by step. Each step increases the size of the partial solution and is based on local optimization. The choice made is that which produces the largest immediate gain while maintaining feasibility. Since each step consists of little work based on a small amount of information, the resulting algorithms are typically efficient. The hard part in the design of a greedy algorithm is proving that the algorithm does indeed solve the problem it is designed for.
翻译:贪心算法通常被设计来解决最优化问题,即最小化或最大化一个量。它通常由一个迭代过程组成,该过程试图找到一个局部最优解。在某些实例中,这些局部最优解转变为全局最优解。在其他实例中,它们不能给出最优解。贪心算法在很少计算的基础上做出选择,并且不关心未来。因此,它逐步构建了一个解。每一步都基于局部优化增加了部分解的规模。所做出的选择,是在保持可行性的同时产生最大的即时收益。由于每一步都由基于少量信息的少量工作组成,因此产生的算法通常是有效的。贪心算法设计中的困难部分是证明该算法确实解决了它被要求解决的问题。


七.迪杰斯特拉算法(Dijkstra’s Algorithm)

  1. 注意事项:单源最短路径问题(Single-source shortest paths problem)
    先看一道题,一个加权有向图G,采用下面哪个选项,在改动之后能保证最短路径不变。
    A.每条边都加上17
    B.每条边都乘以17
    C.A和B
    D.既不是A也不是B
    答案是B,考虑一种情况最短路径是2条边权值分别为2和3,还有一条边是6,那么最短路径为2+3=5,如果是A那么(2+17)+(3+17) > 6+17,最短路径改变,而B选项是同时扩大,因此最短路径不变。也就是如果最短路径边数很多,那么选A,则会处于明显劣势。

“What’s the shortest way to travel from Rotterdam to Groningen? It is the algorithm for the shortest path, which I designed in about 20 minutes. One morning I was shopping in Amsterdam with my young fiancée, and tired, we sat down on the café terrace to drink a cup of coffee and I was just thinking about whether I could do this, and I then designed the algorithm for the shortest path. ”— Edsger Dijsktra

在介绍迪杰斯特拉算法之前,先说一下需要用到的函数
“松弛操作”中包括2个函数INITIALIZE-SINGLE-SOURCE(G, s)和RELAX(u, v, w)在第377页
松弛是唯一导致最短路径和前驱结点发生变化的操作
其他知识点都在书中,我这里就不说了

  1. 伪代码:

书中:

DIJKSTRA(G, w, s)
INITIALIZE-SINGLE-SOURCE(G, s)
S = emptySet
Q = G.V
while Q != emptySet
	u = EXTRACT-MIN(Q)
	S = S union {u}
	for each vertex in G.Adj[u]
		RELAX(u, v, w)

自己:

DIJKSTRA(G, w, s)
for each vertex v in G.V-{s}
	alpha[v] = infinity // s到v的距离
	pi[v] = NIL // v的前驱结点
alpha[s] = 0
Create an empty priority queue PQ // 创建优先队列PQ
for each vertex v in G.V
	INSERT(PQ, v, alpha[v]) // 关键值为alpha[v]
while PQ != empty
	u = EXTRACT-MIN(PQ) // 从优先队列中找到最小的,把u作为出发点
	for each edge e = (u,v) leaving u
		if alpha[v] > alpah[u] + le // le是边的长度
			alpha[v] = alpha[v] + le
			pi[v] = u
			Reset-Key(PQ, v, alpha[v]) // 修改以调整优先队列
  1. 时间复杂度
    迪杰斯特拉算法的时间复杂度依赖于最小优先队列的实现。我们这里采用的是最小二叉堆。初始化即构建最小二叉堆O(n),将每个结点从Q中删除是O(log n)那n个点是O(nlog n),for each共执行m次,由于是二叉堆修改的时间复杂度为O(log n),那么总共就是O(nlog n + mlog n),由于连通图中 m >= n-1,因此总共就是O(mlog n)

  2. 定理、引理和推论证明
    (1)(Dijsktra算法的正确性)Dijsktra算法运行在带权重的有向图G = (V, E)时,如果所有权重为非负值,则在所有算法终止时,对于所有结点u属于V,我们有u.d = 最短距离(s, u)。
    证明:采用数学归纳法。当|S| = 1时,因为S = {s}并且d[s] = 0,成立。假设当|S| >= 1时也成立。令v为下一个将要被加入到S中的结点,令u是v的在S中的前驱结点,alpha(v) = d[u] + w(u, v)。我们只需证明没有任何一条路径的长度小于alpha(v)。考虑另外一条从s到v的路径P,令x是P中最后一个在S中的结点,令y的前驱结点为x。由于算法中是将alpha(估计值)最小的结点加入S中,因此一定有alpha(v) <= alpha(y),那么P = alpha(y) + P(y, v) >= alpha(y) >= alpha(v),则d[v] = alpha(v)一定是最小值。综上,算法是正确的。
    在这里插入图片描述

注:
迪杰斯特拉算法生成的是最短路径树(shortest path tree)
为什么迪杰斯特拉算法要求权值非负呢?
先举一个反例,如果存在两组边分别为10和-8,3和3,那么算法就会优先选择3和3的一组边,而实际最短的是10和-8。如果权值为负,那么在上面的证明中就无法推出alpha(y) + P(y, v) >= alpha(y),因为P(y, v)可能为负。综上,权值必须为非负的,才能保证正确性。
为什么迪杰斯特拉算法是一种贪心算法?
算法在运行时,每次都将估计值最小的结点加入集合S中。


八.最小生成树(Minimum Spanning Tree)

  1. 注意事项:Kruscal算法和Prim算法都是贪心算法,Kruscal算法每次加入到A中的安全边永远是连接A和A之外某个结点的边中权重最小的边,Prim算法每次加入的边都必须是使树的总权重增加量最小的边。

  2. 伪代码:
    (1)Kruscal算法:

MST-KRUSCAL(G, w)
A = emptySet
for each vertex v in G.V
	MAKE-SET(v)
sort the edges of G.E into nondecreasing order by weight w
for each edge(u, v) in G.E, taken in nondecreasing order by weight
	if FIND-SET(u) != FIND-SET(v)
		A = A union {(u, v)}
		UNION(u, v)
return A

(2)Prim算法:

MST-PRIM(G, w, r)
for each vertex v in G.V-{s}
	alpha[v] = infinity 
	pi[v] = NIL // v的前驱结点
alpha[s] = 0
Create an empty priority queue PQ // 创建优先队列PQ
for each vertex v in G.V
	INSERT(PQ, v, alpha[v]) // 关键值为alpha[v]
while PQ != empty
	u = EXTRACT-MIN(PQ) // 从优先队列中找到最小的
	for each edge e = (u,v) 且v在PQ中
		if w(u, v) < alpha[v] 
			alpha[v] = w(u, v)
			pi[v] = u
			Reset-Key(PQ, v, alpha[v]) // 修改以调整优先队列

3.时间复杂度
Kruscal算法和Prim算法都为O(mlog n)

4.定理、引理和推论证明
(1)定理:设G = (V, E)是一个在边E上定义了实数值权重函数w的连通无向图。设集合A为E的一个子集,且A包括在图G的某棵最小生成树中,设(S, V-S)是图G中尊重集合A的任意一个切割,又设(u, v)是横跨切割(S, V-S)的一条轻量级边。那么边(u, v)对于集合A是安全的。
证明:设T是一棵包含A的最小生成树,如果T包含(u, v),那么该边对A集合一定是安全的。如果T不包含(u, v),那么由于切割(S, V-S)尊重A,而A又是T的一部分,因此一定存在从S到V-S横跨切割的一条边(x, y),该边不在A中且是T的一部分。设u在S中,v在V-S中,那么一定存在一条从u到v的经过(x, y)的简单路径。如果形成了一棵包含(u, v)的最小生成树T’,那么(u, v)对于A一定是安全的。如果直接将(u, v)加入其中,那么会与之前从u到v经过(x, y)的简单路径形成环,因此我们必须将(x, y)删除,T会形成两个连通分量,而加入(u, v)则可以将这两个连通分量连起来形成一棵新的生成树T’,我们要证明T’是最小生成树。因为T是一棵最小生成树,因此T中边的权重之和w(T) <= w(T’),由(u, v)是轻量边,因此有边的权重w(u, v) <= w(x, y),则有w(T’) <= w(T),推出w(T’) = w(T),即T’也是一棵最小生成树。A是T的一部分,(x, y)不是T的一部分,因此A是T’ 的一部分,所以A并上(u, v)也是T’ 的一部分,而T’ 是一棵最小生成树,那么(u, v)对于A来说一定是安全的。
(2)推论:设G = (V, E)是一个连通无向图,并有定义在边集合E上的实数值权重函数w。设集合A为E的一个子集,且该子集包括在G的某棵最小生成树里,并设C = (Vc, Ec)为森林Ga = (V, A)中的一个连通分量(树)。如果边(u, v)是连接C和Ga中某个其他连通分量的一条轻量级边,则边(u, v)对于集合A是安全的。
证明:切割(Vc, V - Vc)尊重集合A,边(u, v)是横跨该切割的一条轻量级边,因此,边(u, v)对于集合A是安全的。
注:关于迪杰斯特拉算法和Prim算法的异同,很好的一篇文章
https://blog.csdn.net/qq284565035/article/details/51583314


九.并查集(Union Find)

并查集表示一组不连通集合的一种数据结构
并查集需要支持三种操作:
(1)MAKE-SET(x):创造一个仅包含x的新连通分支(集合)
(2)FIND(x):返回包含元素x的集合的代表元(就像是国家有主席、总统一样,代表元是该集合的代表)
(3)UNION(x, y):将x和y所在的集合取并集,并用新的集合代替他们

令m = 上述三种操作的总的调用次数、n = 元素的数量

四种实现方式:
naïve linking:自然连接
link-by-rank:根据秩数连接
path compression:路径压缩
link-by-rank with path compression:带有路径压缩的根据秩数的连接

  1. naïve linking方式
    将第一个结点所在树的根节点连接到第二个结点所在树的根节点上
    在这里插入图片描述
    在这里插入图片描述
MAKE-SET(x)
parent[x] = x

FIND(x)
while (x != parent[x])
x = parent[x]
return x

UNION(x, y) 
r = FIND(x)
z = FIND(y)
parent[r] = z

定理:在最坏的情况下(形成了一个链表),一次find操作花费theta(n)时间,n是元素的数量

  1. link-by-rank方式
    每个结点保持一个整数——秩,初始时,所有集合的秩为0,在这里秩数等于树的高度。秩小的集合连接到秩大的集合中。
    在这里插入图片描述在这里插入图片描述
MAKE-SET(x)
parent[x] = x
rank[x] = 0

FIND(x)
while (x != parent[x])
x = parent[x]
return x

UNION(x, y)
r = FIND(x)
s = FIND(y)
if (r == s)
	return 
else if (rank[r] < rank[s])
	parent[r] = s
else if (rank[r] > rank[r])
	parent[s] = r
else 
	parent[r] = s
	rank[s] = rank[s] + 1  // 秩相同时,将其中一个的秩+1,将另一个连过来

该方法的6个性质:
(1)如果x不是根结点,那么rank[x]不会再发生改变。
证明:算法中只改变根结点的秩,而非根结点不会再变为根结点。
(2)如果x不是根结点,那么rank[x] < rank[parent[x]]。
证明:一个秩为k+1的根结点,只会通过两个秩为k的根结点合并产生。
(3)如果parent[x]改变,那么rank[parent[x]]严格增加。
证明:如果parent[x]改变,那么x一定是根结点,那么改变前有parent[x] == x,在x改变,即被连接到新的结点r时,有rank[parent[x]] = rank[r] > rank[x]。
(4)任何一个秩为k的结点,在以它为根结点的树中,至少含有2^k个结点。
证明:当k=0时,成立。做归纳假设,秩为k-1的节点,以它为根的树至少包含2^ (k-1)个结点。证明对秩为k的根结点依然成立。只有两个秩都为k-1的树合并,才能出现秩为k的树,根据归纳假设,这两课树至少含有2^ (k-1)个节点,合并后,至少含有2^k个结点,所以归纳假设成立。
(5)一个结点的秩的最大值为lower(log n)。
证明:有性质(4)很容易推出
(6)秩为k的结点数量最多为n/2^k。
证明:由性质(4)任何一个秩为k的结点,在其子树中至少含有2^k个结点。秩为k的不同结点不可能含有相同的后代。

定理:在最坏的情况下,UNION和FIND操作,需要花费O(log n)的时间。
证明:这两个操作以树的高度为界,而在性质(5)中,树的高度最高为lower(log n)。

  1. path compression
    路径压缩是针对FIND操作的重新组织
    在这里插入图片描述
    在这里插入图片描述
FIND(x)
if (x != parent[x])
parent[x] = FIND(parent[x])
return parent[x]

通过递归调用,将从x到根结点的路径上的所有结点的父结点都改为根结点。
路径压缩改变了树的结构。

路径压缩中不改变树的秩,此时有,height(x) <= rank[x]。

  1. link-by-rank with path compression
    link-by-rank中引入路径压缩后,那六个性质没有改变。
    迭代log函数:
    在这里插入图片描述

十. 动态规划(Dynamic Programming)

动态规划历史:Bellman 动态规划的先驱
斐波那契数列(Fibonacci Sequence)的例子

// 自顶向下
TOP-DOWN(n)
Initialize M[0...n] with -1 // -1: unfilled
M[0] <- 0; M[1] <- 1;
return fibonacci(n)

fibonacci(j)
if (M[j] == -1)
	M[j] <- fibonacci(j-1) + fibonacci(j-2)
return M[j]

// 自底向上
BOTTOM-UP(n)
Initialize M[0...n] with -1
M[0] <- 0; M[1] <- 1;
for i = 2 to n
	M[i] <- M[i-1] + M[i-2]

有向无环图中的单源最短路径问题(第24章 第2节)
有向无环图(directed acyclic graph DAG)
书中:

DAG-SHORTEST-PATH(G, w, s)
topologically sort the vertices of G
INITIALIZE-SINGLE-SOURCE(G, s)
for each vertex u, taken in topologically sorted order
	for each vertex v in G.Adj[u]
		RELAX(u, v, w)

自己:

DAG-SHORTEST-PATH(G, w, s)
topologically sort the vertices of G
for each vertex v in G.V
	v.d = infinity
	v.pi = NIL
s.d = 0
for each vertex u, taken in topologically sorted order
	for each vertex v in G.Adj[u]
		if v.d > u.d + w(u, v)
			v.d = u.d + w(u, v)
			v.pi = u

算法时间复杂度:拓扑排序theta(V+E),初始化theta(V),每条边进行了一次松弛为theta(E),因此总运行时间为theta(V+E)

动态规划可以被使用,如果满足下面的条件
1.由多项式数量的子问题组成
2.原问题的解决可以简单地由子问题计算而来
3.从“最小”到“最大”有一个子问题的简单的顺序

子问题之间有清晰的相互依赖关系,这是动态规划算法的核心。如斐波那契数列、有向无环图中的单源最短路径问题

动态规划技术通常用于解决最优化问题

0-1背包问题(Knapsack problem)
定义:OPT(i, w) = 在重量限制w的情况下,从1到i个物品放入背包中的最大价值。
目标:OPT(n, W)
Bellman equation
在这里插入图片描述

KNAPSACK(n, W, w1, w2, ..., wn, v1, v2, ..., vn)
for w=0 to W
	M[0, w] = 0
for i=1 to n
	for w=0 to W
		if (wi > w)
			M[i, w] = M[i-1, w]
		else
			M[i, w] = max{M[i-1, w], vi+M[i-1, w-wi]}
return M[n, W]

算法时间复杂度:O(nW),但这个不是多项式时间算法。(这个解释如果大家想去了解,可以去网上搜索相关资料)

作业1:(1)如何通过动态规划表回溯得到最优解;(2)编程实现0-1背包动态规划算法
(1)
在这里插入图片描述
从(n, W)的方格向上移动,如果两个方格(i, w)与(i-1, w)的值相同,说明物体i没有放入背包;如果不相同说明物体i放入了背包中,根据Bellman equation,移动到上一行(i-1, w-wi),按照这个规则继续下去。同则向上,不同则移动。
在本例中,右下角为(5, 11)值为40,向上(4, 11)仍为40,说明物体5没有放入背包中,再往上(3, 11)为25,说明物体4放入了背包中,那么11kg-6kg=5kg,所以上一个位置移动到(3, 5)方格值为18,向上(2, 5)值为7,说明物体3放入了背包中,5kg-5kg=0kg,因此上一个位置移动到(2, 0)方格,接下来一直到(0, 0)值没有变化,即没有新的物体放入背包中。
(2)
代码:

#include <iostream>
#include <cstdio>
using namespace std;

int max(int, int);

int main()
{
	// 输入部分
	int W; // 背包容量
	int n; // 物体数量
	cout << "请输入背包容量(正整数):";
	cin >> W;
	cout << "请输入物体数量(正整数):";
	cin >> n;
	int w[n+1] = {0}; // 每个物体的重量
	int v[n+1] = {0}; // 每个物体的价值
	cout << "请输入物体重量和价值按照\"重量 价值\"输入:";
	for (int i = 1; i <= n; i++)
		cin >> w[i] >> v[i];
	
	// 求解部分
	int table[n+1][W+1]; // 动态规划表
	for (int i = 0; i <= W; i++)
		table[0][i] = 0;
	for (int i = 1; i <= n; i++)
	{
		for (int j = 0; j <= W; j++)
		{
			if (w[i] > j)
				table[i][j] = table[i-1][j];
			else
				table[i][j] = max(table[i-1][j], v[i] + table[i-1][j-w[i]]);
		}
	}
	bool things[n+1] = {false}; // 装入背包中的物品
	int k = W;
	for (int i = n; i >= 1; i--)
	{
		if (table[i][k] > table[i-1][k])
		{
			things[i] = true;
			k -= w[i];
		}
	}
	
	// 输出部分
    cout << "动态规划表为:" << endl;
	for (int i = 0; i <= n; i++)
	{
		for (int j = 0; j <= W; j++)
			printf("%3d ", table[i][j]);
		cout << endl;
	}
	cout << "应该装入物品:";
	for (int i = 1; i <= n; i++)
    {
    	if (things[i])
			cout << i << " ";
    }
	cout << endl;
	cout << "最优解的值为:" << table[n][W] << endl;
	
	return 0;
}

int max(int i, int j)
{
	return i >= j ? i : j;
}

运行结果:
在这里插入图片描述
带有负环的最短路径问题
负环(negative cycle)是一个有向环,环中边的权重之和为负值。
正环(positive cycle)是一个有向环,环中边的权重之和为正值。(不可能出现)
0环是一个有向环,环中边的权重之和为0。(可以出现,但没必要)

引理1:如果存在从s -> v的路径包含负环,那么不存在从s -> v的最短路径
证明:如果存在这样的负环,那么可以无限的经过负环,降低路径上的权重和。
引理2:如果图G不存在负环,那么存在从s -> v的一条简单最短路径。
(最短路径不含环)

为了解决一般的单源最短路径问题,我们将这个问题分成两个,一个是不含负环的单源最短路径问题,一个是负环的识别问题。

Def. OPT(i, v) = length of shortest s -> v path that uses at most i edges.
Goal. OPT(n-1, v) for each v.
(我们的目标为什么是n-1?是因为引理2,从s -> v的最短路径长度至多包含n-1条边。)
在这里插入图片描述

SHORTEST-PATHS(V, E, l, s)
foreach node v in V:
	M[0, v] <- infinity
M[0, s] <- 0
for i = 1 to n-1
	foreach node v in V:
		M[i, v] <- M[i-1, v]
		foreach edge(u, v) in E
			M[i, v] <- min{M[i, v], M[i-1, u] + luv}

空间复杂度:O(n^2) 一个二维数组
时间复杂度:O(nm)
存储图的数据结构可以采用以入边为依据的邻接链表
作业2:对下图进行填表(s为出发点)
在这里插入图片描述
结果:
在这里插入图片描述

如何通过表得到顶点s到各个顶点的最短路径长度?表中最后一行就是从s到各个顶点的最短路径长度。
如何通过表得到顶点s到各个顶点的最短路径?其实我认为通过上面的动态规划表也是可以的,和之前的方式一样,如果发生变化就通过计算得到之前的点……。老师说可以改变算法结构,增加一个二维数组p[n-1][v],用来记录前驱顶点,通过这个新的二维数组的最后一行就可以得到路径,但我通过检验其实一维数组p[v]也是完全可以的。但还是通过维护一个记录前驱的一维数组进行回溯方便。
在这里插入图片描述
我们可以通过维护两个一维数组,而不是维护一个二维数组
d[v] = length of a shortest s->v path that we have found so far.
predecessor[v] = preceding node on the above s->v path.

引理3:对每一个顶点,d[v]是单调非递增的。
引理4:在第i次迭代后,d[v]是当前从s->v用最多i条边的最短路径长度。
证明:当i = 0时成立。假设第i次后,成立。那么在第i+1次后,令p是另一个从s->v的含有最多i+1条边的路径,令u是v的前驱顶点,那么从s->u最多包含i条边,设从s->u的任意路径为p’,那么根据归纳假设有d[u] <= l(p’),因此有d[v] = d[u] + luv <= l(p’ ) + luv = l(p ),所以假设成立。(这个证明很重要)

推论:如果最短路径含有k条边,那么算法可以在最多k次迭代后找到最短路径。
定理2:假设没有负环,Bellman-Ford计算从s->v的最短路径,时间复杂度为O(mn),空间复杂度为theta(n)
证明:引理2+引理4

负环检测问题(Detecting negative cycles)
先用Bellman-Ford算法迭代n-1次,接着再迭代1次,如果最短路径长度继续减小,说明一定含有负环。

引理5:对于每个结点v,如果有OPT(n, v) = OPT(n-1, v),那么没有负环。
证明:
在这里插入图片描述
(OPT(n+1, v)的推导中,OPT(n-1, u) + luv = OPT(n, v),因此得到最后的结果。这是老师给的证明方法,更为直观一些,但用归纳法证明更好。)
引理6:如果对某些结点有OPT(n, v) < OPT(n-1, v),那么从s->v的长度为n最短路径一定含有环W,且是负环。
证明:如果OPT(n, v) < OPT(n-1, v),任何s->v的最短路p,最多含有n条边,实际上更准确的是含有n条边,否则OPT(n, v) = OPT(n-1, v)。由鸽巢原理,路径p一定包含重复的结点x,令W为路径p中的环,如果W不是负环,那么删除W,则s->v最多有n-1条边并且有OPT(n, v) = OPT(n-1, v)。此时产生矛盾,因此W是负环。
引理6的逆否命题:引理6’:如果不含负环,那么对于每个结点有OPT(n, v) = OPT(n-1, v)。

定理4:如果对于每个结点v有OPT(n, v) = OPT(n-1, v),当且仅当不含负环。
证明:引理5 + 引理6’
定理5:能够在theta(mn)的时间内,找到一个负环。

BELLMAN-FORD(V, E, l, s)
foreach node v in V:
	d[v] <- infinity
	predecessor[v] <- null
d[s] <- 0
for i = 1 to n-1:
	foreach node v in V:
		foreach edge(u, v) in E:
			if (d[v] > d[u] + luv)
				d[v] <- d[u] + luv
				predecessor[v] <- u
foreach edge(u, v) in E:
	if (d[v] > d[u] + luv)
		return true // 含有负环
return false // 不含有负环

十一. 所有结点对之间的最短距离(All-pairs Shortest-Paths Problem)

给一个nxn的权值矩阵W,表示有向图G = (W, E)边的权值。输出一个距离矩阵D = (dij),表示顶点i和顶点j之间的距离。
定义:Lij(m) = i -> j 的路径且最多含有m条边
目标:Lij(n-1) 每一对顶点i和j
(不能含有负环)
lij(m)表示,从 i 到 j 最多含有m条边的最短路长度
在这里插入图片描述
伪代码:

ALL-PAIRS-SHORTEST-PATHS(W)
n = W.rows
L(1) = W 
for m = 2 to n-1
	let L(m) be a new n x n matrix
	L(m) = EXTEND-SHORTEST-PATHS(L(m-1), W) 
return L(n-1)

EXTEND-SHORTEST-PATHS(L, W)
n = L.rows
let L’ = (Lij’) be a new n x n matrix
for i = 1 to n 
	for j = 1 to n
		Lij’ = infinity
		for k = 1 to n
			Lij’ = min(Lij’, Lik + wkj)
return L’

时间复杂度 O(n^4)
在这里插入图片描述
Lij(m) = min{Lik(m-1) + wkj | 1 <= k <= n},这个我感觉很漂亮,很像矩阵相乘
在这里插入图片描述
我去,和我想的一样
在这里插入图片描述
Lij(4) = min{Lik(2) + Lkj(2) | 1 <= k <= n}

改进后的算法只有主过程需要改进(看明白)
当最后一次进入主过程的循环时,2m > n-1,由第十部分动态规划中的Bellman-Ford算法,我们已经知道,此时的Lij(2m)已经收敛等于Lij(n-1)
(不能含有负环)

ALL-PAIRS-SHORTEST-PATHS(W)
n = W.rows
L(1) = W 
m = 1
while m < n - 1
	let L(2m) be a new n x n matrix
	L(2m) = EXTEND-SHORTEST-PATHS(L(m), L(m)) 
	m = 2m
return L(m)

时间复杂度 O(n^3 logn)

Floyd-Warshall Algorithm
定义:dij(k) = i -> j的最短路径长度,其中间结点的下标取自{1, 2, …, k}中(下标)
目标:对于每一对结点i和j计算dij(n)
第k个结点不在路径中,第k个结点在路径中。只有这两种可能。
(不能含有负环)
在这里插入图片描述
伪代码:

FLOYD-WARSHALL ALGORITHM
n = W.rows
D(0) = W
for k = 1 to n
	let D(k) = (dij(k)) be a new n x n matrix
	for i = 1 to n
		for j = 1 to n
			dij(k) = min(dij(k-1), dik(k-1) + dkj(k-1))
return D(n)

时间复杂度 O(n^3)

可以含有负环的算法:Johnson’s ALgorithm


十二. 流网络(Flow Network)

应用例子:图像分割(image segmentation)、社区发现(community detection)一个流网络是一个多元组G = (V, E, s, t, c),前两个代表有向图V是顶点集合,E是边集合,s和t是两个特殊的顶点,s(source)是原点,t(sink)是汇点,c是容量,将一条边映射为一个实数并且对于E中的任意一条边e,有c(e) >= 0。

流是一个函数,满足两个约束即流量约束和流守恒约束。
容量约束(capacity constraints):对于E中的每一个e,有0 <= f(e) <= c(e)
流守恒约束(flow conservation constraints):对于V中的每一个点v(不包括s和t),有
∑ e   i n t o   v f ( e ) = ∑ e   o u t   o f   v f ( e ) \sum_{e \, into \, v}f(e) = \sum_{e \, out \, of \, v}f(e) eintovf(e)=eoutofvf(e)如果定义
inflow at v = fin(v)
outflow at v = fout(v)
则有对于每个结点(不包括s和t)则有fin(v) = fout(v)
流的值:val(f) = ∑ e   o u t   o f   s f ( e ) − ∑ e   i n t o   s f ( e ) \sum_{e \, out \, of \, s}f(e) - \sum_{e \, into \, s}f(e) eoutofsf(e)eintosf(e)
(注意下标)

最大流问题(Max-flow problem)
即最大化val(f)

最小割问题(Min-cut problem)
(最大流问题和最小割问题是对偶问题,即解决其中一个就解决了另一个)
定义:一个割是顶点集合V的二划分(A, B),s in A,t in B
定义:割的容量 c a p ( A , B ) = ∑ e   o u t   o f   A c ( e ) cap(A,B)=\sum_{e \, out\, of\, A}c(e) cap(A,B)=eoutofAc(e)
(直观理解:A中的顶点为黑点,B中的顶点为白点,割的容量指的是从黑点到白点的边的容量之和,不考虑从白点到黑点)
最小割问题是找一个容量最小的割

(贪心算法无法解决max-flow problem)
在这里插入图片描述
这个贪心算法fail,不能找到最优值(Augment被翻译为“增广”)
Q:Why fails?
A:一旦贪心算法增加了某条边的流量,就再也不能减小,无法undo
Solution:需要采取某种机制undo一个之前不好的选择

剩余网络(残差网络)(Residual network)
在这里插入图片描述
原网络G、剩余网络Gf、forward edge(前向边)、backward edge(后向边、反向边)
定义:一条增广路径(augmenting path)是在剩余网络中一条s -> t的简单路径
定义:增广路径P的瓶颈容量(bottleneck capacity)是剩余网络路径P上的边最小容量
增广操作:根据增广路径来调整值的操作
令f为流,P为Gf中的增广路径,调用f’ <- AUGMENT(f, c, P),结果f’是一个流并且val(f’) = val(f) + bottleneck(Gf, P)

AUGMENT(f, c, P)
temp <- bottleneck capacity of augmenting path P.
foreach edge e in P:
	if e in E 
		f(e) <- f(e) + temp
	else if e not in E and e in Ef
		f(e) <- f(e) - temp
return f

增广操作之后原网络G仍满足容量守恒和流约束守恒
This is easy to see. f ’ respects the capacity constraints since we never increased the flow beyond the residual capacity of any arc. Flow conservation constraints are still satisfied since flow is augmented along a path from s to t, and for any node other than s or t along the path, the flow entering the node, and the flow leaving the node both increase by δ. Finally, the s-t flow increases by δ since the net flow out of s increases by δ.
f’满足容量守恒,因为我们从未增加到超过剩余网络任意一条边容量的流量。流约束守恒也是满足的,因为流是通过s -> t的增广路径执行的增广操作而改变,并且在增广路径P上除s和t之外的任意一点,流入结点的流量和流出结点的流量都增加了相同的瓶颈容量temp。最后,s -> t的流增加了temp,因为离开s的流增加了temp
Ford-Fulkerson Algorithm
在这里插入图片描述
(剩余网络上没有s->t的增广路径即结束)

FORD-FULKERSON(G)
foreach edge e in E
	f(e) <- 0
Gf <- residual network of G with respect to flow f
while (there exists an s -> t path P in Gf)
	Gf(p) = min{Gf(u, v): (u, v) in P}
	for each edge (u, v) in P
		if (u, v) in E
			f(u, v) = f(u, v) + Gf(P)
		else 
			f(u, v) = f(u, v) - Gf(P)
	Update Gf
return f

该算法没有深入发掘网络的结构,即在选择增广路径时太随意,导致其时间复杂度不好,为伪多项式时间算法

引理:流值引理(Flow value lemma):令f为任意一个网络流,令(A, B)为任意一个割。那么,流f的值等于穿过割(A, B)的流量,即
v a l ( f ) = ∑ e   o u t   o f   s f ( e ) − ∑ e   i n t o   s f ( e ) = ∑ v   ∈   A ( ∑ e   o u t   o f   v f ( e ) − ∑ e   i n t o   v f ( e ) ) = ∑ e   o u t   o f   A f ( e ) − ∑ e   i n t o   A f ( e ) val(f) = \sum_{e \, out\, of\, s}f(e)-\sum_{e\, into\, s}f(e) =\sum_{v\, \in\,A}(\sum_{e\, out\, of\, v}f(e)-\sum_{e\, into\, v}f(e)) = \sum_{e\, out\, of\, A}f(e)-\sum_{e\, into\, A}f(e) val(f)=eoutofsf(e)eintosf(e)=vA(eoutofvf(e)eintovf(e))=eoutofAf(e)eintoAf(e)(注意下标)
(注意 割的容量 和 穿过割的流量 的区别)
第二个等号成立:根据流约束守恒,A中所有顶点除了s的都为0(s in A,t in B)
(剩余网络中容量为0的边,我们一般不画,因为对问题的解决毫无贡献)

弱对偶定理(Weak duality):令f为任意一个流,(A, B)为任意一个割,那么val(f) <= cap(A, B)
证明:val(f) = ∑ e   o u t   o f   A f ( e ) − ∑ e   i n t o   A f ( e ) < = ∑ e   o u t   o f   A f ( e ) < = ∑ e   o u t   o f   A c ( e ) = c a p ( A , B ) \sum_{e\, out\, of\, A}f(e) - \sum_{e\, into\, A}f(e) <= \sum_{e\, out\, of\, A}f(e) <= \sum_{e\, out\, of\, A}c(e) = cap(A, B) eoutofAf(e)eintoAf(e)<=eoutofAf(e)<=eoutofAc(e)=cap(A,B)
结论:令f为一个流,令(A, B)为一个割,如果val(f) = cap(A, B),那么f是最大流并且(A, B)是一个最小割。
证明:对于任意一个流f’: val(f’) <= cap(A, B) = val(f)
对于任意一个割(A’, B’): cap(A’, B’) >= val(f) = cap(A, B)

最大流最小割定理(max-flow min-cut theorem):最大流的值等于最小割的容量。
增广路径定理(augmenting path theorem)(非常重要):一个流f是最大流,当且仅当没有增广路径。
对于任意一个流f下面的三个条件是等价的:
i. 有一个割(A, B),且cap(A, B) = val(f)
ii. f是最大流
iii. 剩余网络中不存在增广路径

证明:
i -> ii: 根据弱对偶定理val(f) <= cap(A, B),而cap(A, B) = val(f),故f是最大流
ii -> iii: 假设此时在剩余网络中还存在增广路径,那么我们可以通过增广路径进一步地增大流f,与已知f是最大流矛盾,因此得证
iii-> i: 令f是一个不存在增广路径的流,令A是一组在剩余网络中从s出发可达的顶点集合,其余顶点构成集合B,则有一个割(A, B)。令e = (u, v)是横跨切割(A, B)的一条边。如果u在A中且v在B中,那么f(e) = c(e)(如果f(e) < c(e),那么在剩余网络中存在一条从u -> v的边,使s -> v可达,而s -> v不可达,因此f(e) = c(e)),如果u在B中且v在A中,那么f(e) = 0(如果f(e) > 0,那么在剩余网路中存在从则存在v-> u的边,使s -> u可达,而s -> u不可达,因此f(e) = 0)。由流值定理 v a l ( f ) = ∑ e   o u t   o f   A f ( e ) − ∑ e   i n t o   A f ( e ) = ∑ e   o u t   o f   A c ( e ) − 0 = c a p ( A , B ) val(f) = \sum_{e\, out\, of\, A}f(e) - \sum_{e\, into\, A}f(e) = \sum_{e\, out\, of\, A}c(e) - 0 = cap(A, B) val(f)=eoutofAf(e)eintoAf(e)=eoutofAc(e)0=cap(A,B),得证。

时间复杂度分析:
假设1:每条边的容量都是整数(这个假设是合理的,若是无理数可能存在无法收敛的情况,可以自己再去了解相关信息)
假设2:每条边的容量在1和C之间
Ford-Fulkerson每次迭代至少增加1,那么val(f) <= nC即O(nC),考虑一种极端的情况,s和t之外的每个顶点的前驱顶点为s,后继顶点为t,那么为(n-2)C。在剩余网络上找一条增广路径可以用BFS或DFS,二者的时间复杂度都为O(m+n),因此总的时间复杂度为O(mnc - n^2C) = O(mnC),因为m > n,因此忽略n^2C。这个算法的时间复杂度不是多项式时间算法,是伪多项式时间算法,因为有C存在,这和之前背包算法中的类似。


十三. NP完全性(NP Completeness)

十二.NP完全性(NP Completeness)
问题有两种描述方式,优化模式(Optimization Problem)和判别模式(Decision Problem)
在这里插入图片描述
归约:一个步骤f,可以将问题A的任何一个实例都转换成问题B的实例并且要满足下面的两个要求。
Transformation:这个转换过程必须在多项式时间内完成。
Equivalence:一个实例或一个项,必须同时为YES或NO。
(注意下面的定义,“问题A可以在多项式时间内被归约为问题B”,小p指的是多项式时间)
在这里插入图片描述

定理:如果问题B可以在多项式时间内被解决,那么A也可以在多项式时间内被解决。
证明:
(1)给定一个问题A的一个实例alpha,使用多项式时间归约为beita = f(alpha)
(2)运用B的多项式时间算法,解决实例beita = f(alpha)
(3)使用beita的答案作为alpha的答案

定理:相反地,如果A不存在多项式时间算法,那么B也不存在多项式时间算法(该定理是之前定理的逆否命题)

题:
在这里插入图片描述
选C。C就是之前说的逆否命题。D不行,因为X可能存在一个多项式时间算法,但通过归约后的Y可以不存在多项式时间算法。
在这里插入图片描述
Vertex Cover顶点覆盖问题
输入:一个图G = (V, E)和一个整数k
输出:是否存在一个顶点集合S属于V,|S| = k,使得每条边至少和这k个顶点中的一个相关联,能找到为YES,找不到为NO
Set Cover集合覆盖问题
输入:一个含有n个元素的集合U = {e1, e2, …, en},给一系列的m个子集S = {S1, S2, …, Sm},和一个整数k
输出:是否存在一个集合C属于S,使得C中各集合的元素的并集为U并且|C| <= k,存在为YES,不存在为NO
在这里插入图片描述
定理:顶点覆盖问题可以在多项式时间内归约为集合覆盖问题
证明:给一个顶点集合实例G = (V, E)和k,我们构造一个集合覆盖实例(U, S, k),使得S中的k个集合能够覆盖U,当且仅当G有一个大小为k的顶点集合覆盖。
构造(Construction):
·令U = E
·对于V中的每一个v,构造一个集合Sv = {e属于E,e关联于v}
(简单点说,就是E中的每一条边就是U中的每一个元素,V中的每个顶点就是S中的每个集合,V中顶点关联的边就是S中集合的元素)

断言(Claim):G = (V, E)有一个大小为k的顶点覆盖当且仅当(U, S)有一个大小为k的集合覆盖
证明:
=>
令X是V的子集,是一个在G中大小为k的顶点覆盖
令Y = {Sv : v属于X}是一个大小为k的集合覆盖
<=
令Y属于S,是一个大小为k的集合覆盖
令X = {v : Sv属于Y}是一个在G中大小为k的顶点覆盖

Construction + Claim => 顶点覆盖可以在多项式时间内归约为集合覆盖
(也就是说顶点覆盖比集合覆盖在计算上更容易,因此用式子表示式小于等于号)

3SAT问题(3-SATISFIABILITY Problem)
在这里插入图片描述
在这里插入图片描述
3SAT Problem是SAT问题的特殊情况,即每一个子句中恰好包含了3个文字

独立集问题(Independent Set Problem)
输入:给定一个图G = (V, E),和一个整数k
输出:是否存在一组顶点的集合S属于V,|S| = k,使得没有在S中没有两个顶点由一条边相连(即集合S中的k个顶点,两两不相邻)

定理:3SAT问题可以在多项式时间内归约为独立集问题
在这里插入图片描述
证明:给定一个3SAT问题的实例,我们构造一个独立集问题(G, k),其有一个大小为k的独立集,当且仅当3SAT问题的实例是可满足的
构造(Construction):
·每个子句中的1个文字对应1个顶点
·每个子句中的3个文字所对应的3个顶点,两两相邻,构成一个三角
·不同子句间互为相反的文字连接起来
在这里插入图片描述

断言(Claim): Φ \Phi Φ是可满足的,当且仅当G有一个大小为k = ∣ Φ ∣ |\Phi| Φ的独立集
(注:k为子句的数量,在本例中k = 4)
=>:
由于3SAT为TRUE,那么每个子句中至少有一个为TRUE,从每个子句中选择一个为真的文字为顶点。选中的顶点不可能处于同一个三角(每个三角只选择一个顶点)并且不同三角中的顶点不可能相邻(相邻的都是互为相反的),这些顶点构成了一个大小为k的独立集。
<=:
令S是一个大小为k的独立集,因此任意两个顶点不可能处于同一个三角。那么每个三角对应的子句都为TRUE,3SAT问题为TRUE

<=p 是一个偏序关系,满足自反、反对称和传递。
在这里插入图片描述
在这里插入图片描述
P类(P Class)
可以在多项式时间内解决的问题(排序、最小生成树、拓扑排序、遍历问题、最短路径问题)

NP类(NP Class)
一种定义:比P类问题要求低。给一个解,可以在多项式时间内验证是否正确的问题。
另一种定义:Nondeterministic Polynomial time非确定多项式时间。在非确定图灵机上在多项式时间内求解 对应于 在图灵机上多项式时间验证。

P类问题是NP类问题的一个子集。

关键的问题是:P == NP?
在这里插入图片描述
NP完全问题的定义要记住。
在这里插入图片描述

感受

这门课实在是太棒了,大爱《算法导论》,大爱“算法设计与分析”这门课。刘老师讲的也非常好,非常感谢!剩余没讲的内容,我会在之后一点点学完。我复习所根据的就是这个笔记,希望能对大家有所帮助。

  • 17
    点赞
  • 82
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值