利用广度优先搜索图的算法解决词梯问题

百般思念 提交于 2020-08-05 00:12:16

词梯问题

考虑这样⼀个任务:将单词FOOL转换成SAGE。在解决词梯问题时,必须每次只替换⼀个字⺟,并且每⼀步的结果都必须是⼀个单词,⽽不能是不存在的词。

eg: FOOL -> POOL -> POLL -> POLE -> PALE -> SALE -> SAGE

我们研究从起始单词转换到结束单词所需的最小步数

用图算法解决词梯问题的步骤:

  1. 用图表示单词之间的关系。
  2. 用一种名为广度优先搜索的图算法找到从起始单词到结束单词的最短路径。

构建词梯图

第⼀个问题是如何⽤图来表⽰⼤的单词集合。如果两个单词的区别仅在于有⼀个不同的字⺟,就⽤⼀条边将它们相连。如果能创建这样⼀个图,那么其中的任意⼀条连接两个单词的路径就是词梯问题的⼀个解。下图展示了一个小型图,可用于解决从FOOL到SAGE的词梯问题。注意,它是无向图,并且边没有权重。

创建这个图有多种方式。假设有一个单词列表,其中每个单词的长度都相同。

  1. 为每个单词创建顶点。
  2. 如果两个单词只相差⼀个字⺟,就可以在图中创建⼀条边,将它们连接起来。

对于只有少量单词的情况,这个算法还不错。但是,假设列表中有5110个单词,将⼀个单词与列表中的其他所有单词进⾏⽐较,时间复杂度为 。对于5110个单词来说,这意味着要进⾏2600多万次⽐较。那实在是太糟糕了。

采⽤下述⽅法,可以更⾼效地构建这个关系图。假设有数⽬巨⼤的桶,每⼀个桶上都标有⼀个⻓度为4的单词,但是某⼀个字⺟被下划线代替。当处理列表中的每⼀个单词时,将它与桶上的标签进⾏⽐较。使⽤下划线作为通配符,我们将POPE和POPS放⼊同⼀个桶中。⼀旦将所有单词都放⼊对应的桶中之后,我们就知道,同⼀个桶中的单词⼀定是相连的。

在Python中,可以通过字典来实现上述⽅法。字典的键就是桶上的标签,值就是对应的单词列表。⼀旦构建好字典,就能利⽤它来创建图。

  1. 为每个单词创建顶点。
  2. 在字典中对应同⼀个键的单词之间创建边。

from Graphadj import Graph

def create_word_graph(word_file):
    graph = Graph()
    d = {}

    fp_words = open('word.txt', 'r')

    for line in fp_words:
        word = line[ : -1]

        for i in range(len(word)):
            bucket = word[ : i] + '_' + word[i+1: ]

            if bucket in d:
                d[bucket].append(word)
            else:
                d[bucket] = [word]

        for bucket in d.keys():
            for word1 in d[bucket]:
                for word2 in d[bucket]:
                    if word1 != word2:
                        graph.add_edge(word1, word2)

    return graph

实现广度优先搜索

广度优先搜索(breadth first search,以下简称BFS)是最简单的图搜索算法之一,给定图G和起点,通过边来访问G中与s之间存在路径的顶点。BFS的一个重要特性是,它会在访问完所有与s相距为k的顶点之后再去访问与s相距k+1的顶点。为了理解这种搜索⾏为,可以想象BFS以每次⽣成⼀层的⽅式构建⼀棵树。它会在访问任意⼀个孙节点之前将起点的所有⼦节点都添加进来。

为了记录进度,BFS会将顶点标记成⽩⾊、灰⾊或⿊⾊。在构建时,所有顶点都被初始化成⽩⾊。⽩⾊代表该顶点没有被访问过。当顶点第⼀次被访问时,它就会被标记为灰⾊;当BFS完成对该顶点的访问之后,它就会被标记为⿊⾊。这意味着⼀旦顶点变为⿊⾊,就没有⽩⾊顶点与之相连。灰⾊顶点仍然可能与⼀些⽩⾊顶点相连,这意味着还有额外的顶点可以访问。

from Graphadj import Graph, Vertex
from queue import Queue

