-
Notifications
You must be signed in to change notification settings - Fork 0
127. Word Ladder #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
base: main
Are you sure you want to change the base?
127. Word Ladder #18
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,23 @@ | ||
| # 127. Word Ladder | ||
| - dijkstraを使った(sol1.py)が、コストが1なのでbfsで十分だあと気づいた | ||
| - 時間計算量 | ||
| - はじめに隣接行列をつくるところでO(LN**2) | ||
| - ヒープ部分でO((N+E)log N) | ||
| - 空間計算量 O(E) | ||
| - 距離行列以外の場合にもグラフと見做せることを強く認識すべき | ||
| - https://github.com/mamo3gr/arai60/blob/127_word-ladder/127_word-ladder/memo.md | ||
| - 丁寧に解いているので参考になる | ||
| - subwordを利用すれば事前計算が O(LN) の時間計算量となるのはなるほど | ||
| - デコレータclassmethodとstaticmethodの使い分け | ||
| - bfsを使おうと思ったら愚直にキューを選択してしまいそうなので練習を兼ねてレベルbfsを書いてみる(sol2.py) | ||
| - subwordの生成は理論的にはO(NL)だがスライス生成でO(NL**2)。組み込みを使っているので実際には高速だろう。 | ||
| - BFSの計算量は、最悪O(N**2L) (各単語subword L 個、to_visit N 個) | ||
| - to_visitが分散してくれれば O(NL) より少し大きいぐらいか | ||
| - 実際かなり速かった | ||
| - 双方向BFS | ||
| - https://github.com/plushn/SWE-Arai60/pull/20/changes#r2588357494 | ||
| - 自力では書けそうにない | ||
| - sol3.py | ||
| - 常に小さい方から広げることで計算量を削減 | ||
| - 時間計算量 O(LN) (charの数を定数とみなす) | ||
| - 空間計算量 O(N) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import heapq | ||
|
|
||
|
|
||
| class Solution: | ||
| def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: | ||
| def is_adjacent(w1, w2): | ||
| if len(w1) != len(w2): | ||
| return False | ||
| diff = 0 | ||
| for i in range(len(w1)): | ||
| if w1[i] != w2[i]: | ||
| diff += 1 | ||
| if diff > 1: | ||
| return False | ||
| return diff == 1 | ||
|
|
||
| idx_endWord = -1 | ||
| for i, w in enumerate(wordList): | ||
| if w == endWord: | ||
| idx_endWord = i | ||
| break | ||
| if idx_endWord == -1: | ||
| return 0 | ||
|
|
||
| wordList.append(beginWord) | ||
| n = len(wordList) | ||
| adjacent_matrix = [[] for _ in range(n)] | ||
| for i in range(n - 1): | ||
| wi = wordList[i] | ||
| for j in range(i + 1, n): | ||
| if is_adjacent(wi, wordList[j]): | ||
| adjacent_matrix[i].append(j) | ||
| adjacent_matrix[j].append(i) | ||
|
|
||
| INF = float("inf") | ||
| costs = [INF] * n | ||
| costs[-1] = 0 | ||
| cost_heap = [(0, n - 1)] | ||
| while cost_heap: | ||
| c, v = heapq.heappop(cost_heap) | ||
| if c > costs[v]: | ||
| continue | ||
| if v == idx_endWord: | ||
| return c + 1 | ||
| for i in adjacent_matrix[v]: | ||
| if c + 1 < costs[i]: | ||
| costs[i] = c + 1 | ||
| heapq.heappush(cost_heap, (costs[i], i)) | ||
| return costs[idx_endWord] + 1 if costs[idx_endWord] != INF else 0 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| from collections import defaultdict | ||
|
|
||
|
|
||
| class NeighborWords: | ||
| def __init__(self): | ||
| self.subword_to_words = defaultdict(list) | ||
|
|
||
| @classmethod | ||
| def from_words(cls, words): | ||
| neighbor_words = NeighborWords() | ||
| for word in words: | ||
| neighbor_words.add(word) | ||
| return neighbor_words | ||
|
|
||
| @staticmethod | ||
| def to_subwords(word): | ||
| for i in range(len(word)): | ||
| yield (word[:i], word[i + 1 :]) | ||
|
|
||
| def add(self, word): | ||
| for subword in self.to_subwords(word): | ||
| self.subword_to_words[subword].append(word) | ||
|
|
||
| def iter_all(self, word): | ||
| for subword in self.to_subwords(word): | ||
| yield from self.subword_to_words[subword] | ||
|
|
||
|
|
||
| class Solution: | ||
| def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: | ||
| neighbor_words = NeighborWords.from_words(wordList) | ||
| to_visit = {beginWord} | ||
| level = 1 | ||
| visited = set() | ||
| while to_visit: | ||
| next_to_visit = set() | ||
| for word in to_visit: | ||
| if word == endWord: | ||
| return level | ||
| if word in visited: | ||
| continue | ||
| for neighbor in neighbor_words.iter_all(word): | ||
| next_to_visit.add(neighbor) | ||
| visited.add(word) | ||
| to_visit = next_to_visit | ||
| level += 1 | ||
| return 0 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| class Solution: | ||
| def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: | ||
| if beginWord == endWord: | ||
| return 1 | ||
|
|
||
| word_set = set(wordList) | ||
| if endWord not in word_set: | ||
| return 0 | ||
|
|
||
| word_set.discard(beginWord) | ||
|
|
||
| begin_frontier = {beginWord} | ||
| end_frontier = {endWord} | ||
|
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. 細かいですが、snake_caseとlowerCamelCaseが入り混じった変数名には意図があるのか気になりました。
Owner
Author
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. 引数がCamel Caseで自分が使っているのがsnake_caseなのでこうなってしまいました。 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. 本当は LeetCode のシグネイチャーが標準的な記法に従っていないのですが、Python の場合は、keyword 呼び出しがあるので、シグネイチャーを変えると意味が変わる可能性がありますね。
Owner
Author
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. https://docs.python.org/3/tutorial/controlflow.html#special-parameters |
||
| level = 1 | ||
| word_len = len(beginWord) | ||
| letters = "abcdefghijklmnopqrstuvwxyz" | ||
|
|
||
| while begin_frontier and end_frontier: | ||
| if len(begin_frontier) > len(end_frontier): | ||
| begin_frontier, end_frontier = end_frontier, begin_frontier | ||
|
|
||
| next_frontier = set() | ||
| for word in begin_frontier: | ||
| for i in range(word_len): | ||
| prefix = word[:i] | ||
| suffix = word[i + 1 :] | ||
| original = word[i] | ||
| for ch in letters: | ||
| if ch == original: | ||
| continue | ||
| candidate = prefix + ch + suffix | ||
| if candidate in end_frontier: | ||
| return level + 1 | ||
| if candidate in word_set: | ||
| word_set.remove(candidate) | ||
| next_frontier.add(candidate) | ||
|
|
||
| begin_frontier = next_frontier | ||
| level += 1 | ||
|
|
||
| return 0 | ||
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.
クラスが機能するために必要な情報はコンストラクタ(あるいはbuilderなど)で与えられるべきです。from_wordsを最初に絶対に使わないと使い間違えるクラスになっていませんか?
また、from_wordsは何度でも呼び出せる必要があるものでしょうか?そうでないならばクラス生成後はこのメソッドを叩けないのが望ましいです
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.
classmethodなどは使わずに def init(self, words): とするべきですね。ご指摘のとおりだと思います。