Skip to content

Commit 93834e5

Browse files
committed
feat: add word ladder
1 parent 32e2f2c commit 93834e5

File tree

7 files changed

+251
-1
lines changed

7 files changed

+251
-1
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"problem_name": "word_ladder",
3+
"solution_class_name": "Solution",
4+
"problem_number": "127",
5+
"problem_title": "Word Ladder",
6+
"difficulty": "Hard",
7+
"topics": "Hash Table, String, Breadth-First Search",
8+
"tags": ["grind-75"],
9+
"readme_description": "A **transformation sequence** from word `beginWord` to word `endWord` using a dictionary `wordList` is a sequence of words `beginWord -> s1 -> s2 -> ... -> sk` such that:\n\n- Every adjacent pair of words differs by a single letter.\n- Every `si` for `1 <= i <= k` is in `wordList`. Note that `beginWord` does not need to be in `wordList`.\n- `sk == endWord`\n\nGiven two words, `beginWord` and `endWord`, and a dictionary `wordList`, return the **number of words** in the **shortest transformation sequence** from `beginWord` to `endWord`, or `0` if no such sequence exists.",
10+
"readme_examples": [
11+
{
12+
"content": "```\nInput: beginWord = \"hit\", endWord = \"cog\", wordList = [\"hot\",\"dot\",\"dog\",\"lot\",\"log\",\"cog\"]\nOutput: 5\n```\n**Explanation:** One shortest transformation sequence is \"hit\" -> \"hot\" -> \"dot\" -> \"dog\" -> \"cog\", which is 5 words long."
13+
},
14+
{
15+
"content": "```\nInput: beginWord = \"hit\", endWord = \"cog\", wordList = [\"hot\",\"dot\",\"dog\",\"lot\",\"log\"]\nOutput: 0\n```\n**Explanation:** The endWord \"cog\" is not in wordList, therefore there is no valid transformation sequence."
16+
}
17+
],
18+
"readme_constraints": "- 1 <= beginWord.length <= 10\n- endWord.length == beginWord.length\n- 1 <= wordList.length <= 5000\n- wordList[i].length == beginWord.length\n- beginWord, endWord, and wordList[i] consist of lowercase English letters.\n- beginWord != endWord\n- All the words in wordList are unique.",
19+
"readme_additional": "",
20+
"solution_imports": "",
21+
"solution_methods": [
22+
{
23+
"name": "ladder_length",
24+
"parameters": "begin_word: str, end_word: str, word_list: list[str]",
25+
"return_type": "int",
26+
"dummy_return": "0"
27+
}
28+
],
29+
"test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution",
30+
"test_class_name": "WordLadder",
31+
"test_helper_methods": [
32+
{ "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }
33+
],
34+
"test_methods": [
35+
{
36+
"name": "test_ladder_length",
37+
"parametrize": "begin_word, end_word, word_list, expected",
38+
"parametrize_typed": "begin_word: str, end_word: str, word_list: list[str], expected: int",
39+
"test_cases": "[('hit', 'cog', ['hot', 'dot', 'dog', 'lot', 'log', 'cog'], 5), ('hit', 'cog', ['hot', 'dot', 'dog', 'lot', 'log'], 0), ('a', 'c', ['a', 'b', 'c'], 2)]",
40+
"body": "result = self.solution.ladder_length(begin_word, end_word, word_list)\nassert result == expected"
41+
}
42+
],
43+
"playground_imports": "from solution import Solution",
44+
"playground_test_case": "# Example test case\nbegin_word = 'hit'\nend_word = 'cog'\nword_list = ['hot', 'dot', 'dog', 'lot', 'log', 'cog']\nexpected = 5",
45+
"playground_execution": "result = Solution().ladder_length(begin_word, end_word, word_list)\nresult",
46+
"playground_assertion": "assert result == expected"
47+
}

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
PYTHON_VERSION = 3.13
2-
PROBLEM ?= lowest_common_ancestor_of_a_binary_search_tree
2+
PROBLEM ?= word_ladder
33
FORCE ?= 0
44
COMMA := ,
55