def bfs(g, start):
    start.set_distance(0)
    start.set_predecessor(None)
    vertex_queue = Queue()
    vertex_queue.enqueue(start)

    while vertex_queue.size() > 0:
        current_vertex = vertex_queue.dequeue()

        for nbr in current_vertex.get_connections():
            if nbr.get_color() == "white":
                nbr.set_color('grey')
                nbr.set_distance(current_vertex.get_distance() + 1)
                nbr.set_predecessor(current_vertex)
                vertex_queue.enqueue(nbr)

        current_vertex.set_color('black')

BFS从起点 开始,将它标记为灰⾊,以表⽰正在访问它。另外两个变量,distance 和predecessor ,被分别初始化为0 和None 。随后,start 被放⼊Queue 中。下⼀步是系统化地访问位于队列头部的顶点。我们通过遍历邻接表来访问新的顶点。在访问每⼀个新顶点时,都会检查它的颜⾊。如果是⽩⾊,说明顶点没有被访问过,那么就执⾏以下4步。

  1. 将新的未访问顶点nbr 标记成灰⾊。
  2. 将nbr 的predecessor 设置成当前顶点currentVert 。
  3. 将nbr 的distance 设置成到currentVert 的distance 加1。
  4. 将nbr 添加到队列的尾部。这样做为之后访问该顶点做好了准备。但是,要等到currentVert 邻接表中的所有其他顶点都被访问之后才能访问该顶点。

来看看bfs 函数如何构建对应于图7-5的宽度优先搜索树。从顶点fool开始,将所有与之相连的顶点都添加到树中。相邻的顶点有pool、foil、foul,以及cool。它们都被添加到队列中,作为之后要访问的顶点。下图展示了正在构建中的树以及完成这一步之后的队列。

接下来,bfs 函数从队列头部移除下⼀个顶点(pool)并对它的邻接顶点重复之前的过程。但是,当检查cool的时候,bfs 函数发现它的颜⾊已经被标记为了灰⾊。这意味着从起点到cool有⼀条更短的路径,并且cool已经被添加到了队列中。下图展⽰了树和队列的新状态。

队列中的下⼀个顶点是foil。唯⼀能添加的新顶点是fail。当bfs 函数继续处理队列时,后⾯的两个顶点都没有可供添加到队列和树中的新顶点。下图展⽰了树和队列在扩展了第2层之后的状态。

请继续研究bfs 函数,直到能够理解其原理为⽌。⾮常神奇的⼀点是,我们不仅解决了⼀开始提出的从FOOL转换成SAGE的问题,同时也解决了许多其他问题。可以从宽度优先搜索树中的任意节点开始,跟随predecessor 回溯到根节点,以此来找到任意单词到fool的最短词梯。代码清单7-5中的函数展⽰了如何通过回溯predecessor 链来打印整个词梯。


def traverse(y):
    x = y

    while x.get_predecessor():
        print(x.get_id())
        x = x.get_predecessor()

    print(x.get_id())

词梯问题完整代码


from Graphadj import Graph, Vertex
from queue import Queue

def create_word_graph(word_file):
    graph = Graph()
    d = {}

    fp_words = open('word.txt', 'r')

    for line in fp_words:
        word = line[ : -1]

        for i in range(len(word)):
            bucket = word[ : i] + '_' + word[i+1: ]

            if bucket in d:
                d[bucket].append(word)
            else:
                d[bucket] = [word]

        for bucket in d.keys():
            for word1 in d[bucket]:
                for word2 in d[bucket]:
                    if word1 != word2:
                        graph.add_edge(word1, word2)

    return graph


def bfs(g, start):
    start.set_distance(0)
    start.set_predecessor(None)
    vertex_queue = Queue()
    vertex_queue.enqueue(start)

    while vertex_queue.size() > 0:
        current_vertex = vertex_queue.dequeue()

        for nbr in current_vertex.get_connections():
            if nbr.get_color() == "white":
                nbr.set_color('grey')
                nbr.set_distance(current_vertex.get_distance() + 1)
                nbr.set_predecessor(current_vertex)
                vertex_queue.enqueue(nbr)

        current_vertex.set_color('black')


def traverse(y):
    x = y

    while x.get_predecessor():
        print(x.get_id())
        x = x.get_predecessor()

    print(x.get_id())


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!