Skip to content

Kruskal

概述

K算法解决无向带权图的最小支撑树问题。输入图G,该算法从T=(V(G),)出发,按照边权从小到大每次选择一条边,如果T加上这条边不会产生圈,更新T=T+e否则检查下一条边。最终,如果T边数达到n1,则输出T为图G的最小支撑树,否则,则该图不连通。

算法伪代码及代码

伪代码

  1. Sort the edge such that c(e1)c(e2)c(em)
  2. Set T=(V(G),)
  3. For i=1 to m do:if T+e contains no cycle then T=T+e

代码

算法的关键点在于如何判断添加边e后是否有圈,下面是使用标号判断法的代码示例。

python
def K_sig(vvw, n):  # 输入三元组,标号判断回路的Kruskal算法
    vvw = sorted(vvw, key=lambda x: x[2])  # 使用lambda函数,意为 key(x) = x[2]
    parent = [i for i in range(n)]  # 首先以每个点作为一棵子树
    tree = []
    for edge in vvw:
        root1 = parent[edge[0]]
        root2 = parent[edge[1]]
        if root1 != root2:
            tree.append(edge)
            if len(tree) == n - 1:
                break
            for i in range(n):
                if parent[i] == root2:
                    parent[i] = root1
    if len(tree) < n - 1:
        print("Don't connected")
    else:
        print(tree)


vvw = [
    (0, 1, 1),
    (0, 2, 2),
    (1, 2, 2),
    (1, 3, 4),
    (1, 4, 3),
    (3, 4, 2),
    (2, 4, 4),
    (2, 3, 4),
]
K_sig(vvw, 5)

算法复杂度分析

标号判断 O(mlogn+n2)
根数判断O(mlogn)
路径压缩O(mα(n)),α(n)为阿克曼数A(n,1)的逆

Graph-scanning Algorithm

概述

输入图G及一些点s,算法给出图G中点s能够到达的所有点的集合R,并记录一个边集T,满足(R,T)为树。

伪代码及代码

伪代码

  1. Set R={s},Q={s}andT=,
  2. If Q=, then stop;else choose vQ
  3. choose a wV(G)R with e=(v,w)E(G),if there is no such w then set Q=Q{v},go to 2
  4. R=R{w},Q=Q{w},T=T{e}, go to 2

代码

python
test_list = [[1,2,3],[0,5],[0,3,5],[0,2,4],[3,5],[1,4]]
n = 6
s = 0
def Graph_Scan(adjlist,n,s):
    """
    Input: adjlist 邻接表
           n 点数
           s 起始点
    Output: R 从s出发能够到达的所有点的集合
            T R、T组成一个以s为根的树形图/树
    """
    viewed = [0]*n
    viewed[s] = 1
    R = [s]
    Q = [s]
    T = []
    while len(Q) != 0:
        v = Q[-1]
        w = s
        for vertex in adjlist[v]:
            if viewed[vertex] == 0:
                w = vertex
                break
        if w == s: 
            Q.pop()
            break
        else:
            R.append(w)
            Q.append(w)
            T.append([v,w])
            viewed[w] = 1
    return R,T

print(Graph_Scan(test_list,n,s))

正确性及算法复杂度

正确性

首先,(R,T)在任何时间步下都是树,这是由w仅从不在R中的点选取决定的。验证算法正确性,只需证明R确实包含了从s出发所有可达点。
假设ws可达但wR,令P表示一条ws路,令x,y表示在路P上且满足xR,yR的点。算法仅在Q=时结束,而将xQ中删除需要VR中不含与x相邻的点,但显然,yVR(x,y)E(G),产生矛盾。定理证明完成。

算法复杂度

数据结构采用邻接表的情况下,可以证明算法复杂度达到O(m)
邻接表下,采用深度优先搜索,则每次搜索都是由邻接表存储的起点出发,沿指针指向的搜索所有存储单元,最终该算法在邻接表上遍历一次,故算法复杂度为O(m)

Prim Algorithm

概述

在割中找最小权的边

伪代码和代码

伪代码

  1. Choose vV,Set T=({v},)
  2. while V(T)V(G):choose an edge eδG(V(T)) of minmum weight, T=T+e

代码