leetcode/word_ladder/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Word Ladder
2+
3+
**Difficulty:** Hard
4+
**Topics:** Hash Table, String, Breadth-First Search
5+
**Tags:** grind-75
6+
7+
**LeetCode:** [Problem 127](https://leetcode.com/problems/word-ladder/description/)
8+
9+
## Problem Description
10+
11+
A **transformation sequence** from word `beginWord` to word `endWord` using a dictionary `wordList` is a sequence of words `beginWord -> s1 -> s2 -> ... -> sk` such that:
12+
13+
- Every adjacent pair of words differs by a single letter.
14+
- Every `si` for `1 <= i <= k` is in `wordList`. Note that `beginWord` does not need to be in `wordList`.
15+
- `sk == endWord`
16+
17+
Given two words, `beginWord` and `endWord`, and a dictionary `wordList`, return the **number of words** in the **shortest transformation sequence** from `beginWord` to `endWord`, or `0` if no such sequence exists.
18+
19+
## Examples
20+
21+
### Example 1:
22+
23+
```
24+
Input: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
25+
Output: 5
26+
```
27+
28+
**Explanation:** One shortest transformation sequence is "hit" -> "hot" -> "dot" -> "dog" -> "cog", which is 5 words long.
29+
30+
### Example 2:
31+
32+
```
33+
Input: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"]
34+
Output: 0
35+
```
36+
37+
**Explanation:** The endWord "cog" is not in wordList, therefore there is no valid transformation sequence.
38+
39+
## Constraints
40+
41+
- 1 <= beginWord.length <= 10
42+
- endWord.length == beginWord.length
43+
- 1 <= wordList.length <= 5000
44+
- wordList[i].length == beginWord.length
45+
- beginWord, endWord, and wordList[i] consist of lowercase English letters.
46+
- beginWord != endWord
47+
- All the words in wordList are unique.

leetcode/word_ladder/__init__.py

Whitespace-only changes.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": 1,
6+
"id": "imports",
7+
"metadata": {},
8+
"outputs": [],
9+
"source": [
10+
"from solution import Solution"
11+
]
12+
},
13+
{
14+
"cell_type": "code",
15+
"execution_count": 2,
16+
"id": "setup",
17+
"metadata": {},
18+
"outputs": [],
19+
"source": [
20+
"# Example test case\n",
21+
"begin_word = \"hit\"\n",
22+
"end_word = \"cog\"\n",
23+
"word_list = [\"hot\", \"dot\", \"dog\", \"lot\", \"log\", \"cog\"]\n",
24+
"expected = 5"
25+
]
26+
},
27+
{
28+
"cell_type": "code",
29+
"execution_count": 3,
30+
"id": "execute",
31+
"metadata": {},
32+
"outputs": [
33+
{
34+
"data": {
35+
"text/plain": [
36+
"5"
37+
]
38+
},
39+
"execution_count": 3,
40+
"metadata": {},
41+
"output_type": "execute_result"
42+
}
43+
],
44+
"source": [
45+
"result = Solution().ladder_length(begin_word, end_word, word_list)\n",
46+
"result"
47+
]
48+
},
49+
{
50+
"cell_type": "code",
51+
"execution_count": 4,
52+
"id": "test",
53+
"metadata": {},
54+
"outputs": [],
55+
"source": [
56+
"assert result == expected"
57+
]
58+
}
59+
],
60+
"metadata": {
61+
"kernelspec": {
62+
"display_name": "leetcode-py-py3.13",
63+
"language": "python",
64+
"name": "python3"
65+
},
66+
"language_info": {
67+
"codemirror_mode": {
68+
"name": "ipython",
69+
"version": 3
70+
},
71+
"file_extension": ".py",
72+
"mimetype": "text/x-python",
73+
"name": "python",
74+
"nbconvert_exporter": "python",
75+
"pygments_lexer": "ipython3",
76+
"version": "3.13.7"
77+
}
78+
},
79+
"nbformat": 4,
80+
"nbformat_minor": 5
81+
}

leetcode/word_ladder/solution.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
class Solution:
2+
# Time: O(M^2 * N) where M is length of each word, N is total number of words
3+
# Space: O(M * N) for the visited sets
4+
def ladder_length(self, begin_word: str, end_word: str, word_list: list[str]) -> int:
5+
if end_word not in word_list:
6+
return 0
7+
8+
word_set = set(word_list)
9+
begin_set = {begin_word}
10+
end_set = {end_word}
11+
length = 1
12+
13+
while begin_set and end_set:
14+
if len(begin_set) > len(end_set):
15+
begin_set, end_set = end_set, begin_set
16+
17+
next_set = set()
18+
for word in begin_set:
19+
for i in range(len(word)):
20+
for c in "abcdefghijklmnopqrstuvwxyz":
21+
new_word = word[:i] + c + word[i + 1 :]
22+
23+
if new_word in end_set:
24+
return length + 1
25+
26+
if new_word in word_set:
27+
next_set.add(new_word)
28+
word_set.remove(new_word)
29+
30+
begin_set = next_set
31+
length += 1
32+
33+
return 0

leetcode/word_ladder/tests.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import pytest
2+
3+
from leetcode_py.test_utils import logged_test
4+
5+
from .solution import Solution
6+
7+
8+
class TestWordLadder:
9+
def setup_method(self):
10+
self.solution = Solution()
11+
12+
@pytest.mark.parametrize(
13+
"begin_word, end_word, word_list, expected",
14+
[
15+
# Basic cases
16+
("hit", "cog", ["hot", "dot", "dog", "lot", "log", "cog"], 5),
17+
("hit", "cog", ["hot", "dot", "dog", "lot", "log"], 0),
18+
("a", "c", ["a", "b", "c"], 2),
19+
# Edge cases
20+
("hot", "dog", ["hot", "dog"], 0), # No intermediate words
21+
("hot", "hot", ["hot"], 1), # Same word
22+
("cat", "dog", [], 0), # Empty word list
23+
("cat", "dog", ["cat"], 0), # End word not in list
24+
# Single character changes
25+
("a", "b", ["a", "b"], 2),
26+
("ab", "cd", ["ab", "ad", "cd"], 3),
27+
# Longer paths
28+
("red", "tax", ["ted", "tex", "red", "tax", "tad", "den", "rex", "pee"], 4),
29+
# Multiple possible paths (should return shortest)
30+
("cat", "dog", ["cat", "bat", "bag", "dag", "dog", "cag", "cog"], 4),
31+
# No path exists
32+
("abc", "def", ["abc", "def", "ghi"], 0),
33+
# Direct transformation
34+
("cat", "bat", ["cat", "bat"], 2),
35+
# Longer word length
36+
("word", "form", ["word", "worm", "form", "foam", "flam", "flab"], 3),
37+
],
38+
)
39+
@logged_test
40+
def test_ladder_length(self, begin_word: str, end_word: str, word_list: list[str], expected: int):
41+
result = self.solution.ladder_length(begin_word, end_word, word_list)
42+
assert result == expected

0 commit comments

Comments
 (0)