-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add 127. Word Ladder.md #20
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ここで、num_of_diffが2以上だったらループを抜けると無駄な処理を減らせそうです |
||
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となった。 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. この問題、結構制限が適当に決められているという印象があります。 |
||
|
||
```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をするなら、最短経路以外の同じ地点に至るルートはできるだけ | ||
早く探索対象から外した方が実行時間が短くなるようだった。 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LeetCode の実行環境は実行時間の計測の精度が良くないようです。 LeetCode に表示される実行時間はあまり信用しないほうがよいと思います。 |
||
|
||
```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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 後のステップで直っていますが、ここのネストが深すぎてちょっと読みづらさを感じました。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for pattern in generate_pattern(word):
for next_word in pattern_to_words[pattern]: この二行圧縮すべきでしたね、ありがとうございます |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 373-375 行目は、 if generated_word in word_set:
yield generated_word のほうが簡潔ではありませんか? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 個人的には、そこまで簡潔さが変わっている印象はないです。 ただリンク先の問題と異なり、この部分は、条件も、それが成立した時・しなかった時にする処理も1行で表せるので、いただいた提案でも簡潔だと思います。 |
||
|
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. word_set から取り除くのも手かと思います。 |
||
words_and_lengths.append((next_word, length + 1)) | ||
return 0 | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ladderLength() 全体の時間計算量も求めてみてください。そして、時間計算量を求めたあと、制約にあるデータサイズの上限を代入し、どれくらいの数字になるかを確かめてみてください。その数字が言語ごとの処理速度の違いを考慮したうえで、一定の数値以下に入っていれば、現実的な時間で処理が終わると思います。
今回の場合ですと O(m*n^2) とります。制約に 1 <= m <= 10、1 <= n <= 5000 とありますので、 m = 10、n = 5000 を代入します。すると 2.5 * 10^8 となります。 Python は比較的遅い言語で、 1 秒間に 100 万ステップくらいしか計算できません。2.5 * 10^8 / 10^6 = 250 となり、 250 秒くらいかかりそうということが分かります。これは制限時間を大きく超えそうです。
言語ごと速度の差は以下をご覧ください。
https://github.com/niklas-heer/speed-comparison
https://benchmarksgame-team.pages.debian.net/benchmarksgame/box-plot-summary-charts.html
上記の実行時間の概算方法は、 IPC やメモリのアクセスパターンや整数・浮動小数の計算速度の違い等、様々な要因で大きくぶれます。目安程度に考えることをお勧めいたします。