Временная сложность этого алгоритма: Word Ladder - PullRequest
0 голосов
/ 31 октября 2018

Вопрос:

С учетом двух слов (beginWord и endWord) и списка слов в словаре, найти все кратчайшие последовательности преобразования из beginWord в endWord, такой что:

Одновременно можно изменить только одну букву. Каждое преобразованное слово должно существуют в списке слов. Обратите внимание, что beginWord не является преобразованным словом.

Пример 1:

Ввод: beginWord = "hit", endWord = "винтик", wordList = [ "Горячая", "точка", "собака", "много", "журнал", "винтик"]

Вывод: [["hit", "hot", "dot", "dog", "cog"], ["hit", "hot", "lot", "log", "cog"]]

Мое решение основано на этой идее, но как мне проанализировать временную и пространственную сложность этого решения?

1) Выполните BFS, начиная с beginWord, преобразовав каждую букву в одну из 26 букв, и посмотрите, есть ли преобразованное слово в списке слов, если это так, поместите в очередь.

2) Во время BFS ведите график {word: nextWord} для всех действительных следующих слова

3) Когда nextWord достигает endWord, выполните возврат DFS (предварительный заказ Обход) на графе, чтобы получить все пути.

class Solution:
    def findLadders(self, beginWord, endWord, wordList):
        """
        :type beginWord: str
        :type endWord: str
        :type wordList: List[str]
        :rtype: List[List[str]]
        """
        wordListSet = set(wordList+[beginWord])
        graph = collections.defaultdict(list)
        q = set([beginWord])    
        count = 0
        result = []
        while q:
            count +=1
            newQ = set()
            for word in q:
                wordListSet.remove(word)
            for word in q:
                if word == endWord:                                        
                    self.getAllPaths(graph, beginWord, endWord, result, [])
                    return result
                for i in range(len(word)):
                    for sub in 'abcdefghijklmnopqrstuvwxyz':
                        if sub != word[i]:
                            newWord = word[:i] + sub + word[i+1:]
                            if newWord in wordListSet:
                                graph[word].append(newWord)
                                newQ.add(newWord)
            q = newQ
        return []

    def getAllPaths(self, graph, node, target, result, output):
        #This is just a backtracking pre-order traversal DFS on a DAG.
        output.append(node)
        if node==target:
            result.append(output[:])
        else:
            for child in graph[node]:
                self.getAllPaths(graph,child, target, result, output)
                output.pop()

Мне трудно придумать время и пространственную сложность этого. Мое утверждение:

Время: O(26*L*N + N), где L - это средняя длина каждого слова, а N - это количество слов в списке слов . В худшем случае каждое преобразованное слово находится в списке, поэтому для каждого преобразования требуется 26 * length of word. Часть DFS просто O(N). Так что асимптотически это просто O(L*N)

Пробел: O (N)

Ответы [ 2 ]

0 голосов
/ 10 ноября 2018

Это звучит как забавная проблема. Да, ответ O(L * N). Если вы исправили свой код для возврата всех решений, процедура рекурсивной печати будет O(L!).

  1. У вас есть этот внешний цикл, for all nodes being considered. Это может быть равно длине вашего списка слов. Рассмотрим полностью связный набор из трех буквенных комбинаций ['aaa', 'aab', ... 'zzz']. Число узлов равно 26 ^ 3 или 27576. Преобразование из aaa в zzz имеет шесть ответов: aaa->zaa->zza->zzz, aaa->zaa->aza->zzz, aaa->aza->zza->zzz и т. Д. Вы бы рассмотрели все три пути длины (25+ 25 + 25) (25 + 25) (25) или 93 750 путей, чтобы убедиться, что не было более короткого пути.

  2. У вас есть два варианта для внутреннего цикла: for i in range(len(word)) и ваш рекурсивный вызов get_all_paths() для получения списка всех путей. Вы знаете, что у вас есть порядок length_of_word для внутреннего, что подразумевает O(L * N). Обратите внимание, что O(L * N * 26) означает то же самое; обозначение Big O заботится только о масштабе изменений. Я не доказал, что вы остаетесь линейными в этом цикле get_all_paths.

  3. Это особый случай Кратчайшего пути Дейкстры . Вы можете лучше добавить эвристику к вашей конкретной проблеме. Общая длина пути через узел всегда больше или равна расстоянию до сих пор плюс количество букв, которые все еще неверны. Это означает, что в полностью подключенном случае у вас есть aaa (0 length)->aab (1)->abb (2)->bbb (3), поэтому вы избегаете исследования aaa (0 actual + 3 heuristic) -> aab (1 actual + 3 heuristic).

  4. Вы можете исправить свой код так, чтобы он возвращал все цепочки слов, и я сделал это здесь . Проблема в том, что рекурсивная подпрограмма getAllPaths() теперь растет быстрее, чем O(L * N). В примере кода вход имеет два набора «выбора пути» или подграфов, набор которых умножает количество путей. Таким образом, утроение количества узлов увеличит число вариантов пути в три раза, а количество вариантов выбора будет выбрано в кубе.

0 голосов
/ 08 ноября 2018

Вы не найдете все простые пути, потому что могут быть альтернативные кратчайшие пути к конечному слову. Простейший контрпример выглядит следующим образом:

beginWord = aa,
endWord = bb
wordList = [aa, ab, ba, bb]

Ваш алгоритм пропустит путь aa -> ba -> bb. На самом деле он всегда найдет не более одного пути.

Сложность времени действительно равна O(L * N), как вы написали, но сложность пространства равна O(L*N), то есть пространству, которое занимает ваш граф или wordList.

...