You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求优化单词阶梯(Laddergram)最短路径查找Python代码

Laddergram单词最短替换路径代码优化

需求与问题

实现同长度单词间的最短字符替换路径(对应Laddergram游戏):每步仅替换一个字符,且生成的新单词必须存在于dictionary.txt字典文件中。例如从LANE到BODY的路径为:LANE→LONE→BONE→BONY→BODY。

当前代码处理6个及以上字符的单词时速度较慢,且无法确认是否找到最短路径,现对代码进行审查与优化。

原代码

from collections import deque
import time


def load_words(word_length):
    with open('dictionary.txt', encoding="utf8") as file:
        words = {line.strip() for line in file if len(line.strip()) == word_length}
    return words


def get_neighbors(word, word_set):
    neighbors = []
    for i in range(len(word)):
        for char in 'abcdefghijklmnopqrstuvwxyz':
            if char != word[i]:
                new_word = word[:i] + char + word[i + 1:]
                if new_word in word_set:
                    neighbors.append(new_word)
    return neighbors


def find_shortest_path(start, end):
    if start == end:
        return [start]

    word_set = load_words(len(start))

    if start not in word_set or end not in word_set:
        return None

    queue = deque([(start, [start])])
    visited = {start}

    while queue:
        current_word, path = queue.popleft()

        for neighbor in get_neighbors(current_word, word_set):
            if neighbor == end:
                return path + [neighbor]
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append((neighbor, path + [neighbor]))

    return None


start_word = input("Insert the starting word: ")
end_word = input("Insert the ending word: ")
start_time = time.time()
if not start_word.isalpha():
    print(f"Invalid input: {start_word} contains not alphabetic characters.")
elif not end_word.isalpha():
    print(f"Invalid input: {end_word} contains not alphabetic characters.")
elif len(start_word) != len(end_word):
    print(f"Invalid input: {start_word} and {end_word} must be the same length")
else:
    start_word = start_word.lower()
    end_word = end_word.lower()
    path_found = find_shortest_path(start_word, end_word)
    if path_found:
        print(f"\nMinimum path length is: {len(path_found) - 1}")
        print("\n".join(path_found).upper())
    else:
        print(f"\nDoesnt' exist any path from {start_word} to {end_word}.")
print(f"Time taken: {time.time() - start_time}")

关键说明与优化点

1. 最短路径的确认

原代码采用广度优先搜索(BFS),BFS的特性是优先遍历所有距离起点最近的节点,因此第一次找到终点时的路径必然是最短路径,无需额外验证。

2. 速度优化方案

(1)预生成模式映射替代逐字符枚举

原get_neighbors函数通过逐个替换字符生成新单词再查字典,效率极低。改为预先生成"模式-单词"映射:将每个单词的每个位置替换为通配符(如*),把相同模式的单词归为一组。例如单词lane会生成*ane、l*ne、la*e、lan*四个模式,每个模式对应所有符合该结构的单词。

找邻居时,只需生成当前单词的所有模式,直接取对应模式下的单词即可,无需逐字符枚举。

(2)双向BFS减少搜索空间

单向BFS的搜索空间会随着步数指数增长,双向BFS同时从起点和终点开始搜索,当两边的搜索集合出现交集时,即可拼接出最短路径,能大幅减少搜索节点数量,尤其是长单词场景下效率提升显著。

(3)缓存字典加载结果

原代码每次调用find_shortest_path都会重新加载字典,改为缓存不同长度的单词集合和模式映射,避免重复IO操作。

(4)记录父节点替代路径复制

原代码每次入队都会复制整个路径列表,内存开销大。改为记录每个节点的父节点,找到交集后再回溯生成路径,节省内存和时间。

优化后的代码

from collections import deque, defaultdict
import time

# 缓存不同长度的单词集合和模式映射
word_cache = {}
pattern_map_cache = {}


