Skip to content
Open
Show file tree
Hide file tree
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
107 changes: 107 additions & 0 deletions problems/127.word-ladder/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
## step1
- 幅優先探索で行けるのではないかと思ったがどうか?
制約
- wordListが5000
- 単語は1~10文字
- beginWord != endWord
- 単語リストの重複はなし
- beginWordはwordListの中になくてもいいが、endWordはないとだめ

- 文字を1つ変えたものを探すのが結構大変そう
- 都度判定する場合は5000単語を確認する場合5000*10(何番目の単語が変わるか)で50000回
- 遷移可能な単語を算出して(25**10)から単語リストから照らし合わせることもできるが単語の算出が大変すぎるのでありえなさそう
- 二次元配列or辞書(word_to_next)を持っておいて連結を管理する(無向グラフだと考えて)
- その後に幅優先探索するとできそう
- 下記でTLE
```py
#
# @lc app=leetcode id=127 lang=python3
#
# [127] Word Ladder
#

# @lc code=start
import collections


class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: list[str]) -> int:
word_to_neighber = collections.defaultdict(set)
word_list_with_begin = wordList[:]
word_list_with_begin.append(beginWord)

for index, word in enumerate(word_list_with_begin):
for neighber_candidate in word_list_with_begin[index + 1 :]:
if self._is_adjacent_pair(word, neighber_candidate):
word_to_neighber[word].add(neighber_candidate)
word_to_neighber[neighber_candidate].add(word)

# endWordは必ずwordListに含まれなければ到達不可
if not word_to_neighber[endWord]:
return 0

transformation_word_count = 1
neighbers = set([beginWord])
visited_words = set([beginWord])
while neighbers:
print(neighbers)
if endWord in neighbers:
return transformation_word_count
next_neighbers = set()
for neighber in neighbers:
for next_neighber in word_to_neighber[neighber]:
if next_neighber not in visited_words:
next_neighbers.add(next_neighber)
visited_words.add(next_neighber)

neighbers = next_neighbers
transformation_word_count += 1

return 0

@staticmethod
def _is_adjacent_pair(word1, word2):
assert len(word1) == len(word2)
diff_character_count = 0
for i in range(len(word1)):
if word1[i] != word2[i]:
diff_character_count += 1

return diff_character_count == 1
# @lc code=en
```
- 時間計算量はword_to_neighberの作成でO(n^2) 1/2n(n+1) * 10 n=5000の時約5000*2500*10 = 1250万
- 幅優先探索はN+Nなはずなので改善の余地としては遷移可能かを判定する箇所?
- 良い方法が浮かばなかったのでChatGPTに聞いた
- ★ ワイルドカード中間表現を使う
- ワイルドカードの表現をkey,valueのsetに元の文字列?
- patternを毎回作るのがめんどくさい気がする
- word_to_patternも用意しておくのは?
- 新しい辞書を作成するコストは掛かるが探索する際の時間が1/文字の数にできる?(step1-2)
- 実行時間はあまり変わらなかった
- patternsの中に次の単語が含まれる割合が小さすぎる&元の単語を毎回拾いvisited_wordsの条件でskipする回数が支配的?

## step2
- 他の人のコードを見てみる
- https://github.com/mamo3gr/arai60/pull/19/files
- 選択肢のまとめがすごくわかりやすかった
- `whileブロックの主役を明らかにする。` 意識したい
- `for c1, c2 in zip(word1, word2):` zipも使えるようにしたい
- 関数に細かく分かれてあって洗練されているように感じる
- https://github.com/Yuto729/LeetCode_arai60/pull/25
- 先ほどのコードもそうだがデータを作成して操作する部分をクラスにするとスッキリ見える
- `Python の場合は、タプルも dict の Key にできるのでそれも一つかなと思います。`覚えておく
- `最短経路の単語列をすべて返す => バックトラック`最短経路の復元のやり方は思い出したい
- `wordListが非常に長い場合 => 1つのパターンあたりの要素数が平均して多くなるので, 小文字アルファベット26文字を総当りするほうが良くなるかもしれない` 確かに文字の長さが短く、wordListが非常に長い場合は有効かもしれない
- `この辺り、コードの大筋から外れて単に網羅的に1文字入れ替える作業が挟まるのが読みにくさを感じました。`僕にも当てはまっている
- https://github.com/yas-2023/leetcode_arai60/pull/20
- `文字列を2箇所以上で結合するときは、二項演算子+よりf-stringの方がパフォーマンスが優れており、Google Style Guideでもf-stringによる結合が推奨されているそうです。`読みやすさもf-stringの方が優れていると思うので積極的に使っていきたい
- https://github.com/naoto-iwase/leetcode/pull/19/files
- `これはlistでもいいのではないかと思ったのですが、wordList内に重複がある場合(今回の問題では全てunique仮定ですが)に無駄な探索を減らすためということでしょうか?`自分もListで良い時にsetを使うことがあるが、コストの観点を見ていなかった。
- `setの初期化は以下のようにも書いてもよさそうです` {initial}と書けるのを覚えておく

## step3
- 頭の中で整理できてきた気がする
- インデントのミスがあったりしたが見返して処理がイメージできるのでミスに早めに気づけた
- 1回書くのに10分~5分はかかっている

49 changes: 49 additions & 0 deletions problems/127.word-ladder/step1-2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#
# @lc app=leetcode id=127 lang=python3
#
# [127] Word Ladder
#
import collections


class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: list[str]) -> int:
# if endWord not in wordList:
# return 0

word_list_with_begin = wordList[:]
word_list_with_begin.append(beginWord)

pattern_to_words = collections.defaultdict(set)
word_to_patterns = collections.defaultdict(set)
word_length = len(beginWord)

