算法专题: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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容