python
def Triarray2adjlist(n, vvw):  # 将三元数组转化为邻接表的形式
    adjlist = []
    for i in range(n):
        col_edge = []
        for edge in vvw:
            v1, v2 = edge[0], edge[1]
            if v1 == i:
                col_edge.append((v2, edge[2]))
            if v2 == i:
                col_edge.append((v1, edge[2]))
        adjlist.append(col_edge)
    return adjlist


def Prim(n, adjlist, startv):  # 输入邻接表
    path = [-1] * n
    lowcost = [float("inf")] * n  # 初始定义所有点cost为无穷,起始点cost为0
    lowcost[startv] = 0
    left = set() # 记录未参与到划分中的点。
    left.add(startv)
    while len(left) > 0:
        min, k = float("inf"), -1
        for i in left:
            if lowcost[i] < min:
                min, k = lowcost[i], i
        if k >= 0:
            left.remove(k)
        print("edge  =(" + str(path[k]) + "," + str(k) + "), cost = " + str(lowcost[k]))
        col_edge = adjlist[k] # 刚检测的点的邻接表所在行
        for edge in col_edge:
            j = edge[0]
            if lowcost[j] > edge[1] and j in left: # j in left 才能进行
                lowcost[j], path[j] = edge[1], k
            elif lowcost[j] == float("inf"): # 在首次将所有的都加入了left,并对与现有划分求距离。
                lowcost[j], path[j] = edge[1], k
                left.add(j)  # add


vvw = [
    (0, 1, 1),
    (0, 2, 2),
    (1, 2, 2),
    (1, 3, 4),
    (1, 4, 3),
    (3, 4, 2),
    (2, 4, 4),
    (2, 3, 4),
]
adjlist = Triarray2adjlist(5, vvw)
Prim(5, adjlist, 0)

算法复杂度分析

在Prim算法的一般实现中,需要用到两个一维数组,使用
lowcost[1:n]记录还未进入T中的点的集合U中的各顶点与i的连边中权最小的边的权值。
adj[1:n]保存上述边的除i的令一个端点
算法实现需要

