難問の解説:Word Ladder IIとAlien Dictionary
BFS+バックトラッキングによるword-ladder-IIと、トポロジカルソートによるalien-dictionaryという2つの難問に、完全な説明を交えながら最初から最後まで取り組みます。
「難問の解説:Word Ladder IIとAlien Dictionary」はCoddyKit上の無料Coding Interview Prepレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはCoding Interview Prep学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Coding Interview Prepコースには全4レッスンが含まれています。
難しい問題が異なる理由
難易度の高いLeetCode問題は、中級の問題と比べて2つの点で異なります。(1) 2つ以上のアルゴリズム技法を組み合わせる必要があること、(2) 問題文だけから最適解を見抜くのが難しいことです。表面的な説明の奥にあるグラフ構造やDP構造を見抜かなければなりません。Word Ladder IIとAlien Dictionaryは、FAANGの面接に繰り返し登場する、難問の代表例です。
難しい問題に取り組むときは、最初から完全な解法を見つけようとしないでください。代わりに、問題を小さな部分問題に分け、それぞれの構造を特定し、個別に解いてから、それらをつなぎ合わせます。このモジュール化して考える力が、プレッシャーの中で難問を解く鍵となります。
# Hard problem meta-strategy
strategy = [
'1. Read the problem 2x — hard problems often have subtle constraints',
'2. Model it as a known structure: graph? DP table? sorted order?',
'3. Break into sub-problems: separate the graph-building from the traversal',
'4. Solve sub-problems in order, verifying each before connecting',
'5. Handle the edge case where no solution exists (empty result, -1, [])',
'6. Optimise only after the correct but slow solution works',
]
print('Hard problem meta-strategy:')
for step in strategy:
print(f' {step}')Word Ladder II:問題文
Word Ladder II(LeetCode 126):開始単語、終了単語、単語リストが与えられたとき、開始から終了までのすべての最短変換シーケンスを見つけます。各ステップでは必ず1文字だけを変え、途中の各単語は単語リストに含まれていなければなりません。最短経路を1つだけ見つけるWord Ladder Iよりも、すべての最適経路を列挙する必要があるため、厳密に難しい問題です。
例:beginWord='hit'、endWord='cog'、wordList=['hot','dot','dog','lot','log','cog'] → [['hit','hot','dot','dog','cog'],['hit','hot','lot','log','cog']]。どちらも長さは5です。
# Word Ladder II problem breakdown
begin_word = 'hit'
end_word = 'cog'
word_list = ['hot','dot','dog','lot','log','cog']
# What we need:
# 1. Build a graph: word -> set of words that differ by one character
# 2. BFS to find the MINIMUM number of steps (shortest path distance)
# 3. DFS/backtracking to enumerate ALL paths of that minimum length
# Key insight: BFS finds shortest distance; DFS reconstructs all shortest paths
# Two-phase approach:
print('Phase 1: BFS from begin_word to find min distance to each word')
print('Phase 2: DFS/backtrack from end_word using only edges that decrease distance')
print()
print(f'Input: {begin_word} -> {end_word}')
print(f'Word list: {word_list}')
print('Expected: [[hit,hot,dot,dog,cog],[hit,hot,lot,log,cog]]')Word Ladder II:BFSフェーズ
フェーズ1では、開始単語からレベルごとにBFSを実行します。各レベルで、すべての隣接語(1文字だけ異なる単語)を見つけます。各単語に初めて到達したレベル(開始地点からの距離)を記録します。end_word に到達しても停止しません。すべての最短経路を探索できるよう、end_word が見つかったレベルが終わるまで続けます。
重要なのは、各単語について、いずれかの最短経路でその単語の直前に来ることのできる単語の集合を対応付ける parents 辞書を構築することです。これが、フェーズ2でバックトラッキングに使用するグラフになります。
from collections import defaultdict, deque
def find_parents(begin, end, word_set):
parents = defaultdict(set)
layer = {begin}
found = False
while layer and not found:
next_layer = set()
for word in layer:
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
new_word = word[:i] + c + word[i+1:]
if new_word in word_set and new_word not in parents:
next_layer.add(new_word)
parents[new_word].add(word)
if new_word == end:
found = True
layer = next_layer
return parents if found else {}
words = {'hot','dot','dog','lot','log','cog'}
parents = find_parents('hit', 'cog', words)
print('Parents map (which words can precede each word):')
for word, preds in sorted(parents.items()):
print(f' {word}: {preds}')Word Ladder II:DFSバックトラッキングフェーズ
フェーズ2では、終了単語からDFSバックトラッキングを行い、parents マップを逆向きにたどります。終了から開始に向かって経路を構築し、最後に反転させます。開始単語に到達したら、完全な最短経路が見つかったことになります。parentsマップによって、見つかるすべての経路が最小の長さであることが保証されます。より長い経路へ「逸れる」ことはありません。
この2段階のアプローチ(レベルの探索にはBFS、経路の復元にはDFS)が標準的な解法です。BFSは O(n × L × 26) で実行されます。ここで n は単語リストのサイズ、L は単語の長さです。さらに、DFSは O(K × L) です。Kは最短経路の数を表します。
def find_ladders(beginWord, endWord, wordList):
word_set = set(wordList)
if endWord not in word_set:
return []
# Phase 1: BFS to build parents map
parents = defaultdict(set)
layer = {beginWord}
found = False
visited = {beginWord}
while layer and not found:
next_layer = set()
for word in layer:
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
nw = word[:i] + c + word[i+1:]
if nw in word_set and nw not in visited:
next_layer.add(nw)
parents[nw].add(word)
if nw == endWord: found = True
visited |= next_layer
layer = next_layer
# Phase 2: DFS backtrack from endWord to beginWord
result = []
def dfs(word, path):
if word == beginWord:
result.append(path[::-1])
return
for parent in parents[word]:
dfs(parent, path + [parent])
dfs(endWord, [endWord])
return result
print(find_ladders('hit','cog',['hot','dot','dog','lot','log','cog']))Alien Dictionary:問題文
Alien Dictionary(LeetCode 269):異星語で辞書順に並べられた単語のリストが与えられたとき、その言語における文字の順序を求めます。文字の順序を文字列として返してください。有効な順序が存在しない場合(矛盾している場合)は、空文字列を返します。
例:['wrt','wrf','er','ett','rftt'] → 'wertf'。隣接する単語を比較すると、wrtとwrfから 't' < 'f'、wrtとerから 'w' < 'e'、erとettから 'r' < 't'、ettとrfttから 'e' < 'r' が分かります。これは、これらの文字順序の制約に対するトポロジカルソートです。
words = ['wrt', 'wrf', 'er', 'ett', 'rftt']
# Compare adjacent pairs to extract ordering:
# wrt vs wrf: first diff at index 2: t < f (t comes before f)
# wrf vs er: first diff at index 0: w < e (w comes before e)
# er vs ett: first diff at index 1: r < t (r comes before t)
# ett vs rftt:first diff at index 0: e < r (e comes before r)
ordering_constraints = [
('t', 'f', 'from wrt vs wrf'),
('w', 'e', 'from wrf vs er'),
('r', 't', 'from er vs ett'),
('e', 'r', 'from ett vs rftt'),
]
print('Ordering constraints extracted from adjacent word pairs:')
for a, b, source in ordering_constraints:
print(f' {a} -> {b} ({source})')
print('\nThis is a directed graph: find topological order = alien alphabet order')Alien Dictionary:グラフの構築
最初のステップは制約の抽出です。隣接する単語の各ペアを比較し、最初に異なる文字を見つけ、小さい文字から大きい文字への有向辺を追加します。ある単語が次の単語の接頭辞でありながら、次の単語より長い場合(例:'abc'が'ab'の前にある場合)、入力は無効です。その場合は直ちに空文字列を返します。
単語リストに現れるすべての文字は、順序の制約がない場合でもグラフのノードになります。これらの孤立したノードは、最終的な順序のどこにでも配置できます。
from collections import defaultdict
def build_alien_graph(words):
adj = defaultdict(set) # char -> set of chars that come after it
in_degree = {c: 0 for word in words for c in word}
for i in range(len(words) - 1):
w1, w2 = words[i], words[i+1]
min_len = min(len(w1), len(w2))
found_diff = False
for j in range(min_len):
if w1[j] != w2[j]:
if w2[j] not in adj[w1[j]]: # avoid duplicate edges
adj[w1[j]].add(w2[j])
in_degree[w2[j]] += 1
found_diff = True
break
if not found_diff and len(w1) > len(w2):
return {}, {} # invalid: 'abc' before 'ab'
return adj, in_degree
words = ['wrt', 'wrf', 'er', 'ett', 'rftt']
adj, in_degree = build_alien_graph(words)
print('Adjacency list (directed):', {k: list(v) for k, v in adj.items()})
print('In-degrees:', in_degree)Alien Dictionary:トポロジカルソート
グラフを構築したら、KahnのBFSトポロジカルソートを適用します。まず、入次数が0(前提条件がない)のすべての文字をキューに入れます。各文字を処理し、その後続文字の入次数を減らします。後続文字の入次数が0になったら、キューに追加します。処理した順に文字を集めると、それが異星語のアルファベット順になります。
結果にすべての文字が含まれていれば、有効な順序です。予想より少ない文字しか含まれていなければ、サイクルが存在します。つまり制約が矛盾しているため、空文字列を返します。
from collections import deque, defaultdict
def alien_order(words):
adj = defaultdict(set)
in_degree = {c: 0 for word in words for c in word}
for i in range(len(words) - 1):
w1, w2 = words[i], words[i + 1]
min_len = min(len(w1), len(w2))
found = False
for j in range(min_len):
if w1[j] != w2[j]:
if w2[j] not in adj[w1[j]]:
adj[w1[j]].add(w2[j])
in_degree[w2[j]] += 1
found = True; break
if not found and len(w1) > len(w2):
return '' # invalid: 'abc' before 'ab'
# Kahn's BFS topological sort
queue = deque([c for c in in_degree if in_degree[c] == 0])
result = []
while queue:
c = queue.popleft()
result.append(c)
for neighbor in sorted(adj[c]): # sort for determinism
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
return ''.join(result) if len(result) == len(in_degree) else ''
print(alien_order(['wrt','wrf','er','ett','rftt'])) # e.g., 'wertf'
print(alien_order(['z','x'])) # 'zx'
print(alien_order(['z','x','z'])) # '' (cycle z->x->z)エッジケースへの対応:両方の問題
Word Ladder IIとAlien Dictionaryには、対応しないと誤答につながる微妙なエッジケースがあります。
- Word Ladder II:beginWordとendWordが同じ場合(
[[beginWord]]、または長さ1を返します)。endWordがwordListに含まれていない場合(空を返します)。経路が存在しない場合(空を返します)。 - Alien Dictionary:重複する単語(制約を抽出しません)。単語が1つだけの場合(すべての重複しない文字を返します)。制約にサイクルがある場合(''を返します)。ある単語が次の単語より長い接頭辞である場合(入力が無効なので、''を返します)。すべての文字が孤立している場合(任意の順序を返します)。
# Edge case tests for Word Ladder II
def test_word_ladder_edge_cases():
from collections import defaultdict
def find_ladders(begin, end, word_list):
# [abbreviated implementation for testing]
if end not in word_list: return []
if begin == end: return [[begin]]
return [] # placeholder
tests = [
('hit', 'cog', ['hot','dot','dog','lot','log'], []), # no path (cog missing)
('hit', 'hit', ['hit'], [['hit']]), # begin==end
('a', 'c', ['a','b','c'], [['a','c']]), # short words
]
for begin, end, wl, expected in tests:
result = find_ladders(begin, end, wl)
print(f'{begin}->{end}: result={result}')
# Edge case tests for Alien Dictionary
def test_alien_edge_cases():
from collections import defaultdict, deque
# (using alien_order from previous scene)
tests = [
(['abc', 'ab'], ''), # 'abc' before 'ab' = invalid
(['a'], 'a'), # single word
(['z','z'], 'z'), # duplicate: no constraint
]
print('Alien dictionary edge cases:')
for words, expected in tests:
print(f' {words} -> expected: "{expected}"')
test_word_ladder_edge_cases()
test_alien_edge_cases()計算量の分析:両方の問題
Word Ladder IIの計算量:BFSフェーズは O(n × L × 26) です。nはリスト内の単語数、Lは単語の長さを表します。BFSの各レベルで各単語について26L個の候補単語を生成し、単語集合に含まれているかを確認します。各確認は O(1) です。DFSフェーズは O(K × L) で、Kは最短経路の数です(理論上は指数関数的になる可能性があります)。
Alien Dictionaryの計算量:グラフの構築は O(C) です。Cはすべての単語に含まれる文字の合計数を表します。トポロジカルソートは O(V + E) です。Vは重複しない文字数、Eは順序制約の数を表します。全体では O(C)、つまり入力に含まれる文字の総数に対して線形です。
# Complexity analysis for both problems
complexities = [
{
'problem': 'Word Ladder II',
'time': 'O(n * L * 26) BFS + O(K * L) DFS backtracking',
'space': 'O(n * L) for word set + parents map',
'notes': 'K (number of shortest paths) can be exponential in pathological cases',
},
{
'problem': 'Alien Dictionary',
'time': 'O(C) where C = total characters in all words',
'space': 'O(V + E) for adjacency list',
'notes': 'V <= 26 (alphabet), E <= V^2 = 676; often treated as O(C) total',
},
]
for c in complexities:
print(f'{c["problem"]}:')
print(f' Time: {c["time"]}')
print(f' Space: {c["space"]}')
print(f' Notes: {c["notes"]}')
print()パターンのまとめ:再利用できる2つのテンプレート
どちらの問題からも、再利用できるパターンを学べます。Word Ladder II = 距離を求めるBFS + 経路を復元するDFS:このパターンは、重みなしグラフですべての最短経路が必要な場合に登場します。BFS中に親マップを構築し、その後、目的地から始点へバックトラックします。
Alien Dictionary = 辺の抽出 + トポロジカルソート:このパターンは、整列されたシーケンスが与えられ、その背後にある順序規則を推測しなければならない場合に登場します。隣接するペアから有向制約を抽出し、その後Kahnのアルゴリズムを適用します。サイクルを検出した場合(順序を作れない場合)は '' を返します。
# Pattern templates
print('Template 1: All Shortest Paths in Unweighted Graph')
template_1 = '''
1. BFS from source, recording parents[node] = set of nodes that lead to node
2. Continue each BFS level fully (do not stop at first endNode reach)
3. DFS backtrack from endNode, following parents map
4. Reverse each path found (built end->start, need start->end)
'''
print(template_1)
print('Template 2: Infer Ordering from Sorted Sequence')
template_2 = '''
1. Compare adjacent pairs, extract first differing element as directed constraint
2. Build adjacency list + in-degree map
3. Check for invalid input (prefix longer than successor)
4. Kahn's BFS topological sort
5. If result length < number of nodes => cycle => return invalid
'''
print(template_2)難しい問題に対する自信をつける
難しい問題は最初は不可能に思えても、適切な考え方を身につければ取り組みやすくなります。重要なポイントは次のとおりです。
- 関心を分離する:それぞれの部分問題を、つなぎ合わせる前に個別に解決します
- 基本要素を把握する:BFS/DFS、トポロジカルソート、Dijkstra、DPテーブルなど、難しい問題ではこれらが予想外の形で組み合わされます
- 例から始める:小さな例を使って問題を手作業で追跡し、背後にある構造を見つけます
- 部分問題を検証する:フェーズ1(グラフの構築)を実装したら、次のフェーズ2に進む前にグラフを出力し、手作業で確認します
# Hard problem confidence-building practice plan
practice_plan = [
('Week 1', 'BFS/DFS fundamentals', ['Number of Islands', 'Clone Graph', 'Word Ladder I']),
('Week 2', 'Topological sort', ['Course Schedule I & II', 'Alien Dictionary (easy)']),
('Week 3', 'All-paths problems', ['All Paths to Target', 'Word Ladder II (hard)']),
('Week 4', 'Hard combos', ['Minimum Window Substring', 'Serialize/Deserialize Tree']),
]
print('4-week hard problem practice plan:')
for week, theme, problems in practice_plan:
print(f'\n{week} — {theme}:')
for p in problems:
print(f' - {p}')
print('\nAfter each problem, write:')
print(' 1. The pattern it belongs to')
print(' 2. The 2-3 key sub-problems')
print(' 3. One insight you would not have had before solving it')理解度チェック
このレッスンで扱った Data Structures & Algorithms — Coding Interview Prep の概念について、理解度を確認しましょう。
レッスンの振り返り
このレッスンでは、Word Ladder IIではBFSを使ってすべての最短経路の直前にある単語を記録したparentsマップを構築し、その後、終点から始点へparentsをたどるDFSバックトラッキングによって、すべての最短経路を列挙すること、Alien Dictionaryでは隣接する単語のペアから有向制約を抽出し、Kahnのトポロジカルソートを適用して文字を並べ、サイクルを検出した場合は空文字列を返すこと、そして難しい問題は複数の部分問題に分解できること。グラフの構築、距離の計算、経路の復元などを、それぞれ慣れ親しんだアルゴリズムで個別に解決することを学びました。これでDSA Interview Prepコースをすべて修了しました。このトラックで学んだすべてのパターンと技法を、次の面接で自信を持って活用してください。
よくある質問
「難問の解説:Word Ladder IIとAlien Dictionary」レッスンは無料ですか?
はい。「難問の解説:Word Ladder IIとAlien Dictionary」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Coding Interview Prepコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Coding Interview Prepコースには全4レッスンが含まれています。
「難問の解説:Word Ladder IIとAlien Dictionary」で何を学びますか?
BFS+バックトラッキングによるword-ladder-IIと、トポロジカルソートによるalien-dictionaryという2つの難問に、完全な説明を交えながら最初から最後まで取り組みます。 ブラウザで直接実行するハンズオンコードでCoding Interview Prepを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
Coding Interview Prepを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのCoding Interview Prepは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。
「難問の解説:Word Ladder IIとAlien Dictionary」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このCoding Interview Prepレッスンでコードを書いて実行できますか?
はい。すべてのCoding Interview Prepレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- パターン認識チートシート
- 時間制限付き模擬面接:EasyとMediumの問題
- エッジケースへの対応と面接での意思疎通
- 難問の解説:Word Ladder IIとAlien Dictionary