Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
269 changes: 269 additions & 0 deletions 127_word-ladder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
# 127. Word Ladder

## 1st

### ①

DFSでやろうとして時間制限を超えてやり直すなどでかなり時間がかかった。エッジの重みがすべて1のグラフ上の最短距離を求めるのだからBFSがまず候補に入るはず。
DFSを書くのにも動く状態になるまで40分以上かけている。問題を見たときに自然な発想ができないのと思いついた方法を検討したときに上手くいかない or 計算量などで問題があることを速く思いつければいいのだが...

他の方のコードを見て思ったが、幅優先探索している意識が強いのでqueueを使っているが、このコードだとword_queue, next_word_queueはキューじゃなくてもリストなどでも良い。

snake caseとcamel caseが混じってるのは少し気になるが、問題と自分の書き方の都合。何なら与えられたコードの引数を変えればいい。

所要時間: 1:01:27

n: len(wordList), m: wordListに含まれるwordとbeginWordの長さの平均, l: 出現しうる文字の数 (今回はa-zで定数)
- 時間計算量: O(nml)
- 空間計算量: O(nm)

```py
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
words = set(wordList)
num_words = 1
word_queue = deque([beginWord])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

変数名の queue は型と情報が重複しているため、不要だと思います。 3rd のように words next_words のほうが良いと思います。ただ、この命名も人によっては違和感を感じるかもしれません。自分は幅優先探索を書くときは、 frontier explorered という単語を使いますが、少数派かもしれません。

また、幅優先探索を書くとき、この解答のようにキューを 2 つ用意する実装方法も良いと思います。一方、キューは 1 つだけ用意し、先頭から取り出す、末尾に入れるという形で実装することもあります。その場合、キューまたは別の領域に、何回辿ったかを表す整数を保存しておく必要があります。個人的には最近はこちらの書き方で書くことが多いです。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

自分は幅優先探索を書くときは、 frontier explorered という単語を使いますが、少数派かもしれません。

ダイクストラのfrontierのイメージですかね。_queueとsuffixを付けたのは他の変数でwordsという名前を使ってたので付けました。今回みたいな仕方ないので_queue付ける、みたいなときの別の選択肢として良いかもと思いました。

while word_queue:
next_word_queue = deque()
while word_queue:
word = word_queue.popleft()
if word == endWord:
return num_words
for next_word in self._getNextWords(word, words):
next_word_queue.append(next_word)
Comment on lines +32 to +33
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

next_word_queue.extend(self._getNextWords(word, words)) でよいと思います。

word_queue = next_word_queue
num_words += 1
return 0

def _getNextWords(self, word: str, words: set[str]) -> Generator[str, None, None]:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

自分で書く関数名は、 3rd のように lower snake で統一してよいと思います。

for i, original_ch in enumerate(word):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

自分が書く場合は、 words でループを回し、 word と 1 文字違いかどうかを調べると思います。。ただし、 1 <= wordList.length <= 5000 のため Python だと TLE になると思います。自分の場合は C++ を使います。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

問題の制約が 1 <= beginWord.length <= 10 なので、最初は O(len(wordList)^2) でもいいがそこから制約が厳しくなったときに高速化する方法はあるか、という話に繋がりそうな気はしますね。言語を変えて定数倍高速化するという方法は一つではあるでしょうが、例えばここだけC++で他多言語みたいな状態にすると保守性は下がりそうな気はします

for ch in ascii_lowercase:
if ch == original_ch:
continue
new_word = f'{word[:i]}{ch}{word[i + 1:]}'
if new_word in words:
words.remove(new_word)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

入力データを変更している点に違和感を感じます。自分だったら、使用済みの単語を表す set() を作ると思います。

yield new_word
```

### ②

次のwordを見つけるのにハミング距離を用いた解法。問題の制約上、len(wordList)<=5000なのでwordListでループを回すと時間がかかる。①はlen(word)でループを回しており、こちらは <=10なので速い。
この解法で通ったときは8278msだったので、たまたま通っただけだろう。
get_distanceは `return sum(a[i] != b[i] for i in range(len(a)))` や `return sum(ai != bi for ai, bi in zip(a, b))` のようにsumを使っても良いと思う。

所要時間: 14:56

n: len(wordList), m: wordListに含まれるwordとbeginWordの長さの平均,
- 時間計算量: O(n^2 * m)
- 空間計算量: O(nm)

```py
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
words = set(wordList)
word_queue = deque([beginWord])
num_words = 1
while word_queue:
next_word_queue = deque()
while word_queue:
word = word_queue.popleft()
if word == endWord:
return num_words
for next_word in self._getNextWords(word, words):
next_word_queue.append(next_word)
word_queue = next_word_queue
num_words += 1
return 0

def _getNextWords(self, word: str, words: set[str]) -> Generator[str, None, None]:
def get_distance(a: str, b: str) -> int:
assert len(a) == len(b)
distance = 0
for i in range(len(a)):
if a[i] != b[i]:
distance += 1
return distance

used_words = set()
for candidate in words:
if get_distance(word, candidate) == 1:
used_words.add(candidate)
yield candidate
words.difference_update(used_words)
```

## 2nd

### 参考

- https://discord.com/channels/1084280443945353267/1227073733844406343/1235880072238334022
- https://github.com/sakupan102/arai60-practice/pull/20

先に隣接リストを作ってから幅優先探索。隣接リストにはindexを入れたが、そのまま文字列を入れても良い。usedはset()を使って `in` で判定しても良い。