for word in word_list_with_begin:
for i in range(word_length):
pattern = word[:i] + "*" + word[i + 1 :]
pattern_to_words[pattern].add(word)
word_to_patterns[word].add(pattern)

transformation_word_count = 1
neighbers = set([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.

nit: neighbors でしょうか。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

次に調べる予定の要素の集合に対して neighbors と名付けるのは、やや違和感があります。今調べている対象の要素に隣接し、次に調べる予定の要素に対して neighbor とつけるのは自然に感じます。

個人的には frontier または frontiers あたりが良いと思います。

visited_words = set([beginWord])

while neighbers:
if endWord in neighbers:
return transformation_word_count

next_neighbers = set()
for word in neighbers:
for pattern in word_to_patterns[word]:
for neighber in pattern_to_words[pattern]:
if neighber not in visited_words:
visited_words.add(neighber)
next_neighbers.add(neighber)

neighbers = next_neighbers
transformation_word_count += 1

return 0


# @lc code=end
48 changes: 48 additions & 0 deletions problems/127.word-ladder/step1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#
# @lc app=leetcode id=127 lang=python3
#
# [127] Word Ladder
#
import collections


class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: list[str]) -> int:
# if endWord not in wordList:
# return 0

word_list_with_begin = wordList[:]
word_list_with_begin.append(beginWord)

pattern_to_words = collections.defaultdict(set)
word_length = len(beginWord)

for word in word_list_with_begin:
for i in range(word_length):
pattern = word[:i] + "*" + word[i + 1 :]
pattern_to_words[pattern].add(word)

transformation_word_count = 1
neighbers = set([beginWord])
visited_words = set([beginWord])

while neighbers:
if endWord in neighbers:
return transformation_word_count

next_neighbers = set()
for word in neighbers:
for i in range(word_length):
pattern = word[:i] + "*" + word[i + 1 :]
for next_word in pattern_to_words[pattern]:
if next_word not in visited_words:
visited_words.add(next_word)
next_neighbers.add(next_word)

neighbers = next_neighbers
transformation_word_count += 1

return 0


# @lc code=end
58 changes: 58 additions & 0 deletions problems/127.word-ladder/step2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#
# @lc app=leetcode id=127 lang=python3
#
# [127] Word Ladder
#

# @lc code=start
import collections


class NeighberWord:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nits]
タイポですね。エディタでスペルチェックができると良いと思います。

Suggested change
class NeighberWord:
class NeighborWord:

def __init__(self):
self.pattern_to_neighbers = collections.defaultdict(set)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

どういう構造になっているのか気になる(し、add_word を読むまで分からない)ので、型ヒントで補うとよいのではないでしょうか。

Suggested change
self.pattern_to_neighbers = collections.defaultdict(set)
self.pattern_to_neighbers: dict[tuple[str, str], list[str]] = collections.defaultdict(set)

ちょっと複雑なので、tuple[str, str] を独自の型で置き直したい気もします。


def add_word(self, word):
for pattern in self.word_to_pattern_iter(word):
self.pattern_to_neighbers[pattern].add(word)

@staticmethod
def word_to_pattern_iter(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.

自分なら名前よりタイプヒントを書くかなと思いました。mypyなどの静的解析ツールも使えますし。

Suggested change
def word_to_pattern_iter(word):
def word_to_pattern(word) -> Iterable[tuple[str, str]]:

for i in range(len(word)):
yield word[:i], word[i + 1 :]

def get_neighber_iter(self, word):
for pattern in self.word_to_pattern_iter(word):
for neighber in self.pattern_to_neighbers[pattern]:
yield neighber


class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
neighber_words = NeighberWord()
neighber_words.add_word(beginWord)
for word in wordList:
neighber_words.add_word(word)

visited_words = {beginWord}
neighbers = {beginWord}
transformation_word_count = 1
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

問題文や既存のコードに含まれる単語を使う方が誤解が少ないと思うので、自分なら以下にします。

Suggested change
transformation_word_count = 1
ladder_length = 1

現状の方向性でいくなら num_transformation あるいは word_count あたりがしっくりきます。


while neighbers:
if endWord in neighbers:
return transformation_word_count

next_neighbers = set()
for neighber in neighbers:
for next_neighber in neighber_words.get_neighber_iter(neighber):
if next_neighber in visited_words:
continue
visited_words.add(next_neighber)
next_neighbers.add(next_neighber)
neighbers = next_neighbers
transformation_word_count += 1

return 0


# @lc code=end
58 changes: 58 additions & 0 deletions problems/127.word-ladder/step3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#
# @lc app=leetcode id=127 lang=python3
#
# [127] Word Ladder
#

# @lc code=start
import collections


class NeighberWord:
def __init__(self):
self.pattern_to_neighbers = collections.defaultdict(set)

def add_word(self, word):
for pattern in self.word_to_pattern_iter(word):
self.pattern_to_neighbers[pattern].add(word)

@staticmethod
def word_to_pattern_iter(word):
for i in range(len(word)):
yield word[:i], word[i + 1 :]

def get_neighber_iter(self, word):
for pattern in self.word_to_pattern_iter(word):
for neighber in self.pattern_to_neighbers[pattern]:
yield neighber


class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
neighber_word = NeighberWord()
neighber_word.add_word(beginWord)
for word in wordList:
neighber_word.add_word(word)

neighbers = {beginWord}
visited_words = {beginWord}
transformation_word_count = 1

while neighbers:
if endWord in neighbers:
return transformation_word_count
next_neighbers = set()

for neighber in neighbers:
for next_neighber in neighber_word.get_neighber_iter(neighber):
if next_neighber in visited_words:
continue
next_neighbers.add(next_neighber)
visited_words.add(next_neighber)
neighbers = next_neighbers
transformation_word_count += 1

return 0


# @lc code=end