算法专题:Graph Theory

图论Graph Theory是CS里面相当重要的一个领域,也是非常博大精深的一块。这里主要实现一些比较基础的算法。
图可以分为有向图和无向图,有权图和无权图。图的基本表示方法有邻接矩阵,邻接链表。两者可以互相转换,这里都用邻接链表作为图的表示。

BFS/DFS
BFS和DFS是图的遍历的基础算法,就是从某一个节点开始遍历整个图。对图的有向性和有权性并没有要求,对无向图可视为每条边都是双向的。
简而言之,BFS就是维持一个queue,每次把节点的未遍历neighbors放进这个队列,重复此过程直至队列为空。这种方式是层层递进的。DFS则是一条道先走到黑,然后再换一条路,用递归实现会简洁很多。两个方法都需要一个辅助空间visited来记录已经遍历过的节点,避免走回头路。假如需要记录路径,可以用path代替visited。
graph = {‘A’: [‘B’, ‘C’], ‘B’: [‘D’, ‘E’], ‘C’: [‘D’, ‘E’], ‘D’: [‘E’], ‘E’: [‘A’]}

# BFS DFS
def recursive_dfs(graph, start, path=[]):
    path = path + [start]
    for node in graph[start]:
        if node not in path:
            path = recursive_dfs(graph, node, path)
    return path

def iterative_dfs(graph, start):
    path = []
    # dfs uses a stack, so that it visits the last node
    # pushed to stack (explores as deep as possible first)
    stack = [start]
    while stack:
        v = stack.pop()  # pops out the last in (go deeper)
        if v not in path:
            path += [v]  # add v to path
            stack += graph[v]  # push v's neighbors to stack
    return path

def iterative_bfs(graph, start):
    path = []
    queue = [start]
    # bfs uses a queue, so that it visits the nodes pushed
    # to the queue first. So it first visits all the neighbors
    # and then the neighbors' neighbors (explores the soroundings
    # as much as possible without going deep)
    while queue:
        v = queue.pop(0)  # pops out the first in (go wider)
        if v not in path:
            path += [v]
            queue += graph[v]  # push v's neighbors to queue
     return path 

两种方法各有千秋。T:O(V+E) S:O(V)

** Dijkstra**
问题:在无向图G=(V,E)中,假设每条边E[i]的长度为W[i],找到顶点V0到其余各点的最短路径。这个算法是典型的单源最短路径算法,要求不能出现负权值,即W[i]>=0.
算法的思想很简单。在算法进行中的某个时刻,整个图的所有顶点可以分成两块,一块是已经完成的,一块是待完成的。那么待完成的肯定有一部分是和已经完成的部分相连的,因此它们距离源点V0的路径长度也是可以获得的,从里面挑一个最小的加入已经完成的部分,并更新这个点的下一层neighbors的可能路径值。重复步骤直至所有点都完成。
事实上,因为刚开始只有W[i]的值,可以给每个点增加一个属性,即到V0的最短路径。刚开始,只有V0到自己的值是0,别的点都是正无穷。然后考虑V0的neighbors,其可能(因为可能有更短的路径)的路径值就是V0的路径值加上V0到其的W[i],选择一个最小的,确定其路径值。然后再考虑V0和刚刚加进来的点的neighbors的可能路径值,再找一个最小的。反复直至完成。

# Dijkstra T:O(V^2) S:O(V)
def popmin(pqueue):
    # A (ascending or min) priority queue keeps element with
    # lowest priority on top. So pop function pops out the element with
    # lowest value. It can be implemented as sorted or unsorted array
    # (dictionary in this case) or as a tree (lowest priority element is
    # root of tree)
    lowest = 1000
    keylowest = None
    for key in pqueue:
        if pqueue[key] < lowest:
            lowest = pqueue[key]
            keylowest = key
    del pqueue[keylowest]
    return keylowest

