diff --git a/127. Word Ladder.md b/127. Word Ladder.md new file mode 100644 index 0000000..0bfc863 --- /dev/null +++ b/127. Word Ladder.md @@ -0,0 +1,243 @@ +URL: https://leetcode.com/problems/word-ladder/description/ + +# Step 1 + +- 実装時間: 20分 + +TLEした回答 + +```python +class Solution: + def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: + def is_adjacent_pair(str1, str2): + distance = 0 + for i in range(len(str1)): + if str1[i] != str2[i]: + distance += 1 + return distance == 1 + + used_words = set() + word_and_lengths = deque([(beginWord, 1)]) + while word_and_lengths: + word, length = word_and_lengths.popleft() + if word == endWord: + return length + if word in used_words: + continue + used_words.add(word) + for next_word in wordList: + if is_adjacent_pair(word, next_word): + word_and_lengths.append((next_word, length + 1)) + return 0 +``` + +- wordListの要素数を`n`, wordの長さ`l`をとして、 + - 時間計算量: O(nl**2) + - is_adjacent_pairでO(l) + - search_adjacentsでO(n) + - メインのループでO(l) + - 空間計算量: O(n) + + +```python +class Solution: + def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: + def is_adjacent_pair(str1, str2): + distance = 0 + for i in range(len(str1)): + if str1[i] != str2[i]: + distance += 1 + return distance == 1 + + def search_adjacents(word): + adjacents = [] + for next_word in wordList: + if next_word in used_words: + continue + if is_adjacent_pair(word, next_word): + adjacents.append(next_word) + return adjacents + + used_words = set([beginWord]) + word_and_lengths = deque([(beginWord, 1)]) + while word_and_lengths: + word, length = word_and_lengths.popleft() + if word == endWord: + return length + for next_word in search_adjacents(word): + if next_word in used_words: + continue + word_and_lengths.append((next_word, length + 1)) + used_words.add(next_word) + return 0 +``` + +- いけるんじゃないかと思って、appendのタイミングとかを調整して対応した。 + - PythonだとTLEということを事前に見積もれていなかった。 + - https://github.com/fhiyo/leetcode/pull/22/files#r1641702410 + +# Step 2 + +- 参考にしたURL + - https://github.com/fhiyo/leetcode/pull/22 + - https://github.com/kazukiii/leetcode/pull/21 + - https://github.com/Yoshiki-Iwasa/Arai60/pull/22 + - https://github.com/TORUS0818/leetcode/pull/22 + - https://github.com/Ryotaro25/leetcode_first60/pull/22 + - https://github.com/seal-azarashi/leetcode/pull/19 + - https://github.com/goto-untrapped/Arai60/pull/57 + - https://github.com/nittoco/leetcode/pull/36 + - https://github.com/hroc135/leetcode/pull/19 + - https://github.com/Yusan1234/arai60/pull/1 + - https://github.com/tarinaihitori/leetcode/pull/20 + - https://github.com/colorbox/leetcode/pull/34 + - https://github.com/hayashi-ay/leetcode/pull/42/files + +- used_wordsを管理するのではなくて、`unused_words = set(wordList)`を管理する。 + - Step1では実装ではListに対してin演算をしていたので、遅い。setのinはO(1)。 + +```python +class Solution: + def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: + def is_adjacent_pair(str1, str2): + distance = 0 + for i in range(len(str1)): + if str1[i] != str2[i]: + distance += 1 + return distance == 1 + + def search_adjacents(word): + adjacents = [] + for next_word in wordList: + if next_word not in unused_words: + continue + if is_adjacent_pair(word, next_word): + adjacents.append(next_word) + return adjacents + + unused_words = set(wordList) + word_and_lengths = deque([(beginWord, 1)]) + while word_and_lengths: + word, length = word_and_lengths.popleft() + if word == endWord: + return length + for next_word in search_adjacents(word): + if next_word not in unused_words: + continue + word_and_lengths.append((next_word, length + 1)) + unused_words.remove(next_word) + return 0 +``` + +- search_adjacentsがリストを作成するのは遅い? + - リストを作って返すのではなく、yieldで都度返す。 + - やって見たけどあんまりかわらなかった。 + +```python +class Solution: + def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: + def is_adjacent_pair(str1, str2): + distance = 0 + for i in range(len(str1)): + if str1[i] != str2[i]: + distance += 1 + return distance == 1 + + def search_adjacents(word): + for next_word in wordList: + if next_word not in unused_words: + continue + if is_adjacent_pair(word, next_word): + yield next_word + + unused_words = set(wordList) + word_and_lengths = deque([(beginWord, 1)]) + while word_and_lengths: + word, length = word_and_lengths.popleft() + if word == endWord: + return length + for next_word in search_adjacents(word): + if next_word not in unused_words: + continue + word_and_lengths.append((next_word, length + 1)) + unused_words.remove(next_word) + return 0 +``` + +- > 「wordList全体に対して隣接比較する」のではなく「1文字ずつ変えて、wordListに含まれるかチェック」の方が早い + - 前者は`O(len(wordList)*len(word))` + - 後者は`O(26*len(word))` + - https://github.com/TORUS0818/leetcode/pull/22/files#r1666932153 + +```python +class Solution: + def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: + def generate_adjacents(word): + for i in range(len(word)): + for letter in ascii_lowercase: + next_word = word[:i] + letter + word[i+1:] + if next_word not in unused_words: + continue + yield next_word + + unused_words = set(wordList) + word_and_lengths = deque([(beginWord, 1)]) + while word_and_lengths: + word, length = word_and_lengths.popleft() + if word == endWord: + return length + for next_word in generate_adjacents(word): + word_and_lengths.append((next_word, length + 1)) + unused_words.remove(next_word) + return 0 +``` + +- `ascii_lowercase`を見てもピンとこなかったので、ドキュメントを読み返した。 + - https://docs.python.org/3/library/string.html#string.ascii_lowercase + +- 文字列の結合を2回しているのが若干気になる + - f-stringでやった方がいい? + - 2回くらいなら許容な気がした。3回以上足し算があるなら、f-stringの方が読みやすそう + +- > で、そのおまけの話として、 +hamming_distance(a, b) と hamming_distance(b, a) が同じ結果なのに別のキャッシュになるのを避けたい気持ちがあるので、@cache つけるのを補助関数にするなどを考えていました。 + - 今回の実装と関係ないけど、気にしたことがなかった。 + +# Step 3 + +- 実装時間: 4分 +- wordListの要素数を`n`, wordの長さ`l`をとして、 + - 時間計算量: O(ln) + - generate_adjacentsでO(26 * l) + - メインのループでO(l) + - 空間計算量: O(n) + +```python +class Solution: + def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: + unused_words = set(wordList) + def generate_adjacents(word): + for i in range(len(word)): + for letter in ascii_lowercase: + next_word = word[:i] + letter + word[i+1:] + if next_word in unused_words: + yield next_word + + word_and_lengths = deque([(beginWord, 1)]) + while word_and_lengths: + word, length = word_and_lengths.popleft() + if word == endWord: + return length + for next_word in generate_adjacents(word): + word_and_lengths.append((next_word, length + 1)) + unused_words.remove(next_word) + return 0 +``` + +- 書いた感想 + - 変数定義の場所について + - `unused_words`は補助関数`generate_adjacents()`でも使うので、最初に定義したい。 + - 関数外の変数を見に行くのちょっと嫌だけど、`ladderLength()`の関数内だし許容する。 + - 一方で、`word_and_lengths`はループの中で使うものなので、whileループの近くで定義したい。 + - 昨日と同様、覚えるというよりも考えて出力してる感じだった。今日も半日ぐらい間を空けたけど、気持ちよく書けた。 + - `ascii_lowercase`だけ正式な名前が思い出せなくて、ドキュメントを見た。