From 7f89af4c9640b279c1815e26f37f655622003fbc Mon Sep 17 00:00:00 2001 From: Taito Ohsumi Date: Fri, 31 Jan 2025 10:05:44 +1100 Subject: [PATCH] Add 127. Word Ladder.md --- 127. Word Ladder.md | 390 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 127. Word Ladder.md diff --git a/127. Word Ladder.md b/127. Word Ladder.md new file mode 100644 index 0000000..4e9a2e4 --- /dev/null +++ b/127. Word Ladder.md @@ -0,0 +1,390 @@ +# step 1 +wordに対して、1文字違いをwordListから毎回探すやり方でTLE + +find_words_diff_by_one()に時間がかかる。m = len(word), n = len(wordList)として、 +find_words_diff_by_one()のtime complexityがO(mn). + +`if next_word in path:`(下から五行目)も遅くなりそう。 + +```python +from collections import deque + + +class Solution: + def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: + def is_diff_by_one(word1: str, word2: str) -> bool: + if len(word1) != len(word2): + return False + len_word = len(word1) + num_of_diffs = 0 + for i in range(len_word): + if word1[i] == word2[i]: + continue + num_of_diffs += 1 + return num_of_diffs == 1 + + def find_words_diff_by_one( + wordList: List[str], + keyword: str + ) -> List[str]: + diff_words = [] + for word in wordList: + if is_diff_by_one(keyword, word): + diff_words.append(word) + return diff_words + + next_transformations = deque() + next_transformations.append([beginWord]) + while next_transformations: + path = next_transformations.popleft() + last_word = path[-1] + if last_word == endWord: + return len(path) + words_diff_by_one = find_words_diff_by_one(wordList, last_word) + for next_word in words_diff_by_one: + if next_word in path: + continue + next_path = path.copy() + next_path.append(next_word) + next_transformations.append(next_path) + return 0 +``` + +先に1文字違いを調べておく方針。 +1文字違いを調べるのに、time complexityがO(n^2 * m)かかり、 +これはMemory limit exceededとなった。 + +```python +from collections import defaultdict +from collections import deque + + +class Solution: + def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: + def is_diff_by_one(word1: str, word2: str) -> bool: + if len(word1) != len(word2): + return False + len_word = len(word1) + num_of_diffs = 0 + for i in range(len_word): + if word1[i] == word2[i]: + continue + num_of_diffs += 1 + return num_of_diffs == 1 + + word_to_words_diff_by_one = defaultdict(list) + for word1 in wordList: + for word2 in wordList: + if is_diff_by_one(word1, word2): + word_to_words_diff_by_one[word1].append(word2) + if beginWord not in wordList: + for word in wordList: + if is_diff_by_one(beginWord, word): + word_to_words_diff_by_one[beginWord].append(word) + + next_transformations = deque() + next_transformations.append([beginWord]) + while next_transformations: + path = next_transformations.popleft() + last_word = path[-1] + if last_word == endWord: + return len(path) + words_diff_by_one = word_to_words_diff_by_one[last_word] + for next_word in words_diff_by_one: + if next_word in path: + continue + next_path = path.copy() + next_path.append(next_word) + next_transformations.append(next_path) + return 0 +``` + +ここで解答を見る。 + +m <= 10より、ある文字から遷移可能な文字数は高々10 * 26 = 260通りなので、それを全列挙する。 +一度訪問済みのwordに関しては、2度目の訪問時はpathの長さが最小ではないので、飛ばす。 + +こういう時、素直にこういう発想ができないことが多い。一つの方針が立った時、他のものを考えずに +突き進む癖がある。 + +```python +from collections import deque +import string + + +class Solution: + def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: + len_word = len(beginWord) + words_set = set(wordList) + visited = set() + next_candidates = deque() + next_candidates.append([beginWord, 1]) + while next_candidates: + last_word, path_length = next_candidates.popleft() + if last_word == endWord: + return path_length + visited.add(last_word) + for i in range(len_word): + for char in string.ascii_lowercase: + char_list = list(last_word) + char_list[i] = char + next_word = ''.join(char_list) + if next_word not in words_set: + continue + if next_word in visited: + continue + next_candidates.append([next_word, path_length + 1]) + return 0 +``` + +上だとテストケースは通るが、かなり遅かった。考えられる原因としては、 +- endWordかの確認が遅い + - next_wordの時点で確認可能 +- これと合わせて、next_wordを作る時にわざわざリストを使っている +- visitedに追加される前に同じwordをnext_candidatesに加えている + +修正版は以下の通り + +一番最後の、「visitedに追加される前に同じwordをnext_candidatesに加えている」によって +かなり遅くなっているみたいだった。BFSをするなら、最短経路以外の同じ地点に至るルートはできるだけ +早く探索対象から外した方が実行時間が短くなるようだった。 + +```python +from collections import deque +import string + + +class Solution: + def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: + len_word = len(beginWord) + words_set = set(wordList) + next_candidates = deque() + next_candidates.append([beginWord, 1]) + while next_candidates: + last_word, path_length = next_candidates.popleft() + for i in range(len_word): + for char in string.ascii_lowercase: + next_word = f"{last_word[:i]}{char}{last_word[i+1:]}" + if next_word not in words_set: + continue + if next_word == endWord: + return path_length + 1 + next_candidates.append([next_word, path_length + 1]) + words_set.remove(next_word) + return 0 +``` + +BFSのtime complexityはO(V + E)であるが、これはどう考えればいいかわからなかった。 +`words_set.remove(next_word)`は、高々n = len(wordList)しか実行されないので、 +`next_candidates.append([next_word, path_length + 1])`もn程度で済む。 + +上の方針で、words_setから探索済みのwordを省いていくと、 +wordに対して、1文字違いをwordListから毎回探すやり方でもleetcodeのテストはTLEとならなかった。 + +# step 2 +- https://github.com/olsen-blue/Arai60/pull/20/files + - ある単語からその単語から1文字とった分割へのhash table, 分割からそれを構成しうる単語へのhash tableを用意 + - 二つを組み合わせて、ある単語から次に遷移可能な単語を全て列挙していた + - 初めは分割の間に`*`を入れていたが、後半はtupleに変わっており、後者は`*`が文字として + wordListに入ってきても動くようになっている + - 自分のやっている総当たりみたいなやつよりも必要最低限を調べている感じだった +- https://github.com/katataku/leetcode/pull/18/files#r1910141758 + - > distance が 2 になったところで return False すると、処理量が少し減ると思います。 + - 自分もハミング距離を求める操作をしていたが、同じように最後まで数えていた + - 最悪計算量は変わらないが、今回は特に、最悪のシチュエーションが多いとも考えられない + - dequeの命名がwords_and_lengthsだった。 + - next_candidatesよりも叙述的だった。 +- https://github.com/TORUS0818/leetcode/pull/22/files + - is_convertible() 問題文はっきり読んでいなくてもわかりやすい + - is_diff_by_oneとしたが、問題文読んでいれば通じるとは思う。 + - [zip()](https://docs.python.org/3.3/library/functions.html#zip) + - [itertools.zip_longest()](https://docs.python.org/3/library/itertools.html#itertools.zip_longest) + - 長さが同じ保証があるので、今回はスッキリ書ける。 + - dijkstraでも解いていた + - 面接で解くとすると、今回はノード間距離が変わらないからオーバーな気もする。 +- https://github.com/TORUS0818/leetcode/pull/22/files#r1667212013 + - 二方向BFS + +m = len(word), n = len(wordList)とする。 +基本方針はBFSで、一度生成できたwordについては2回は探索しないようにする。 +あるwordから次のwordへと至るための方針として +- wordに対してその都度、計算 + - wordを1文字ずつずらしていって、wordListに入っているか確認 + - time complexity: O(n) (wordListがsetになっているとして) + - wordを1文字ずらしたものがm <= 10より、高々260通りある。 + - wordListから順に単語をとりだして、もとのwordと比較 + - time complexity: O(m * n) +- 事前にword -> adjacent wordsの対応を作っておく + - word -> 可能な分割、分割 -> 同値類 + - time complexity: O(m * n) + +wordをずらす +```python +from collections import deque +import string + + +class Solution: + def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: + def generate_adjacent_word( + word: str, + word_set: Set[str] + ) -> Iterator[str]: + for index in range(len(word)): + for char in string.ascii_lowercase: + next_word = f"{word[:index]}{char}{word[index+1:]}" + if next_word not in word_set: + continue + yield next_word + + words_and_lengths = deque([(beginWord, 1)]) + generated_words = set([beginWord]) + word_set = set(wordList) + while words_and_lengths: + last_word, length = words_and_lengths.popleft() + for next_word in generate_adjacent_word(last_word, word_set): + if next_word in generated_words: + continue + if next_word == endWord: + return length + 1 + words_and_lengths.append((next_word, length + 1)) + generated_words.add(next_word) + return 0 +``` + +wordListの中から探す(TLE) +```python +class Solution: + def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: + def is_adjacent(word1: str, word2: str) -> bool: + if len(word1) != len(word2): + return False + num_of_diffs = 0 + for char1, char2 in zip(word1, word2): + if char1 == char2: + continue + num_of_diffs += 1 + if num_of_diffs > 1: + return False + return num_of_diffs == 1 + + def generate_adjacent_word( + word: str, + word_set: Set[str] + ) -> Iterator[str]: + for possible_word in word_set: + if is_adjacent(word, possible_word): + yield possible_word + + words_and_lengths = deque([(beginWord, 1)]) + word_set = set(wordList) + generated_words = set(beginWord) + while words_and_lengths: + word, length = words_and_lengths.popleft() + for next_word in generate_adjacent_word(word, word_set): + if next_word in generated_words: + continue + if next_word == endWord: + return length + 1 + generated_words.add(next_word) + words_and_lengths.append((next_word, length + 1)) + return 0 +``` + +同値類をまとめる +```python +from collections import defaultdict +from collections import deque + + +class Solution: + def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: + def generate_pattern(word: str) -> Iterator[Tuple[str, str]]: + pattern = [] + for index in range(len(word)): + yield (word[:index], word[index + 1:]) + + pattern_to_words = defaultdict(list) + for word in wordList: + for pattern in generate_pattern(word): + pattern_to_words[pattern].append(word) + + generated_words = set([beginWord]) + words_and_lengths = deque([(beginWord, 1)]) + while words_and_lengths: + word, length = words_and_lengths.popleft() + for pattern in generate_pattern(word): + for next_word in pattern_to_words[pattern]: + if next_word == endWord: + return length + 1 + if next_word in generated_words: + continue + generated_words.add(next_word) + words_and_lengths.append((next_word, length + 1)) + return 0 +``` + +# step 3 +一度,`popleft()`を`pop()`と書いてDFSをしていた。 +```python +from collections import deque +from collections import defaultdict + + +class Solution: + def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: + def generate_pattern(word: str) -> Iterator[Tuple[str, str]]: + for index in range(len(word)): + yield word[:index], word[index + 1:] + + pattern_to_words = defaultdict(list) + for word in wordList: + for pattern in generate_pattern(word): + pattern_to_words[pattern].append(word) + + words_and_lengths = deque([(beginWord, 1)]) + generated_words = set([beginWord]) + while words_and_lengths: + word, length = words_and_lengths.popleft() + for pattern in generate_pattern(word): + for next_word in pattern_to_words[pattern]: + if next_word == endWord: + return length + 1 + if next_word in generated_words: + continue + generated_words.add(next_word) + words_and_lengths.append((next_word, length + 1)) + return 0 +``` + +```python +from collections import deque + + +class Solution: + def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: + def generate_adjacent_word( + word: str, + word_set: Set[str] + ) -> Iterable[str]: + for index in range(len(word)): + for char in string.ascii_lowercase: + generated_word = f"{word[:index]}{char}{word[index + 1:]}" + if generated_word not in word_set: + continue + yield generated_word + + words_and_lengths = deque([(beginWord, 1)]) + generated_words = set([beginWord]) + word_set = set(wordList) + while words_and_lengths: + word, length = words_and_lengths.popleft() + for next_word in generate_adjacent_word(word, word_set): + if next_word == endWord: + return length + 1 + if next_word in generated_words: + continue + generated_words.add(next_word) + words_and_lengths.append((next_word, length + 1)) + return 0 +``` \ No newline at end of file