def load_words_and_patterns(word_length):
    if word_length in word_cache and word_length in pattern_map_cache:
        return word_cache[word_length], pattern_map_cache[word_length]
    
    word_set = set()
    pattern_map = defaultdict(list)
    
    with open('dictionary.txt', encoding="utf8") as file:
        for line in file:
            word = line.strip().lower()
            if len(word) == word_length:
                word_set.add(word)
                # 生成当前单词的所有模式
                for i in range(word_length):
                    pattern = word[:i] + '*' + word[i+1:]
                    pattern_map[pattern].append(word)
    
    word_cache[word_length] = word_set
    pattern_map_cache[word_length] = pattern_map
    return word_set, pattern_map


def get_neighbors(word, pattern_map):
    neighbors = set()
    word_len = len(word)
    for i in range(word_len):
        pattern = word[:i] + '*' + word[i+1:]
        # 取当前模式下的所有单词,排除自身
        for neighbor in pattern_map[pattern]:
            if neighbor != word:
                neighbors.add(neighbor)
    return neighbors


def find_shortest_path(start, end):
    if start == end:
        return [start]
    
    word_len = len(start)
    word_set, pattern_map = load_words_and_patterns(word_len)
    
    if start not in word_set or end not in word_set:
        return None
    
    # 双向BFS的两个队列和父节点记录
    start_queue = deque([start])
    end_queue = deque([end])
    
    start_visited = {start: None}
    end_visited = {end: None}
    
    while start_queue and end_queue:
        # 处理起点方向的一层节点
        current_level_size = len(start_queue)
        for _ in range(current_level_size):
            current_word = start_queue.popleft()
            # 检查是否和终点方向的访问集合有交集
            if current_word in end_visited:
                # 拼接路径:起点到交集节点 + 终点到交集节点的反向
                path = []
                temp = current_word
                while temp:
                    path.append(temp)
                    temp = start_visited[temp]
                path.reverse()
                temp = end_visited[current_word]
                while temp:
                    path.append(temp)
                    temp = end_visited[temp]
                return path
            
            for neighbor in get_neighbors(current_word, pattern_map):
                if neighbor not in start_visited:
                    start_visited[neighbor] = current_word
                    start_queue.append(neighbor)
        
        # 处理终点方向的一层节点
        current_level_size = len(end_queue)
        for _ in range(current_level_size):
            current_word = end_queue.popleft()
            if current_word in start_visited:
                path = []
                temp = start_visited[current_word]
                while temp:
                    path.append(temp)
                    temp = start_visited[temp]
                path.reverse()
                path.append(current_word)
                temp = end_visited[current_word]
                while temp:
                    path.append(temp)
                    temp = end_visited[temp]
                return path
            
            for neighbor in get_neighbors(current_word, pattern_map):
                if neighbor not in end_visited:
                    end_visited[neighbor] = current_word
                    end_queue.append(neighbor)
    
    return None


start_word = input("输入起始单词:")
end_word = input("输入目标单词:")
start_time = time.time()

if not start_word.isalpha():
    print(f"无效输入:{start_word}包含非字母字符。")
elif not end_word.isalpha():
    print(f"无效输入:{end_word}包含非字母字符。")
elif len(start_word) != len(end_word):
    print(f"无效输入:{start_word}和{end_word}长度必须相同")
else:
    start_word = start_word.lower()
    end_word = end_word.lower()
    path_found = find_shortest_path(start_word, end_word)
    if path_found:
        print(f"\n最短路径长度:{len(path_found) - 1}")
        print(" → ".join(path_found).upper())
    else:
        print(f"\n不存在从{start_word}到{end_word}的路径。")

print(f"耗时:{time.time() - start_time:.4f}秒")

优化效果说明

  • 预生成模式映射将邻居查找的时间复杂度从O(26*N)降至O(M)(M为同模式单词数量),长单词场景下效率提升明显。
  • 双向BFS将搜索空间从O(bd)降至O(b(d/2))(b为平均分支数,d为最短路径长度),大幅减少遍历节点数。
  • 缓存机制避免了重复加载字典的IO开销。
  • 父节点记录减少了路径复制的内存和时间开销。

内容的提问来源于stack exchange,提问作者frex713

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 14:55:59