Skip to content

Word Ladder.md #18

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
243 changes: 243 additions & 0 deletions 127. Word Ladder.md
Original file line number Diff line number Diff line change
@@ -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):
Copy link

Choose a reason for hiding this comment

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

長さが違うものが来たらどうなるか考えておきましょう。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。
せっかくなので、面接だったらどういう選択肢を視野に入れて面接官とディスカッションするか考えてみました。

  • 異常な値として対応するパターン
    • 例外を投げて処理を停止する。
      • wordListに異なる値が含まれている時点で例外を投げる。
    • 異常な値だけ無視して処理を続行する。
      • “正しい長さ”も受け取るように修正する方法。
      • データの内容から”正しい長さ”も決める方法。(多数決など)
  • 正常な値として対応するパターン
    • adjacentの定義をハミング距離からレーベンシュタイン距離に変更して対応する

P.S. レーベンシュタイン距離は、正式な用語がわからずググりました。また、ハミング距離は”ハフマン距離”と間違えて覚えていました。。。
https://ja.wikipedia.org/wiki/%E3%83%AC%E3%83%BC%E3%83%99%E3%83%B3%E3%82%B7%E3%83%A5%E3%82%BF%E3%82%A4%E3%83%B3%E8%B7%9D%E9%9B%A2

Choose a reason for hiding this comment

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

自分なら、問題としては長さが違うものが来てもOK。単にその単語が到達不能なだけで無視すれば良い。
is_adjacent_pairの入力として長さが違う物が来たら、Falseを返すかなという感じです。

Choose a reason for hiding this comment

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

正常な値として対応するパターン
adjacentの定義をハミング距離からレーベンシュタイン距離に変更して対応する

これは問題全体の要件が変わっているじゃんという気持ちです。まあそのレイヤーでの話をしているなら、良いですが、odaさんの意図とは違う気がします。

distance = 0
for i in range(len(str1)):
if str1[i] != str2[i]:
distance += 1
Copy link

Choose a reason for hiding this comment

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

distance が 2 になったところで return False すると、処理量が少し減ると思います。

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
Copy link

Choose a reason for hiding this comment

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

2以上になったら False を返すのも一つです。

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
Comment on lines +160 to +161
Copy link

Choose a reason for hiding this comment

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

L148~149かここの、どっちかは必要ないですかね

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

Copy link

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/1295357747545505833/1309222881330335816
変えるところを "*" に置き換えてルックアップという方法もありました。

```python
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
def generate_adjacents(word):
Copy link

Choose a reason for hiding this comment

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

個人的にはここは外側で定義して、generate_unused_adjacentsとして、引数はunused_wordsも含めます。(その方がインターフェーとしてわかりすい気がしました)

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 つけるのを補助関数にするなどを考えていました。
Copy link

Choose a reason for hiding this comment

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

これ、たとえば、大小関係を入れようとすると、大小関係の確認に文字列を比べてしまい、hamming 距離の計算と遜色がない時間かかるので、id 化するなどちょっと工夫が必要でしょう。

- 今回の実装と関係ないけど、気にしたことがなかった。

# Step 3

- 実装時間: 4分
- wordListの要素数を`n`, wordの長さ`l`をとして、
- 時間計算量: O(ln)
- generate_adjacentsでO(26 * l)

Choose a reason for hiding this comment

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

文字数だけループするのと、その中で新しい文字列を作成しているのでl^2ですかね?
なんかlだと視認性が悪いので他のアルファベットを使ってもらう方が良いかもです。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます!
文字列作成気づいてませんでした。ご指摘の通りです。

lが見にくいも確かにです。ありがとうございます。

- メインのループでO(l)

Choose a reason for hiding this comment

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

これはO(n)?

- 空間計算量: 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)])

Choose a reason for hiding this comment

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

好みの問題かもですが、words_and_lengthsと、両方とも複数形にするほうが違和感が少ないと思いました。

while word_and_lengths:
word, length = word_and_lengths.popleft()
if word == endWord:

Choose a reason for hiding this comment

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

この判定をnext_wordに対して行うことで、dequeに入れる前に判定できるので少しだけ高速化可能です。

return length
for next_word in generate_adjacents(word):
word_and_lengths.append((next_word, length + 1))
unused_words.remove(next_word)
Copy link

Choose a reason for hiding this comment

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

個人的にはused_wordsに突っ込んでく方が好きですが、好みの問題かもしれません

return 0
```

- 書いた感想
- 変数定義の場所について
- `unused_words`は補助関数`generate_adjacents()`でも使うので、最初に定義したい。
- 関数外の変数を見に行くのちょっと嫌だけど、`ladderLength()`の関数内だし許容する。
- 一方で、`word_and_lengths`はループの中で使うものなので、whileループの近くで定義したい。
- 昨日と同様、覚えるというよりも考えて出力してる感じだった。今日も半日ぐらい間を空けたけど、気持ちよく書けた。
- `ascii_lowercase`だけ正式な名前が思い出せなくて、ドキュメントを見た。