```py
class Solution:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

自分はこちらの解法を先に思い付きました。ただ、 Python で O(len(wordList) ** 2) が通ると思えません。自分だったらこちらの解法を C++ で書くと思います。

def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
wordList.append(beginWord)
adjacentList = self._makeAdjacentList(wordList)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

自分で定義する変数名は、 3rd のように lower snake でよいと思います。

numWords = 1
wordIndexes = deque([len(adjacentList) - 1]) # beginWord index in wordList
used = [False] * len(wordList)
while wordIndexes:
nextWordIndexes = deque()
while wordIndexes:
wordIndex = wordIndexes.popleft()
used[wordIndex] = True
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

訪れたことを記録するタイミングは、nextWordIndexes.append(nextWordIndex) と同時にする方が好みです。

if wordList[wordIndex] == endWord:
return numWords
for nextWordIndex in adjacentList[wordIndex]:
if used[nextWordIndex]:
continue
nextWordIndexes.append(nextWordIndex)
wordIndexes = nextWordIndexes
numWords += 1
return 0

def _makeAdjacentList(self, wordList: List[str]) -> List[List[int]]:
def areAdjacentPair(x: str, y: str) -> int:
assert len(x) == len(y)
distance = 0
for i in range(len(x)):
if distance > 1:
return False
if x[i] != y[i]:
distance += 1
return distance == 1

adjacentList = [[] for _ in range(len(wordList))]
for i in range(len(wordList)):
for j in range(i + 1, len(wordList)):
if areAdjacentPair(wordList[i], wordList[j]):
adjacentList[i].append(j)
adjacentList[j].append(i)
return adjacentList
```

- https://discord.com/channels/1084280443945353267/1200089668901937312/1215117909450424410
- https://github.com/hayashi-ay/leetcode/pull/42

endWordがwordListになかったら0を返すようにしている。高速化したくなったらやるかも、くらいの感覚。

i番目の文字が任意となることを表すパターンをkeyとして、そのパターンに合致するwordの集合をvalueとする辞書を作る。
ちょっとネストが深い。
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://discord.com/channels/1084280443945353267/1200089668901937312/1216123084889788486

https://cs.stackexchange.com/questions/93467/data-structure-or-algorithm-for-quickly-finding-differences-between-strings
頭から半分または尻尾から半分が一致しているはずなので、それでバケットを作ってバケット内でのみ比較すればいいというやりかたもありますね。(編集距離が1であるかの確認に、頭から何文字一致していて、尻尾から何文字一致しているかを足してやればいいという方法をどっかで使ったことあります。)


```py
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
def make_patterns(word: str) -> Generator[str, None, None]:
for i in range(len(word)):
yield f'{word[:i]}.{word[i + 1:]}'

pattern_to_words = defaultdict(list)
for word in [beginWord] + wordList:
for pattern in make_patterns(word):
pattern_to_words[pattern].append(word)
words = [beginWord]
seen_words = set()
num_words = 1
while words:
next_words = []
for word in words:
seen_words.add(word)
if word == endWord:
return num_words
for pattern in make_patterns(word):
for next_word in pattern_to_words[pattern]:
if next_word in seen_words:
continue
next_words.append(next_word)
words = next_words
num_words += 1
return 0
```

## 3rd

```py
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
unused_words = set(wordList)

def generate_adjacent_words(word: str) -> Generator[str, None, None]:
for i in range(len(word)):
for ch in ascii_lowercase:
adjacent_word = f'{word[:i]}{ch}{word[i + 1:]}'
if adjacent_word in unused_words:
unused_words.remove(adjacent_word)
yield adjacent_word

num_words = 1
words = [beginWord]
while words:
next_words = []
for word in words:
if word == endWord:
return num_words
for next_word in generate_adjacent_words(word):
next_words.append(next_word)
words = next_words
num_words += 1
return 0
```


## 4th

https://github.com/fhiyo/leetcode/pull/22#discussion_r1642796993 の練習。


```py
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
queue = deque([(beginWord, 1)])
half_length = len(beginWord) // 2
first_half_to_words = defaultdict(list)
second_half_to_words = defaultdict(list)
used_words = set()
for word in wordList:
first_half_to_words[word[:half_length]].append(word)
second_half_to_words[word[half_length:]].append(word)
while queue:
word, length = queue.popleft()
if word == endWord:
return length
for adjacent_word in self._generate_adjacent_words(first_half_to_words, second_half_to_words, word, used_words):
queue.append((adjacent_word, length + 1))
used_words.add(adjacent_word)
return 0

def _generate_adjacent_words(
self,
first_half_to_words: dict[str, list[str]],
second_half_to_words: dict[str, list[str]],
word: str,
used_words: set[str]
):
half_length = len(word) // 2
for first_half_word in first_half_to_words[word[:half_length]]:
if self._are_adjacents(first_half_word[half_length:], word[half_length:]) \
and first_half_word not in used_words:
yield first_half_word
for second_half_word in second_half_to_words[word[half_length:]]:
if self._are_adjacents(second_half_word[:half_length], word[:half_length]) \
and second_half_word not in used_words:
yield second_half_word

def _are_adjacents(self, a: str, b: str) -> bool:
assert len(a) == len(b)
length = 0
for i in range(len(a)):
if a[i] == b[i]:
continue
length += 1
if length > 1:
return False
return length == 1
```