Conversation
| @@ -0,0 +1,127 @@ | |||
| グラフの問題として解ける。beginWordからendWordまでの最短経路を求めれば良いので、BFSが良い。 | |||
| hitの次の候補は"\*it", "h\*t", "hi\*"になる。\*には任意のアルファベットが入る。 | |||
| 事前に"\*it"などの一文字を任意にした状態のワードの一覧を作ってあげれば次のノードを見つけるのが楽になる。 | |||
There was a problem hiding this comment.
考え方がやや複雑に感じました。 1 <= beginWord.length <= 10、 1 <= wordList.length <= 5000 のため、あらかじめ全単語対の異なる文字数が 1 文字かどうかを愚直に調べ、グラフ構造に落とし込んだほうが、シンプルになるように思いました。
There was a problem hiding this comment.
確かにそうですね。一度解いたことがあり、解法を覚えていたのでこちらで解きましたが、素直に隣接リストを作るのが自然ですね。
(メモ)
wordの長さをM, wordListをNとしたときに、一文字を任意にした一覧を作る方法だとO(MN)で単語ごとに比較して隣接リスト方法だとO(MN^2)だが、Nが5000程度なら隣接リストを作成する方法も許容できる。
There was a problem hiding this comment.
解きました。こちらの方がシンプルで良いですね。
ただLeetCodeだとM=10, N=5000のケースがあり、TLEの基準の10s前後の実行時間になりました。
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
def hamming_distance(word1, word2):
distance = 0
for i in range(len(word1)):
if word1[i] != word2[i]:
distance += 1
# LeetCodeのTLEを回避するために早めに打ち切る
# 結構ギリギリでTLEを超える場合もある
if distance > 1:
return distance
return distance
adj = defaultdict(list)
for i in range(len(wordList) - 1):
for j in range(i + 1, len(wordList)):
word1, word2 = wordList[i], wordList[j]
if hamming_distance(word1, word2) == 1:
adj[word1].append(word2)
adj[word2].append(word1)
if beginWord not in wordList:
for word in wordList:
if hamming_distance(beginWord, word) == 1:
adj[beginWord].append(word)
adj[word].append(beginWord)
words = [beginWord]
seen = set(words)
num_of_words = 1
while words:
next_words = []
num_of_words += 1
for word in words:
for next_word in adj[word]:
if next_word in seen:
continue
if next_word == endWord:
return num_of_words
seen.add(next_word)
next_words.append(next_word)
words = next_words
return 0There was a problem hiding this comment.
C++ だと 329ms だったのでうっかりしていました。
言語によって速度がまちまちです。主要な言語については、どの程度の速度差があるか覚えておくとよいと思います。
https://github.com/niklas-heer/speed-comparison
There was a problem hiding this comment.
PythonはC++に比べて50倍くらいを目安にしていましたが、添付の結果だと100倍弱ありそうですね。
同じスクリプト言語のJavaScriptが3から4倍ほどと早いのはJITの影響でしょうか?Python3.13からJITが入るそうなので、Pythonの速度差も今後縮まるかもですね。
https://tonybaloney.github.io/posts/python-gets-a-jit.html
There was a problem hiding this comment.
JavaScript が速い具体的な原因は分かりません。ただ、ブラウザの中の人がかなり頑張って最適化しているという話は聞いたことがあります。
Python に関しては、 3.11 から高速化に注力しているとの事ですので、少しずつ速くなっていくと思います。
There was a problem hiding this comment.
V8 EngineにGoogleの人材が投下されてて色々な高速化がされて複合的に早いという感じなんですね。
| groups = defaultdict(list) | ||
| for word in wordList: | ||
| for i in range(len(word)): | ||
| key = f"{word[:i]}*{word[i+1:]}" |
There was a problem hiding this comment.
キーを作成するコードが複数回登場するので、関数に切り出したほうが読みやすくかつ、書き間違えにくくなると思います。
| seen = set() | ||
| while words: | ||
| num_of_words += 1 | ||
| next_words = [] |
There was a problem hiding this comment.
next_words に同じ単語が重複して含まれる場合があるように思います。処理結果は正しいと思うのですが、無駄な処理になっていると思います。
|
|
||
| num_of_words = 0 | ||
| words = [beginWord] | ||
| seen = set() |
There was a problem hiding this comment.
このタイミングで seen に beginWord を入れてしまうのはいかがでしょうか?
There was a problem hiding this comment.
wordsをこれから訪れる一覧ではなく、すでに訪れた一覧と考えるということですね。効率は先にseenに入れてしまう方が良さそうですね。
| key = f"{word[:i]}*{word[i+1:]}" | ||
| for next_word in groups[key]: | ||
| if next_word not in seen: | ||
| next_words.append(next_word) |
There was a problem hiding this comment.
このタイミングで seen に next_word を入れれば、 next_words に同じ単語が重複してはいることは亡くなると思います。
また、その場合、
if next_word in seen:
と書いたほうが、ソースコードが読みやすくなると思います。
| for word in words: | ||
| if word == endWord: | ||
| return num_of_words | ||
| if word in seen: |
There was a problem hiding this comment.
26~28 行目の処理は省略し、最内周ループで seen の処理を行うとよいと思います。
| return 0 | ||
| ``` | ||
|
|
||
| 両側から。 |
There was a problem hiding this comment.
一応LeetCodeの解法にあったのでこちらでも解いてみました。
LeetCodeのテストの通過が120msくらいが90msと確かに早くなったのですが、書いていて難しかったので実際はシンプルな1stのアプローチが良さそうですね。
There was a problem hiding this comment.
beginWordの長さが多くなると横に広がるので両側探索のメリットがよりあるかもですね。
| patterns = make_patterns(word) | ||
| for pattern in patterns: |
There was a problem hiding this comment.
ここだけ他と記述方法が異なっています。
for next_word in pattern_to_words[pattern]:
個人的には一度変数に格納する今の書き方のほうが読みやすいです。
| seen_from_end = set([endWord]) | ||
| num_of_words = 2 | ||
| while words_from_begin and words_from_end: | ||
| words, seen, other_seen = words_from_begin, seen_from_begin, seen_from_end |
There was a problem hiding this comment.
3変数に同時に代入は読むのがつらいと感じました。
swapでないなら、1行ずつ代入でいいのではないかと思いました。
There was a problem hiding this comment.
確かに変数名も長いですし、1行だと読みづらいですね。
https://leetcode.com/problems/word-ladder