choosei,U={1,2,...n}{i},lowcost[i]=0while|U|>0:computekst,lowcost[k]=minjUlowcost[j]U=Uk,forall(k,i)E:lowcost[i]={lowcost[i],ifwkilowcost[i]wki,elseadj[i]={adj[i],ifwiklowcost[i]k,else

在上述实现过程中,选择k进行比较的过程中,第一次需要n2次,第二次需要n3次...,总共需要(n2)(n1)2次比较。而对lowcostadj进行更新,如果采用邻接表,显然每条边恰好被检查两次,则最终复杂度O(m+n2),而使用邻接矩阵O(n2)

算法改进

采用二叉树数据结构可以改进到O(mlogn),使用斐波那契堆可改进到O(m+nlogn)

Push-relabel Algorithm

概述

显然有两个过程,推流(更新f)重标(更新ψ)

符号定义

  • 超出量exexf(v)=eδ(v)f(e)eδ+(v)f(e),如果exf(v)=0,我们称f正在点v守恒。
  • 预流f:如果其满足f(e)μ(e)exf(e)0,那么f(e)为一个预流。特别的,如果exf(v)=0,vV(G){s},那么f为一个流。
  • 活动点:vV(G){s,t}exf(v)>0则称v为一个活动点。
  • 距离标号及容许的:(G,μ,s,t)为一个网络且f为预流。距离标号ψ:V(G)Z+且满足ψ(t)=0,ψ(s)=n以及(v,w)E(Gf),ψ(v)ψ(w)+1,特别的若一条边e=(v,w)E(G)eE(Gf),ψ(v)=ψ(w)+1,那么称它是容许的。

算法伪代码

伪代码

  1. For every eδ+(s): f(e)=μ(e).
    The other e: f(e)=0
  2. ψ(s)=n, for vV(G){s}: ψ(v)=0.
  3. While exist active vertex, choose active one v:
    if \existeδ+(v) and e is admissible: PUSH(e),else RELABEL(v).

PUSH

  1. compute γ=min{exf(v),μf(e)}
  2. Augment f along e by γ

RELABEL
Set ψ(v)=min{ψ(w)+1:(v,w)δGf+(v)}

代码

正确性及复杂度分析

算法复杂度为O(nm2),证明繁琐。
对其正确性给出如下证明,首先,在算法执行过程中,容易证明f,ψst预流和关于f的距离标号。当算法停止时,此时无活动点,f为一个st流,下证Gf不存在st路。
对于Gf的任一vwP=v0=v,v1,,vk=w\existψψ(vi)ψ(vi+1)+1,i=0,,k。那么ψ(v0)ψ(vk)+k。注意到,路长kn1。而ψ(s)=n,ψ(t)=0,故没有st路,则f为最大流。

Srong Connected component in digraph

概述

事实上算法在做这样的事情:

  1. 以DFS按有向图的所有点出发,得到树形森林,将点按照搜索过程出栈顺序放入Ψ中。
  2. Ψ中的点从后向前取点w,以w出发按图逆邻接表能搜到且为进入其他强连通分支的所有点构成一个强连通分支。

伪代码及代码

伪代码

这里,Comp(v)记录了点v属于的连通分支。Ψ在这里作为一个映射,将点映射到出栈序号。

SetR=,SetN=0ForallvinV(G)do:ifvRthenVISIT1(v)SetR=,SetK=0Fori=|V(G)|downto1do:ifΨ1(i)RthenSetK=K+1andVISIT2(Ψ1(i))

VISIT1

SetR=R{v}Forallwwith(v,w)Edo:ifwRthenVISIT1(w)SetN=N+1,Ψ(v)=NandΨ1(N)=v

VISIT2

SetR=R{v}Forallwwith(v,w)Edo:ifwRthenVISIT2(w)SetComp(v)=K

代码

python
n = 7
adjlist = [[5],[2],[1],[1,5],[0,1,2,3],[4],[2,4]]
viewed = [0]*n
P_dfs = []
scc = []
def dfs(adjlist,u): # 给到出栈顺序
    viewed[u] = 1
    for v in adjlist[u]:
        if not viewed[v]:
            dfs(adjlist,v)
    P_dfs.append(u)

def reverse_adj(adjlist,n):
    r_adjlist = [[] for _ in range(n)]
    for v in range(n):
        if len(adjlist[v])!= 0:
            for w in adjlist[v]:
                r_adjlist[w].append(v)
    return r_adjlist

def rdfs(adjlist,u,scc):
    viewed[u] = 1
    scc.append(u)
    for v in adjlist[u]:
        if not viewed[v]:
            rdfs(adjlist,v,scc)

dfs(adjlist,0) # 第一次dfs
P_dfs.reverse() # 反回出栈顺序,由于第二次dfs从后向前取点
viewed = [0]*n # 重置一下
r_adj = reverse_adj(adjlist,n) # 反向邻接表
for i in range(n):
    if not viewed[i]: # i还不在任何强连通分支中
        scc = []
        rdfs(r_adj,i,scc)
        print(scc)

可行性及算法复杂度

可行性

事实上,如果u,v属于同一个强连通分支,那么Comp(u)=Comp(v)是显然的,只需证明当Comp(u)=Comp(v)时,u,v确实属于同一个强连通分支。
r(u),r(v)表示从u,v可到达的且Ψ值最大的点。由于Comp(u)=Comp(v)那么一定有r=r(u)=r(v)Ψ(r)Ψ(u),这说明ruv皆可达。同时,因为Ψ(r)Ψ(u),这说明当u加入R时,rR,故有一条ru路,于是u是从r可达的,同理对v成立。故,u,v在同一个连通分支中,证明完成。

算法复杂度

算法复杂度为O(m+n)显然。

Floyd-warshall Algorithm

概述

FW算法用来解决图中所有点对间最短路的问题,即输入有向图D及其边权重,通过执行该算法可以得到每个点对s,t间的最短路长lst和最短路径pst
FW算法事实上在做这样一件事情:比较点对之间的经过中转点与不经过中转点的距离哪个更优。

伪代码及代码

初始化SetDij=c(i,j)forall(i,j)E(G).SetDij=forall(i,j)E(V(G)×V(G)E(G))withij.SetDii=0foralli.SetPij=iforall(i,j)E(G).迭代过程Fork=1tondo:Fori=1tondo:ifikthen:Forj=1tondo:ifjkthen:iflij>lik+lkjthensetlij=lik+lkjandpik=pjk.
python
vvw=[(1,2,3),(1,3,8),(1,5,-4),(2,4,1),(2,5,7),(3,2,4),(4,1,2),(4,3,-5),(5,4,6)]
n = 5
def find_mini_path(vvw,n):
    '''
    求解所有点对间的最短路径算法
    Input
    vvw :三元组邻接表
    n :点数
    Output
    D :D[i][j]表示点i到点j的最短加权路的长度
    P :P[i][j]表示i到j途径的点。
    '''
    MAX = float('inf')
    m = len(vvw)
    D = [[MAX for i in range(n)] for j in range(n)]
    P = [[MAX for i in range(n)] for j in range(n)]
    for i in range(m):
        v1,v2 = vvw[i][0]-1,vvw[i][1]-1
        D[v1][v2]=vvw[i][2]
        P[v1][v2] = v1+1 # 注意,这里由于在输入时点记号为1-5,python第一位为0,方便将元素与点对应起来改为+1
    for k in range(n): # 注意,中转点位于最外层
        for i in range(n):
            for j in range(n):
                if D[i][k] + D[k][j] < D[i][j]:
                    D[i][j] =  D[i][k] + D[k][j]
                    P[i][j] = P[k][j]
    return D,P

D,P = find_mini_path(vvw,n)
print(D,P)

正确性及算法复杂度证明

在守恒权的假设下,FW算法能够正确运行,并具有O(n3)的时间复杂度。

TIP

注意到算法对中转点的循环位于最外层,接下来说明,FW迭代过程中,Dij(k)代表节点对vivj不经过点{vk+1,vk+2,,vn}时的最短路。对于k=0显然成立,只要证明k=n时成立,则得到算法正确性,采用归纳假设。而时间复杂度显然。

k=0由初始化过程保证成立。假设对k=k0时成立,令Dij表示k0情况下(即不经过{vk+1,,vn}时)ij最短路,那么k0+1情况下(即不经过{vk+2,,vn}时):
在守恒权的保证下,外循环k0k0+1条件下vivk+1vk+1vj的最短路径相同。外循环k0+1时,vivj经过vk+1时的路径长度大于Di,j时,显然定理成立;反之,我们需证明更新时Di,k+1+Dk+1,j确实记录了该路径的长度,事实上,只要证明vivk+1vk+1vj最短路径不交即可。
vivk+1的最短路径为P, vk+1vj的最短路径为Q。反证,若PQ,则PQ上我们能够找到一个极大圈R,且一定有vk+1R。由守恒权假设,(PQ)R是一条比Di,j更短的vivj路,且不经过{k+1,,n},这与Di,j含义矛盾。

Dinic's Algorithm

概述

解决最大流问题的算法,游戏打多了头疼,改天再仔细看。

符号定义

  • 剩余容量μf:μf(e)=μ(e)f(e),μf(e)=f(e)。其中f表示流,e代表反向弧。
  • 残余图Gf:Gf=(V(G),{eE(G),μf(e)>0})
  • 分层图GfL:GfL=(V(G),{(x,y)E(Gf),dGf(s,y)=dGf(s,x)+1}),
  • 阻塞流:一个stf如果(V(G),{eE(G),f(e)<μ(e)})中没有st路,那么f为阻塞流。注意,阻塞流未必是最大流。

算法伪代码与代码

伪代码

  1. Set f(e)=0 for all eE(G)
  2. Construct GfL of Gf
  3. Find a block flow f in (GfL,μf), if f=0 then stop
  4. Replace f with f and go to 2.

代码

算法正确性证明

该算法在有限时间内能够找到最大流。
首先,说明每一次迭代,最短可扩路长度严格增加。令f为某一次循环开始前的流,f为执行过step 4后当前的流。对于f对应的增广路,他一定包含GfL之外的边,所以,他将包含比dGf(s,t)更多的边,于是,最短可扩路随迭代次数严格增加。那么,之多n1次循环后,该算法停止,此时已没有可扩路,这说明找到了最大流。