-
Notifications
You must be signed in to change notification settings - Fork 0
127. Word Ladder #22
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
base: main
Are you sure you want to change the base?
127. Word Ladder #22
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,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分はかかっている | ||
|
|
| 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]) | ||
|
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. 次に調べる予定の要素の集合に対して 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 | ||
| 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 |
| 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: | ||||||
|
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. [nits]
Suggested change
|
||||||
| def __init__(self): | ||||||
| self.pattern_to_neighbers = collections.defaultdict(set) | ||||||
|
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. どういう構造になっているのか気になる(し、
Suggested change
ちょっと複雑なので、 |
||||||
|
|
||||||
| 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): | ||||||
|
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. 自分なら名前よりタイプヒントを書くかなと思いました。mypyなどの静的解析ツールも使えますし。
Suggested change
|
||||||
| 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 | ||||||
|
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. 問題文や既存のコードに含まれる単語を使う方が誤解が少ないと思うので、自分なら以下にします。
Suggested change
現状の方向性でいくなら |
||||||
|
|
||||||
| 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 | ||||||
| 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 |
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.
nit: neighbors でしょうか。