【算法设计与分析】图搜索算法的应用

'''
一、简化代码,直接输出路径
'''
class DFSResult():
    def __init__(self):
        self.parent = {}
        self.visited = []

def dfs_iterative(graph):
    results = DFSResult()

    for v in graph.keys():
        if v not in results.parent:
            results.parent[v] = None
            print("\n%s:" % v, end=" ")
        if v not in results.visited:
            stack = [v]
            while stack:
                u = stack.pop()
                if u not in results.visited:
                    results.visited.append(u)
                    print(u, end=", ")
                for n in graph[u]:
                    if n not in results.visited:
                        results.parent[n]=results.visited[-1]
                        stack.extend(n)
    return results

if __name__ == '__main__':
    graph = {
        "a": ["b", "d"],
        "b": ["e"],
        "d": ["b"],
        "e": ["d"],
        "c": ["e", "f"],
        "f": ["f"]
    }
    dfs_iterative(graph)
    # 输出
    # a: a, d, b, e, 
    # c: c, f, 

重点问题:

课后习题7-5:给定两个单词(start,end)和一个字典,要求找出从单词start变化到end的最小序列。变化过程中出现的中间单词必须是字典中有的单词,且每次只能是变化其中的一个字母。例如:

    start_state = 'hit'
    end_state = 'cog'
    dic = ['hot', 'dot', 'dog', 'lot', 'log']

输出答案为

hit -> hot -> dot -> dog -> cog

本体使用状态机来解决,参考教材“虎胆龙威”例题,本题源码基于“虎胆龙威”例题修改而来。

解题思路:

设问题的状态为一个三元组,如单词'hot'可拆为('h', 'o', 't')表示一个状态,要求hot的下一状态,只需一次变换其每个位置上的字母,将变换后的单词在给定的字典中搜索是否存在,若存在则将该状态保留下来,否则不保留该状态。根据当前状态计算之后的状态,把所有可能的状态变换结果按照变换的先后顺序构建出一个图,利用广度优先搜索算法得出的结果类,找出从开始状态到结束状态的最短路径。得到的路径即为本题答案。

 先从解决长度为3的单词开始。首先定义结果类

class BFSTransformResult:
    '''状态值组成的图结果类'''
    def __init__(self):
        self.level = {}
        self.parent = {}

接下来写广度优先搜索算法

def bfs_transform(start, end, dic):
    '''
    构建状态图,宽度优先搜索算法的变体
    '''
    dic.append(end)
    r = BFSTransformResult()
    r.parent = {start:None}
    r.level = {start:0}

    nexts = [start]
    while nexts:
        v1 = nexts.pop(0)
        neighbor = find_next_state(v1, dic)
        for v2 in neighbor:
            if v2 not in r.level:
                r.level[v2] = r.level[v1] + 1
                r.parent[v2] = v1
                nexts.append(v2)
    return r

其中find_next_state()函数用于寻找和v1相连的点集,其核心在于寻找接下来的状态值(可能有多个)。函数的定义如下:

def find_next_state(state, dic):
    '''
    寻找下一个状态(只应用于长度为3的单词)
    '''
    next_state = []
    for each in dic:
        if each[1:] == state[1:] and each[0] != state[0]:
            next_state.append(each)
        if each[::2] == state[::2] and each[1] != state[1]:
            next_state.append(each)
        if each[0:2] == state[0:2] and each[2] != state[2]:
            next_state.append(each)
    return next_state

至此,两个函数已经能够构建出一个包含了所有合法状态的图,只缺少一个寻找最短路径的算法,下面给出它的实现:

def find_shortest_path(transform, start_state, end_state):
    '''
    @param transform: BFSTransformResult对象
    @param start_sate: 开始状态(单词)
    @param end_state: 终止状态(单词)
    寻找从开始状态到终止状态的最短路径
    '''
    v_parent_list = [end_state]
    if end_state != start_state:
        v_parent = transform.parent[end_state]
        v_parent_list.append(v_parent)
        while v_parent != start_state and v_parent != None:
            v_parent = transform.parent[v_parent]
            v_parent_list.append(v_parent)
    return v_parent_list

该算法利用了之前定义的BFSTransformResult类,通过level和parent对每个状态的标记,最终找出一条最短路径。接下来我们测试一遍代码,看看能不能出正确结果。

if __name__ == "__main__": 
    start_state = 'hit'
    end_state = 'cog'
    dic = ['hot', 'dot', 'dog', 'lot', 'log']
    transform = bfs_transform(start_state, end_state, dic)
    print(' -> '.join(find_shortest_path(transform,start_state, end_state)))
    # 输出 cog -> dog -> dot -> hot -> hit

怎么输出结果是反的?因为在找最短路径的时候是从后往前找的,不要紧,我们把结果翻转就好了,代码如下:

if __name__ == "__main__": 
    start_state = 'hit'
    end_state = 'cog'
    dic = ['hot', 'dot', 'dog', 'lot', 'log']
    transform = bfs_transform(start_state, end_state, dic)
    # 将结果翻转
    print(' -> '.join(find_shortest_path(transform,start_state, end_state)[::-1]))
    # 输出 hit -> hot -> dot -> dog -> cog

这是单词长度为3的情况,倘若单词字典中的单词长度不一定且开始状态和结束状态的单词长度与字典中单词的长度不一样,这个算法就出问题了。接下来考虑上述情况的解决方案。

现在find_next_state()函数只能解决单词长度为3的问题,为了适应更复杂的情况,应该修改该函数的实现,具体看代码

def find_next_state_wide(state, dic):
    '''
    寻找一下个状态(普适)
    '''
    next_state = []
    size = len(state)
    for each in dic:
        each_size = len(each)
        for i in range(size):
            each_lst = list(each)
            state_lst = list(state)
            each_lst[min(i, each_size - 1)], state_lst[i] = '', ''
            if ''.join(each_lst) == ''.join(state_lst):
                next_state.append(each)
    return next_state

注意其中的变量i,它是当前状态下可以变换的字母的下标,比如当前状态为('h', 'o', 't'),则i的取值范围为0~2,同时将当前状态和字典中的任一单词下标为i的字母删除,得到的新两个字符串(一个是当前状态单词变更而来,一个由字典中的单词变换而来)如果相等,则认为这是一种可行状态,将其加入next_state数组。在i所有取值都取完之后即可得到所有的可行状态。

写完普适版本的find_next_state()函数之后,我们需要把单词长度变为4来测试算法正确与否:

if __name__ == "__main__": 
    start_state = 'good'
    end_state = 'like'
    dic = ['gooe', 'likk', 'doog', 'looe', 'log', 'lioe', 'like']
    transform = bfs_transform(start_state, end_state, dic)
    print(' -> '.join(find_shortest_path(transform,start_state, end_state)[::-1]))
    # 输出 good -> gooe -> looe -> lioe -> like

结果与预期完全相符,本题的状态机解法告一段落。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值