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
23 changes: 23 additions & 0 deletions 127/memo.md
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)
49 changes: 49 additions & 0 deletions 127/sol1.py
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
47 changes: 47 additions & 0 deletions 127/sol2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from collections import defaultdict


class NeighborWords:
Copy link
Copy Markdown

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は何度でも呼び出せる必要があるものでしょうか?そうでないならばクラス生成後はこのメソッドを叩けないのが望ましいです

Copy link
Copy Markdown
Owner Author

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): とするべきですね。ご指摘のとおりだと思います。

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
41 changes: 41 additions & 0 deletions 127/sol3.py
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}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

細かいですが、snake_caseとlowerCamelCaseが入り混じった変数名には意図があるのか気になりました。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

引数がCamel Caseで自分が使っているのがsnake_caseなのでこうなってしまいました。
これを気持ち悪いと思う感覚は大事ですね

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

本当は LeetCode のシグネイチャーが標準的な記法に従っていないのですが、Python の場合は、keyword 呼び出しがあるので、シグネイチャーを変えると意味が変わる可能性がありますね。

https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.wqduubixdpe4

Copy link
Copy Markdown
Owner Author

@tom4649 tom4649 Mar 16, 2026

Choose a reason for hiding this comment

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

https://docs.python.org/3/tutorial/controlflow.html#special-parameters
pos_only, /, standard, *, kwd_only と書くこと、知らなかったです。勉強になりました。

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