def dijkstra(graph, start):
    # Using priority queue to keep track of minium distance from start
    # to a vertex.
    pqueue = {}  # vertex: distance to start
    dist = {}  # vertex: distance to start
    pred = {}  # vertex: previous (predecesor) vertex in shortest path
    # initializing dictionaries
    for v in graph:
        dist[v] = 1000
        pred[v] = -1
    dist[start] = 0
    for v in graph:
        pqueue[v] = dist[v]  # equivalent to push into queue

    while pqueue:
        u = popmin(pqueue)  # for priority queues, pop will get the element with smallest value
        for v in graph[u].keys():  # for each neighbor of u
            w = graph[u][v]  # distance u to v
            newdist = dist[u] + w
            if (newdist < dist[v]):  # is new distance shorter than one in dist?
                # found new shorter distance. save it
                pqueue[v] = newdist
                dist[v] = newdist
                pred[v] = u

     return dist, pred 

可以看到,这里对popmin的实现是用的比较原始的O(n)方法,假如用堆的话,可以将效率提高为O(ElogV)。

** Bellman-Ford**
上面提到,Dijkstra不能在含有负权边的图上使用,而Bellman-Ford算法可以。但是这个算法效率更低,为O(VE)。假如有负权回路,会报错而不会继续进行计算(负权回路的存在让最短路径变得没有意义)。
Bellman-Ford算法可以大致分为三个部分:
第一,初始化所有点。每一个点保存一个值,表示从原点到达这个点的距离,将原点的值设为0,其它的点的值设为无穷大(表示不可达)。
第二,进行循环,循环下标为从1到n-1(n等于图中点的个数)。在循环内部,遍历所有的边,进行松弛计算。
第三,遍历途中所有的边(edge(u,v)),判断是否存在这样情况:
d(v) > d (u) + w(u,v)
有则返回false,表示途中存在从源点可达的权为负的回路。
为什么要循环n-1次?因为最短路径最多n-1条边(不会包含环)。

# Bellman-Ford T:O(VE) S:O(V)
# Step 1: For each node prepare the destination and predecessor
def initialize(graph, source):
    d = {}  # Stands for destination
    p = {}  # Stands for predecessor
    for node in graph:
        d[node] = float('Inf')  # We start admiting that the rest of nodes are very very far
        p[node] = None
    d[source] = 0  # For the source we know how to reach
    return d, p


def relax(node, neighbour, graph, d, p):
    # Step 2: If the distance between the node and the neighbour is lower than the one I have now
    if d[neighbour] > d[node] + graph[node][neighbour]:
        # Record this lower distance
        d[neighbour] = d[node] + graph[node][neighbour]
        p[neighbour] = node


def bellman_ford(graph, source):
    d, p = initialize(graph, source)
    for i in range(len(graph) - 1):  # Run this until is converges
        for u in graph:
            for v in graph[u]:  # For each neighbour of u
                relax(u, v, graph, d, p)  # Lets relax it

    # Step 3: check for negative-weight cycles
    for u in graph:
        for v in graph[u]:
            assert d[v] <= d[u] + graph[u][v]

     return d, p 

Flord-Wayshall
该算法用于求图中任意两点的最短路径,复杂度O(V^3)。这个算法是基于DP的一种算法。思想也非常简单,考虑节点u到节点v的距离为d[u][v],假设有某个节点k使得u到k然后再到v的距离比原来的小,那就替换之。

# Floyd-Warshall T:O(V^3) S:O(V)
def floydwarshall(graph):
    # Initialize dist and pred:
    # copy graph into dist, but add infinite where there is
    # no edge, and 0 in the diagonal
    dist = {}
    pred = {}
    for u in graph:
        dist[u] = {}
        pred[u] = {}
        for v in graph:
            dist[u][v] = 1000
            pred[u][v] = -1
        dist[u][u] = 0
        for neighbor in graph[u]:
            dist[u][neighbor] = graph[u][neighbor]
            pred[u][neighbor] = u

    for t in graph:
        # given dist u to v, check if path u - t - v is shorter
        for u in graph:
            for v in graph:
                newdist = dist[u][t] + dist[t][v]
                if newdist < dist[u][v]:
                    dist[u][v] = newdist
                    pred[u][v] = pred[t][v]  # route new path through t
 return dist, pred
    原文作者:akak18183
    原文地址: https://www.jianshu.com/p/df89db5e9cf1
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