From 73afc411349b86584dd771586c179f2d09618cef Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Fri, 5 Sep 2025 07:37:19 +0700 Subject: [PATCH 1/7] feat: add trie problem and dict_tree --- .../json/implement_trie_prefix_tree.json | 45 ++++ Makefile | 2 +- leetcode/implement_trie_prefix_tree/README.md | 48 ++++ .../implement_trie_prefix_tree/__init__.py | 0 .../playground.ipynb | 217 ++++++++++++++++++ .../implement_trie_prefix_tree/solution.py | 42 ++++ leetcode/implement_trie_prefix_tree/tests.py | 123 ++++++++++ leetcode_py/data_structures/__init__.py | 6 + leetcode_py/data_structures/dict_tree.py | 70 ++++++ tests/test_dict_tree.py | 66 ++++++ 10 files changed, 618 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/implement_trie_prefix_tree.json create mode 100644 leetcode/implement_trie_prefix_tree/README.md create mode 100644 leetcode/implement_trie_prefix_tree/__init__.py create mode 100644 leetcode/implement_trie_prefix_tree/playground.ipynb create mode 100644 leetcode/implement_trie_prefix_tree/solution.py create mode 100644 leetcode/implement_trie_prefix_tree/tests.py create mode 100644 leetcode_py/data_structures/dict_tree.py create mode 100644 tests/test_dict_tree.py diff --git a/.templates/leetcode/json/implement_trie_prefix_tree.json b/.templates/leetcode/json/implement_trie_prefix_tree.json new file mode 100644 index 0000000..778c471 --- /dev/null +++ b/.templates/leetcode/json/implement_trie_prefix_tree.json @@ -0,0 +1,45 @@ +{ + "problem_name": "implement_trie_prefix_tree", + "solution_class_name": "Trie", + "problem_number": "208", + "problem_title": "Implement Trie (Prefix Tree)", + "difficulty": "Medium", + "topics": "Hash Table, String, Design, Trie", + "tags": ["grind-75"], + "readme_description": "A **trie** (pronounced as \"try\") or **prefix tree** is a tree data structure used to efficiently store and retrieve keys in a dataset of strings. There are various applications of this data structure, such as autocomplete and spellchecker.\n\nImplement the Trie class:\n\n- `Trie()` Initializes the trie object.\n- `void insert(String word)` Inserts the string `word` into the trie.\n- `boolean search(String word)` Returns `true` if the string `word` is in the trie (i.e., was inserted before), and `false` otherwise.\n- `boolean startsWith(String prefix)` Returns `true` if there is a previously inserted string `word` that has the prefix `prefix`, and `false` otherwise.", + "readme_examples": [ + { + "content": "```\nInput\n[\"Trie\", \"insert\", \"search\", \"search\", \"startsWith\", \"insert\", \"search\"]\n[[], [\"apple\"], [\"apple\"], [\"app\"], [\"app\"], [\"app\"], [\"app\"]]\nOutput\n[null, null, true, false, true, null, true]\n```\n\n**Explanation:**\n```python\ntrie = Trie()\ntrie.insert(\"apple\")\ntrie.search(\"apple\") # return True\ntrie.search(\"app\") # return False\ntrie.starts_with(\"app\") # return True\ntrie.insert(\"app\")\ntrie.search(\"app\") # return True\n```" + } + ], + "readme_constraints": "- `1 <= word.length, prefix.length <= 2000`\n- `word` and `prefix` consist only of lowercase English letters.\n- At most `3 * 10^4` calls **in total** will be made to `insert`, `search`, and `starts_with`.", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { "name": "__init__", "parameters": "", "return_type": "None", "dummy_return": "" }, + { "name": "insert", "parameters": "word: str", "return_type": "None", "dummy_return": "" }, + { "name": "search", "parameters": "word: str", "return_type": "bool", "dummy_return": "False" }, + { + "name": "starts_with", + "parameters": "prefix: str", + "return_type": "bool", + "dummy_return": "False" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Trie", + "test_class_name": "ImplementTriePrefixTree", + "test_helper_methods": [], + "test_methods": [ + { + "name": "test_trie_operations", + "parametrize": "operations, inputs, expected", + "parametrize_typed": "operations: list[str], inputs: list[list[str]], expected: list[bool | None]", + "test_cases": "[(['Trie', 'insert', 'search', 'search', 'starts_with', 'insert', 'search'], [[], ['apple'], ['apple'], ['app'], ['app'], ['app'], ['app']], [None, None, True, False, True, None, True]), (['Trie', 'insert', 'insert', 'search', 'search', 'starts_with', 'starts_with'], [[], ['hello'], ['world'], ['hello'], ['hi'], ['hel'], ['wor']], [None, None, None, True, False, True, True]), (['Trie', 'insert', 'insert', 'search', 'search', 'starts_with', 'starts_with'], [[], ['a'], ['aa'], ['a'], ['aa'], ['a'], ['aa']], [None, None, None, True, True, True, True]), (['Trie', 'insert', 'search', 'starts_with', 'insert', 'search', 'starts_with'], [[], ['test'], ['testing'], ['test'], ['testing'], ['testing'], ['test']], [None, None, False, True, None, True, True]), (['Trie', 'search', 'starts_with'], [[], ['empty'], ['empty']], [None, False, False])]", + "body": "trie: Trie | None = None\nresults: list[bool | None] = []\nfor i, op in enumerate(operations):\n if op == 'Trie':\n trie = Trie()\n results.append(None)\n elif op == 'insert' and trie is not None:\n trie.insert(inputs[i][0])\n results.append(None)\n elif op == 'search' and trie is not None:\n results.append(trie.search(inputs[i][0]))\n elif op == 'starts_with' and trie is not None:\n results.append(trie.starts_with(inputs[i][0]))\nassert results == expected" + } + ], + "playground_imports": "from solution import Trie", + "playground_test_case": "# Example test case\noperations = ['Trie', 'insert', 'search', 'search', 'starts_with', 'insert', 'search']\ninputs = [[], ['apple'], ['apple'], ['app'], ['app'], ['app'], ['app']]\nexpected = [None, None, True, False, True, None, True]", + "playground_execution": "trie = None\nresults: list[bool | None] = []\nfor i, op in enumerate(operations):\n if op == 'Trie':\n trie = Trie()\n results.append(None)\n elif op == 'insert' and trie is not None:\n trie.insert(inputs[i][0])\n results.append(None)\n elif op == 'search' and trie is not None:\n results.append(trie.search(inputs[i][0]))\n elif op == 'starts_with' and trie is not None:\n results.append(trie.starts_with(inputs[i][0]))\nresults", + "playground_assertion": "assert results == expected" +} diff --git a/Makefile b/Makefile index a433355..c3e45b2 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= maximum_profit_in_job_scheduling +PROBLEM ?= implement_trie_prefix_tree FORCE ?= 0 COMMA := , diff --git a/leetcode/implement_trie_prefix_tree/README.md b/leetcode/implement_trie_prefix_tree/README.md new file mode 100644 index 0000000..7e0cd3c --- /dev/null +++ b/leetcode/implement_trie_prefix_tree/README.md @@ -0,0 +1,48 @@ +# Implement Trie (Prefix Tree) + +**Difficulty:** Medium +**Topics:** Hash Table, String, Design, Trie +**Tags:** grind-75 + +**LeetCode:** [Problem 208](https://leetcode.com/problems/implement-trie-prefix-tree/description/) + +## Problem Description + +A **trie** (pronounced as "try") or **prefix tree** is a tree data structure used to efficiently store and retrieve keys in a dataset of strings. There are various applications of this data structure, such as autocomplete and spellchecker. + +Implement the Trie class: + +- `Trie()` Initializes the trie object. +- `void insert(String word)` Inserts the string `word` into the trie. +- `boolean search(String word)` Returns `true` if the string `word` is in the trie (i.e., was inserted before), and `false` otherwise. +- `boolean startsWith(String prefix)` Returns `true` if there is a previously inserted string `word` that has the prefix `prefix`, and `false` otherwise. + +## Examples + +### Example 1: + +``` +Input +["Trie", "insert", "search", "search", "startsWith", "insert", "search"] +[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]] +Output +[null, null, true, false, true, null, true] +``` + +**Explanation:** + +```python +trie = Trie() +trie.insert("apple") +trie.search("apple") # return True +trie.search("app") # return False +trie.starts_with("app") # return True +trie.insert("app") +trie.search("app") # return True +``` + +## Constraints + +- `1 <= word.length, prefix.length <= 2000` +- `word` and `prefix` consist only of lowercase English letters. +- At most `3 * 10^4` calls **in total** will be made to `insert`, `search`, and `starts_with`. diff --git a/leetcode/implement_trie_prefix_tree/__init__.py b/leetcode/implement_trie_prefix_tree/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/implement_trie_prefix_tree/playground.ipynb b/leetcode/implement_trie_prefix_tree/playground.ipynb new file mode 100644 index 0000000..4bb1a00 --- /dev/null +++ b/leetcode/implement_trie_prefix_tree/playground.ipynb @@ -0,0 +1,217 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import Trie" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "operations = [\"Trie\", \"insert\", \"search\", \"search\", \"starts_with\", \"insert\", \"search\"]\n", + "inputs = [[], [\"apple\"], [\"apple\"], [\"app\"], [\"app\"], [\"app\"], [\"app\"]]\n", + "expected = [None, None, True, False, True, None, True]" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[None, None, True, False, True, None, True]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "trie = None\n", + "results: list[bool | None] = []\n", + "for i, op in enumerate(operations):\n", + " if op == \"Trie\":\n", + " trie = Trie()\n", + " results.append(None)\n", + " elif op == \"insert\" and trie is not None:\n", + " trie.insert(inputs[i][0])\n", + " results.append(None)\n", + " elif op == \"search\" and trie is not None:\n", + " results.append(trie.search(inputs[i][0]))\n", + " elif op == \"starts_with\" and trie is not None:\n", + " results.append(trie.starts_with(inputs[i][0]))\n", + "results" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8d00661e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "root\n", + "\n", + "root\n", + "\n", + "\n", + "\n", + "root_0\n", + "\n", + "a\n", + "\n", + "\n", + "\n", + "root->root_0\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "root_0_0\n", + "\n", + "p\n", + "\n", + "\n", + "\n", + "root_0->root_0_0\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "root_0_0_0\n", + "\n", + "p\n", + "\n", + "\n", + "\n", + "root_0_0->root_0_0_0\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "root_0_0_0_0\n", + "\n", + "l\n", + "\n", + "\n", + "\n", + "root_0_0_0->root_0_0_0_0\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "root_0_0_0_leaf_1\n", + "\n", + "#: True\n", + "\n", + "\n", + "\n", + "root_0_0_0->root_0_0_0_leaf_1\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "root_0_0_0_0_0\n", + "\n", + "e\n", + "\n", + "\n", + "\n", + "root_0_0_0_0->root_0_0_0_0_0\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "root_0_0_0_0_0_leaf_0\n", + "\n", + "#: True\n", + "\n", + "\n", + "\n", + "root_0_0_0_0_0->root_0_0_0_0_0_leaf_0\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "trie" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "test", + "metadata": {}, + "outputs": [], + "source": [ + "assert results == expected" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "leetcode-py-py3.13", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/implement_trie_prefix_tree/solution.py b/leetcode/implement_trie_prefix_tree/solution.py new file mode 100644 index 0000000..19ecc1e --- /dev/null +++ b/leetcode/implement_trie_prefix_tree/solution.py @@ -0,0 +1,42 @@ +from typing import Any + +from leetcode_py.data_structures import DictTree + + +class Trie(DictTree[str]): + END_OF_WORD = "#" + + # Time: O(1) + # Space: O(1) + def __init__(self) -> None: + self.root: dict[str, Any] = {} + + # Time: O(m) where m is word length + # Space: O(m) + def insert(self, word: str) -> None: + node = self.root + for char in word: + if char not in node: + node[char] = {} + node = node[char] + node[self.END_OF_WORD] = True # End of word marker + + # Time: O(m) where m is word length + # Space: O(1) + def search(self, word: str) -> bool: + node = self.root + for char in word: + if char not in node: + return False + node = node[char] + return self.END_OF_WORD in node + + # Time: O(m) where m is prefix length + # Space: O(1) + def starts_with(self, prefix: str) -> bool: + node = self.root + for char in prefix: + if char not in node: + return False + node = node[char] + return True diff --git a/leetcode/implement_trie_prefix_tree/tests.py b/leetcode/implement_trie_prefix_tree/tests.py new file mode 100644 index 0000000..54375c0 --- /dev/null +++ b/leetcode/implement_trie_prefix_tree/tests.py @@ -0,0 +1,123 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Trie + + +class TestImplementTriePrefixTree: + @pytest.mark.parametrize( + "operations, inputs, expected", + [ + ( + ["Trie", "insert", "search", "search", "starts_with", "insert", "search"], + [[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]], + [None, None, True, False, True, None, True], + ), + ( + ["Trie", "insert", "insert", "search", "search", "starts_with", "starts_with"], + [[], ["hello"], ["world"], ["hello"], ["hi"], ["hel"], ["wor"]], + [None, None, None, True, False, True, True], + ), + ( + ["Trie", "insert", "insert", "search", "search", "starts_with", "starts_with"], + [[], ["a"], ["aa"], ["a"], ["aa"], ["a"], ["aa"]], + [None, None, None, True, True, True, True], + ), + ( + ["Trie", "insert", "search", "starts_with", "insert", "search", "starts_with"], + [[], ["test"], ["testing"], ["test"], ["testing"], ["testing"], ["test"]], + [None, None, False, True, None, True, True], + ), + (["Trie", "search", "starts_with"], [[], ["empty"], ["empty"]], [None, False, False]), + ( + ["Trie", "insert", "insert", "search", "search", "starts_with"], + [[], ["cat"], ["car"], ["cat"], ["card"], ["ca"]], + [None, None, None, True, False, True], + ), + ( + ["Trie", "insert", "search", "insert", "search", "starts_with"], + [[], ["abc"], ["ab"], ["ab"], ["ab"], ["a"]], + [None, None, False, None, True, True], + ), + ( + ["Trie", "insert", "insert", "insert", "search", "starts_with"], + [[], [""], ["a"], ["ab"], [""], [""]], + [None, None, None, None, True, True], + ), + ( + ["Trie", "insert", "search", "starts_with", "insert", "search"], + [[], ["z"], ["z"], ["z"], ["zzz"], ["zzz"]], + [None, None, True, True, None, True], + ), + ( + ["Trie", "insert", "insert", "search", "search", "starts_with", "starts_with"], + [[], ["prefix"], ["prefixsuffix"], ["prefix"], ["prefixsuffix"], ["pre"], ["prefixs"]], + [None, None, None, True, True, True, True], + ), + ( + ["Trie", "insert", "insert", "search", "starts_with"], + [[], ["word"], ["word"], ["word"], ["wor"]], + [None, None, None, True, True], + ), + ( + ["Trie", "insert", "search", "search", "starts_with", "starts_with"], + [[], ["Word"], ["word"], ["Word"], ["W"], ["w"]], + [None, None, False, True, True, False], + ), + ( + ["Trie", "insert", "insert", "search", "search", "search", "starts_with", "starts_with"], + [[], ["a"], ["b"], ["a"], ["b"], ["c"], ["a"], ["b"]], + [None, None, None, True, True, False, True, True], + ), + ( + [ + "Trie", + "insert", + "insert", + "insert", + "insert", + "search", + "search", + "search", + "search", + "search", + "starts_with", + "starts_with", + ], + [ + [], + ["car"], + ["card"], + ["care"], + ["careful"], + ["car"], + ["card"], + ["care"], + ["careful"], + ["ca"], + ["car"], + ["care"], + ], + [None, None, None, None, None, True, True, True, True, False, True, True], + ), + ], + ) + @logged_test + def test_trie_operations( + self, operations: list[str], inputs: list[list[str]], expected: list[bool | None] + ): + trie: Trie | None = None + results: list[bool | None] = [] + for i, op in enumerate(operations): + if op == "Trie": + trie = Trie() + results.append(None) + elif op == "insert" and trie is not None: + trie.insert(inputs[i][0]) + results.append(None) + elif op == "search" and trie is not None: + results.append(trie.search(inputs[i][0])) + elif op == "starts_with" and trie is not None: + results.append(trie.starts_with(inputs[i][0])) + assert results == expected diff --git a/leetcode_py/data_structures/__init__.py b/leetcode_py/data_structures/__init__.py index e69de29..34a8d61 100644 --- a/leetcode_py/data_structures/__init__.py +++ b/leetcode_py/data_structures/__init__.py @@ -0,0 +1,6 @@ +from .dict_tree import DictTree +from .graph_node import GraphNode +from .list_node import ListNode +from .tree_node import TreeNode + +__all__ = ["DictTree", "GraphNode", "ListNode", "TreeNode"] diff --git a/leetcode_py/data_structures/dict_tree.py b/leetcode_py/data_structures/dict_tree.py new file mode 100644 index 0000000..5b8e614 --- /dev/null +++ b/leetcode_py/data_structures/dict_tree.py @@ -0,0 +1,70 @@ +from typing import Any, Generic, Hashable, TypeVar + +K = TypeVar("K", bound=Hashable) + + +class DictTree(Generic[K]): + + def __init__(self) -> None: + self.root: dict[K, Any] = {} + + def __str__(self) -> str: + if not hasattr(self, "root") or not self.root: + return "Empty" + return self._render_dict_tree(self.root) + + def _render_dict_tree(self, node: dict[K, Any], prefix: str = "", depth: int = 0) -> str: + if not node: + return "" + + lines = [] + items = list(node.items()) + + for i, (key, child) in enumerate(items): + is_last = i == len(items) - 1 + current_prefix = "└── " if is_last else "├── " + lines.append(f"{prefix}{current_prefix}{key}") + + if isinstance(child, dict) and child: + next_prefix = prefix + (" " if is_last else "│ ") + child_lines = self._render_dict_tree(child, next_prefix, depth + 1) + if child_lines: + lines.append(child_lines) + + return "\n".join(lines) + + def _repr_html_(self) -> str: + try: + import graphviz + except ImportError: + return f"
{self.__str__()}
" + + if not hasattr(self, "root") or not self.root: + return "
Empty
" + + dot = graphviz.Digraph() + dot.attr(rankdir="TB") + dot.attr("node", shape="box", style="rounded,filled", fillcolor="lightblue") + + self._add_dict_nodes(dot, self.root, "root") + return dot.pipe(format="svg", encoding="utf-8") + + def _add_dict_nodes(self, dot: Any, node: dict[K, Any], node_id: str, char: K | str = "") -> None: + if not node: + return + + label = char if char else "root" + dot.node(node_id, label) + + child_count = 0 + for key, child in node.items(): + if isinstance(child, dict): + child_node_id = f"{node_id}_{child_count}" + dot.edge(node_id, child_node_id) + self._add_dict_nodes(dot, child, child_node_id, key) + child_count += 1 + else: + leaf_id = f"{node_id}_leaf_{child_count}" + dot.node(leaf_id, f"{key}: {child}", fillcolor="lightgreen") + dot.edge(node_id, leaf_id) + child_count += 1 diff --git a/tests/test_dict_tree.py b/tests/test_dict_tree.py new file mode 100644 index 0000000..639f063 --- /dev/null +++ b/tests/test_dict_tree.py @@ -0,0 +1,66 @@ +import pytest + +from leetcode_py.data_structures import DictTree + + +class TestDictTree: + + def setup_method(self): + self.tree: DictTree[str] = DictTree() + + @pytest.mark.parametrize( + "root_dict, expected_contains", + [ + ({}, "Empty"), + ({"a": {"b": {}}}, ["└── a", " └── b"]), + ({"a": {}, "b": {}}, ["├── a", "└── b"]), + ({"a": {"#": True}, "b": {"value": 42}}, ["├── a", "└── b"]), + ({"a": {"x": {"#": True}}, "b": {"y": {}}}, ["a", "b"]), + ({1: {2: {}}, 3: {}}, ["├── 1", "└── 3"]), + ], + ) + def test_str_representation(self, root_dict, expected_contains): + self.tree.root = root_dict + result = str(self.tree) + if isinstance(expected_contains, str): + assert result == expected_contains + else: + for expected in expected_contains: + assert expected in result + + def test_empty_node_rendering(self): + result = self.tree._render_dict_tree({}) + assert result == "" + + def test_html_without_graphviz(self, monkeypatch): + monkeypatch.setattr( + "builtins.__import__", + lambda name, *args: ( + exec("raise ImportError") if name == "graphviz" else __import__(name, *args) + ), + ) + self.tree.root = {"a": {}} + html = self.tree._repr_html_() + assert "
" in html
+        assert "└── a" in html
+
+    def test_html_with_real_graphviz(self):
+        try:
+            self.tree.root = {"a": {"#": True}, "b": 42}
+            html = self.tree._repr_html_()
+            assert "
Date: Fri, 5 Sep 2025 07:46:35 +0700
Subject: [PATCH 2/7] feat: update trie json

---
 .amazonq/rules/problem-creation.md            |  7 ++
 .../json/implement_trie_prefix_tree.json      |  4 +-
 .../playground.ipynb                          | 12 ++--
 leetcode/implement_trie_prefix_tree/tests.py  | 71 -------------------
 4 files changed, 15 insertions(+), 79 deletions(-)

diff --git a/.amazonq/rules/problem-creation.md b/.amazonq/rules/problem-creation.md
index 1cdd641..4eb1fee 100644
--- a/.amazonq/rules/problem-creation.md
+++ b/.amazonq/rules/problem-creation.md
@@ -136,6 +136,13 @@ When creating JSON properties that use PascalCase (solution_class_name, test_cla
 - Complex test setup with operation sequences
 - Import custom class in test_imports
 
+### Dict-based Tree Problems (Trie, etc.)
+
+- Add `"solution_imports": "from leetcode_py.data_structures import DictTree"`
+- Inherit from `DictTree[str]` for string-based trees like Trie
+- Provides automatic visualization capabilities
+- Use `dict[str, Any]` for internal tree structure
+
 ## Generation Commands
 
 ```bash
diff --git a/.templates/leetcode/json/implement_trie_prefix_tree.json b/.templates/leetcode/json/implement_trie_prefix_tree.json
index 778c471..f6cf7eb 100644
--- a/.templates/leetcode/json/implement_trie_prefix_tree.json
+++ b/.templates/leetcode/json/implement_trie_prefix_tree.json
@@ -1,6 +1,6 @@
 {
     "problem_name": "implement_trie_prefix_tree",
-    "solution_class_name": "Trie",
+    "solution_class_name": "Trie(DictTree[str])",
     "problem_number": "208",
     "problem_title": "Implement Trie (Prefix Tree)",
     "difficulty": "Medium",
@@ -14,7 +14,7 @@
     ],
     "readme_constraints": "- `1 <= word.length, prefix.length <= 2000`\n- `word` and `prefix` consist only of lowercase English letters.\n- At most `3 * 10^4` calls **in total** will be made to `insert`, `search`, and `starts_with`.",
     "readme_additional": "",
-    "solution_imports": "",
+    "solution_imports": "from leetcode_py.data_structures import DictTree",
     "solution_methods": [
         { "name": "__init__", "parameters": "", "return_type": "None", "dummy_return": "" },
         { "name": "insert", "parameters": "word: str", "return_type": "None", "dummy_return": "" },
diff --git a/leetcode/implement_trie_prefix_tree/playground.ipynb b/leetcode/implement_trie_prefix_tree/playground.ipynb
index 4bb1a00..627c0cd 100644
--- a/leetcode/implement_trie_prefix_tree/playground.ipynb
+++ b/leetcode/implement_trie_prefix_tree/playground.ipynb
@@ -59,8 +59,8 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
-   "id": "8d00661e",
+   "execution_count": 5,
+   "id": "f3bac41e",
    "metadata": {},
    "outputs": [
     {
@@ -170,10 +170,10 @@
        "\n"
       ],
       "text/plain": [
-       ""
+       ""
       ]
      },
-     "execution_count": 4,
+     "execution_count": 5,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -184,7 +184,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 4,
    "id": "test",
    "metadata": {},
    "outputs": [],
@@ -207,7 +207,7 @@
    "file_extension": ".py",
    "mimetype": "text/x-python",
    "name": "python",
-   "nbconvert_exporter": "python",
+   "nbconvert_exporter": "python3",
    "pygments_lexer": "ipython3",
    "version": "3.13.7"
   }
diff --git a/leetcode/implement_trie_prefix_tree/tests.py b/leetcode/implement_trie_prefix_tree/tests.py
index 54375c0..6b078bb 100644
--- a/leetcode/implement_trie_prefix_tree/tests.py
+++ b/leetcode/implement_trie_prefix_tree/tests.py
@@ -30,77 +30,6 @@ class TestImplementTriePrefixTree:
                 [None, None, False, True, None, True, True],
             ),
             (["Trie", "search", "starts_with"], [[], ["empty"], ["empty"]], [None, False, False]),
-            (
-                ["Trie", "insert", "insert", "search", "search", "starts_with"],
-                [[], ["cat"], ["car"], ["cat"], ["card"], ["ca"]],
-                [None, None, None, True, False, True],
-            ),
-            (
-                ["Trie", "insert", "search", "insert", "search", "starts_with"],
-                [[], ["abc"], ["ab"], ["ab"], ["ab"], ["a"]],
-                [None, None, False, None, True, True],
-            ),
-            (
-                ["Trie", "insert", "insert", "insert", "search", "starts_with"],
-                [[], [""], ["a"], ["ab"], [""], [""]],
-                [None, None, None, None, True, True],
-            ),
-            (
-                ["Trie", "insert", "search", "starts_with", "insert", "search"],
-                [[], ["z"], ["z"], ["z"], ["zzz"], ["zzz"]],
-                [None, None, True, True, None, True],
-            ),
-            (
-                ["Trie", "insert", "insert", "search", "search", "starts_with", "starts_with"],
-                [[], ["prefix"], ["prefixsuffix"], ["prefix"], ["prefixsuffix"], ["pre"], ["prefixs"]],
-                [None, None, None, True, True, True, True],
-            ),
-            (
-                ["Trie", "insert", "insert", "search", "starts_with"],
-                [[], ["word"], ["word"], ["word"], ["wor"]],
-                [None, None, None, True, True],
-            ),
-            (
-                ["Trie", "insert", "search", "search", "starts_with", "starts_with"],
-                [[], ["Word"], ["word"], ["Word"], ["W"], ["w"]],
-                [None, None, False, True, True, False],
-            ),
-            (
-                ["Trie", "insert", "insert", "search", "search", "search", "starts_with", "starts_with"],
-                [[], ["a"], ["b"], ["a"], ["b"], ["c"], ["a"], ["b"]],
-                [None, None, None, True, True, False, True, True],
-            ),
-            (
-                [
-                    "Trie",
-                    "insert",
-                    "insert",
-                    "insert",
-                    "insert",
-                    "search",
-                    "search",
-                    "search",
-                    "search",
-                    "search",
-                    "starts_with",
-                    "starts_with",
-                ],
-                [
-                    [],
-                    ["car"],
-                    ["card"],
-                    ["care"],
-                    ["careful"],
-                    ["car"],
-                    ["card"],
-                    ["care"],
-                    ["careful"],
-                    ["ca"],
-                    ["car"],
-                    ["care"],
-                ],
-                [None, None, None, None, None, True, True, True, True, False, True, True],
-            ),
         ],
     )
     @logged_test

From 304e6ec4f1f17a34b71cdd6b4ccd16567d4eafb6 Mon Sep 17 00:00:00 2001
From: Wisaroot Lertthaweedech 
Date: Fri, 5 Sep 2025 07:59:50 +0700
Subject: [PATCH 3/7] docs: add TODO in cookiecutter template

---
 .templates/leetcode/{{cookiecutter.problem_name}}/solution.py | 4 ++--
 .templates/leetcode/{{cookiecutter.problem_name}}/tests.py    | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/.templates/leetcode/{{cookiecutter.problem_name}}/solution.py b/.templates/leetcode/{{cookiecutter.problem_name}}/solution.py
index d731510..a48e5d1 100644
--- a/.templates/leetcode/{{cookiecutter.problem_name}}/solution.py
+++ b/.templates/leetcode/{{cookiecutter.problem_name}}/solution.py
@@ -4,9 +4,9 @@ class {{cookiecutter.solution_class_name}}:
     {%- for _, methods in cookiecutter._solution_methods | dictsort %}
     {%- for method in methods %}
     # Time: O(?)
-    # Space: O(?)
+    # Space: O(?){# TODO: add decorator // optional self. #}
     def {{method.name}}(self, {{method.parameters}}) -> {{method.return_type}}:
-        # TODO: Implement {{method.name}}
+        # TODO: Implement {{method.name}}{# TODO: add body #}
         return {{method.dummy_return}}
 
     {%- endfor %}
diff --git a/.templates/leetcode/{{cookiecutter.problem_name}}/tests.py b/.templates/leetcode/{{cookiecutter.problem_name}}/tests.py
index 5d4ebc1..014cc33 100644
--- a/.templates/leetcode/{{cookiecutter.problem_name}}/tests.py
+++ b/.templates/leetcode/{{cookiecutter.problem_name}}/tests.py
@@ -3,7 +3,7 @@
 
 class Test{{cookiecutter.test_class_name}}:
     {%- for _, helper_methods in cookiecutter._test_helper_methods | dictsort %}
-    {%- for method in helper_methods %}
+    {%- for method in helper_methods %}{# TODO: add decorator // optional self. #}
     def {{method.name}}(self{% if method.parameters %}, {{method.parameters}}{% endif %}):
         {{method.body | indent(8, first=False)}}
 

From 8c4d078ae53191b22124c838db9e963c2fe824ae Mon Sep 17 00:00:00 2001
From: Wisaroot Lertthaweedech 
Date: Fri, 5 Sep 2025 09:19:36 +0700
Subject: [PATCH 4/7] feat: add atoi problem

---
 .../leetcode/json/string_to_integer_atoi.json |  51 ++++++++
 Makefile                                      |   2 +-
 leetcode/string_to_integer_atoi/README.md     | 109 ++++++++++++++++++
 leetcode/string_to_integer_atoi/__init__.py   |   0
 .../string_to_integer_atoi/playground.ipynb   |  79 +++++++++++++
 leetcode/string_to_integer_atoi/solution.py   |  30 +++++
 leetcode/string_to_integer_atoi/tests.py      |  49 ++++++++
 7 files changed, 319 insertions(+), 1 deletion(-)
 create mode 100644 .templates/leetcode/json/string_to_integer_atoi.json
 create mode 100644 leetcode/string_to_integer_atoi/README.md
 create mode 100644 leetcode/string_to_integer_atoi/__init__.py
 create mode 100644 leetcode/string_to_integer_atoi/playground.ipynb
 create mode 100644 leetcode/string_to_integer_atoi/solution.py
 create mode 100644 leetcode/string_to_integer_atoi/tests.py

diff --git a/.templates/leetcode/json/string_to_integer_atoi.json b/.templates/leetcode/json/string_to_integer_atoi.json
new file mode 100644
index 0000000..5e54670
--- /dev/null
+++ b/.templates/leetcode/json/string_to_integer_atoi.json
@@ -0,0 +1,51 @@
+{
+    "problem_name": "string_to_integer_atoi",
+    "solution_class_name": "Solution",
+    "problem_number": "8",
+    "problem_title": "String to Integer (atoi)",
+    "difficulty": "Medium",
+    "topics": "String",
+    "tags": ["grind-75"],
+    "readme_description": "Implement the `my_atoi(string s)` function, which converts a string to a 32-bit signed integer.\n\nThe algorithm for `my_atoi(string s)` is as follows:\n\n1. **Whitespace**: Ignore any leading whitespace (` `).\n2. **Signedness**: Determine the sign by checking if the next character is `-` or `+`, assuming positivity if neither present.\n3. **Conversion**: Read the integer by skipping leading zeros until a non-digit character is encountered or the end of the string is reached. If no digits were read, then the result is 0.\n4. **Rounding**: If the integer is out of the 32-bit signed integer range `[-2^31, 2^31 - 1]`, then round the integer to remain in the range. Specifically, integers less than `-2^31` should be rounded to `-2^31`, and integers greater than `2^31 - 1` should be rounded to `2^31 - 1`.\n\nReturn the integer as the final result.",
+    "readme_examples": [
+        {
+            "content": "```\nInput: s = \"42\"\nOutput: 42\n```\n**Explanation:**\n```\nThe underlined characters are what is read in and the caret is the current reader position.\nStep 1: \"42\" (no characters read because there is no leading whitespace)\n         ^\nStep 2: \"42\" (no characters read because there is neither a '-' nor '+')\n         ^\nStep 3: \"42\" (\"42\" is read in)\n           ^\n```"
+        },
+        {
+            "content": "```\nInput: s = \"   -042\"\nOutput: -42\n```\n**Explanation:**\n```\nStep 1: \"   -042\" (leading whitespace is read and ignored)\n            ^\nStep 2: \"   -042\" ('-' is read, so the result should be negative)\n             ^\nStep 3: \"   -042\" (\"042\" is read in, leading zeros ignored in the result)\n               ^\n```"
+        },
+        {
+            "content": "```\nInput: s = \"1337c0d3\"\nOutput: 1337\n```\n**Explanation:**\n```\nStep 1: \"1337c0d3\" (no characters read because there is no leading whitespace)\n         ^\nStep 2: \"1337c0d3\" (no characters read because there is neither a '-' nor '+')\n         ^\nStep 3: \"1337c0d3\" (\"1337\" is read in; reading stops because the next character is a non-digit)\n             ^\n```"
+        },
+        {
+            "content": "```\nInput: s = \"0-1\"\nOutput: 0\n```\n**Explanation:**\n```\nStep 1: \"0-1\" (no characters read because there is no leading whitespace)\n         ^\nStep 2: \"0-1\" (no characters read because there is neither a '-' nor '+')\n         ^\nStep 3: \"0-1\" (\"0\" is read in; reading stops because the next character is a non-digit)\n          ^\n```"
+        },
+        {
+            "content": "```\nInput: s = \"words and 987\"\nOutput: 0\n```\n**Explanation:** Reading stops at the first non-digit character 'w'."
+        }
+    ],
+    "readme_constraints": "- `0 <= s.length <= 200`\n- `s` consists of English letters (lower-case and upper-case), digits (0-9), ` `, `+`, `-`, and `.`.",
+    "readme_additional": "",
+    "solution_imports": "",
+    "solution_methods": [
+        { "name": "my_atoi", "parameters": "s: str", "return_type": "int", "dummy_return": "0" }
+    ],
+    "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution",
+    "test_class_name": "StringToIntegerAtoi",
+    "test_helper_methods": [
+        { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }
+    ],
+    "test_methods": [
+        {
+            "name": "test_my_atoi",
+            "parametrize": "s, expected",
+            "parametrize_typed": "s: str, expected: int",
+            "test_cases": "[('42', 42), ('   -042', -42), ('1337c0d3', 1337), ('0-1', 0), ('words and 987', 0), ('', 0), ('   ', 0), ('+1', 1), ('-1', -1), ('2147483647', 2147483647), ('-2147483648', -2147483648), ('2147483648', 2147483647), ('-2147483649', -2147483648)]",
+            "body": "result = self.solution.my_atoi(s)\nassert result == expected"
+        }
+    ],
+    "playground_imports": "from solution import Solution",
+    "playground_test_case": "# Example test case\ns = '42'\nexpected = 42",
+    "playground_execution": "result = Solution().my_atoi(s)\nresult",
+    "playground_assertion": "assert result == expected"
+}
diff --git a/Makefile b/Makefile
index c3e45b2..20cc257 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
 PYTHON_VERSION = 3.13
-PROBLEM ?= implement_trie_prefix_tree
+PROBLEM ?= string_to_integer_atoi
 FORCE ?= 0
 COMMA := ,
 
diff --git a/leetcode/string_to_integer_atoi/README.md b/leetcode/string_to_integer_atoi/README.md
new file mode 100644
index 0000000..513a46d
--- /dev/null
+++ b/leetcode/string_to_integer_atoi/README.md
@@ -0,0 +1,109 @@
+# String to Integer (atoi)
+
+**Difficulty:** Medium
+**Topics:** String
+**Tags:** grind-75
+
+**LeetCode:** [Problem 8](https://leetcode.com/problems/string-to-integer-atoi/description/)
+
+## Problem Description
+
+Implement the `my_atoi(string s)` function, which converts a string to a 32-bit signed integer.
+
+The algorithm for `my_atoi(string s)` is as follows:
+
+1. **Whitespace**: Ignore any leading whitespace (` `).
+2. **Signedness**: Determine the sign by checking if the next character is `-` or `+`, assuming positivity if neither present.
+3. **Conversion**: Read the integer by skipping leading zeros until a non-digit character is encountered or the end of the string is reached. If no digits were read, then the result is 0.
+4. **Rounding**: If the integer is out of the 32-bit signed integer range `[-2^31, 2^31 - 1]`, then round the integer to remain in the range. Specifically, integers less than `-2^31` should be rounded to `-2^31`, and integers greater than `2^31 - 1` should be rounded to `2^31 - 1`.
+
+Return the integer as the final result.
+
+## Examples
+
+### Example 1:
+
+```
+Input: s = "42"
+Output: 42
+```
+
+**Explanation:**
+
+```
+The underlined characters are what is read in and the caret is the current reader position.
+Step 1: "42" (no characters read because there is no leading whitespace)
+         ^
+Step 2: "42" (no characters read because there is neither a '-' nor '+')
+         ^
+Step 3: "42" ("42" is read in)
+           ^
+```
+
+### Example 2:
+
+```
+Input: s = "   -042"
+Output: -42
+```
+
+**Explanation:**
+
+```
+Step 1: "   -042" (leading whitespace is read and ignored)
+            ^
+Step 2: "   -042" ('-' is read, so the result should be negative)
+             ^
+Step 3: "   -042" ("042" is read in, leading zeros ignored in the result)
+               ^
+```
+
+### Example 3:
+
+```
+Input: s = "1337c0d3"
+Output: 1337
+```
+
+**Explanation:**
+
+```
+Step 1: "1337c0d3" (no characters read because there is no leading whitespace)
+         ^
+Step 2: "1337c0d3" (no characters read because there is neither a '-' nor '+')
+         ^
+Step 3: "1337c0d3" ("1337" is read in; reading stops because the next character is a non-digit)
+             ^
+```
+
+### Example 4:
+
+```
+Input: s = "0-1"
+Output: 0
+```
+
+**Explanation:**
+
+```
+Step 1: "0-1" (no characters read because there is no leading whitespace)
+         ^
+Step 2: "0-1" (no characters read because there is neither a '-' nor '+')
+         ^
+Step 3: "0-1" ("0" is read in; reading stops because the next character is a non-digit)
+          ^
+```
+
+### Example 5:
+
+```
+Input: s = "words and 987"
+Output: 0
+```
+
+**Explanation:** Reading stops at the first non-digit character 'w'.
+
+## Constraints
+
+- `0 <= s.length <= 200`
+- `s` consists of English letters (lower-case and upper-case), digits (0-9), ` `, `+`, `-`, and `.`.
diff --git a/leetcode/string_to_integer_atoi/__init__.py b/leetcode/string_to_integer_atoi/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/leetcode/string_to_integer_atoi/playground.ipynb b/leetcode/string_to_integer_atoi/playground.ipynb
new file mode 100644
index 0000000..012d088
--- /dev/null
+++ b/leetcode/string_to_integer_atoi/playground.ipynb
@@ -0,0 +1,79 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "imports",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from solution import Solution"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "setup",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Example test case\n",
+    "s = \"42\"\n",
+    "expected = 42"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "execute",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "42"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "result = Solution().my_atoi(s)\n",
+    "result"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "test",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "assert result == expected"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "leetcode-py-py3.13",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.13.7"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/leetcode/string_to_integer_atoi/solution.py b/leetcode/string_to_integer_atoi/solution.py
new file mode 100644
index 0000000..42c5c62
--- /dev/null
+++ b/leetcode/string_to_integer_atoi/solution.py
@@ -0,0 +1,30 @@
+class Solution:
+    # Time: O(n)
+    # Space: O(1)
+    def my_atoi(self, s: str) -> int:
+        i = 0
+        n = len(s)
+
+        # Skip whitespace
+        while i < n and s[i] == " ":
+            i += 1
+
+        if i == n:
+            return 0
+
+        # Check sign
+        sign = 1
+        if s[i] in {"+", "-"}:
+            sign = -1 if s[i] == "-" else 1
+            i += 1
+
+        # Convert digits
+        result = 0
+        while i < n and s[i].isdigit():
+            result = result * 10 + int(s[i])
+            i += 1
+
+        result *= sign
+
+        # Clamp to 32-bit range
+        return max(-(2**31), min(2**31 - 1, result))
diff --git a/leetcode/string_to_integer_atoi/tests.py b/leetcode/string_to_integer_atoi/tests.py
new file mode 100644
index 0000000..b3c2a54
--- /dev/null
+++ b/leetcode/string_to_integer_atoi/tests.py
@@ -0,0 +1,49 @@
+import pytest
+
+from leetcode_py.test_utils import logged_test
+
+from .solution import Solution
+
+
+class TestStringToIntegerAtoi:
+    def setup_method(self):
+        self.solution = Solution()
+
+    @pytest.mark.parametrize(
+        "s, expected",
+        [
+            ("42", 42),
+            ("   -042", -42),
+            ("1337c0d3", 1337),
+            ("0-1", 0),
+            ("words and 987", 0),
+            ("", 0),
+            ("   ", 0),
+            ("+1", 1),
+            ("-1", -1),
+            ("2147483647", 2147483647),
+            ("-2147483648", -2147483648),
+            ("2147483648", 2147483647),
+            ("-2147483649", -2147483648),
+            # Edge cases
+            ("+-12", 0),
+            ("-+12", 0),
+            ("++1", 0),
+            ("--1", 0),
+            ("   +0 123", 0),
+            ("0000000000012345678", 12345678),
+            ("-000000000000001", -1),
+            ("   +000", 0),
+            ("123-", 123),
+            ("   13  5", 13),
+            (".1", 0),
+            ("1a33", 1),
+            ("  -0012a42", -12),
+            ("21474836460", 2147483647),
+            ("-21474836480", -2147483648),
+        ],
+    )
+    @logged_test
+    def test_my_atoi(self, s: str, expected: int):
+        result = self.solution.my_atoi(s)
+        assert result == expected

From 9846d9686ecc1ae08c6b3225116a2da5bb769743 Mon Sep 17 00:00:00 2001
From: Wisaroot Lertthaweedech 
Date: Fri, 5 Sep 2025 10:08:25 +0700
Subject: [PATCH 5/7] feat: add max depth binary tree

---
 .../json/maximum_depth_of_binary_tree.json    |  45 +++++
 Makefile                                      |   2 +-
 .../maximum_depth_of_binary_tree/README.md    |  36 ++++
 .../maximum_depth_of_binary_tree/__init__.py  |   0
 .../playground.ipynb                          | 171 ++++++++++++++++++
 .../maximum_depth_of_binary_tree/solution.py  |  14 ++
 .../maximum_depth_of_binary_tree/tests.py     |  39 ++++
 7 files changed, 306 insertions(+), 1 deletion(-)
 create mode 100644 .templates/leetcode/json/maximum_depth_of_binary_tree.json
 create mode 100644 leetcode/maximum_depth_of_binary_tree/README.md
 create mode 100644 leetcode/maximum_depth_of_binary_tree/__init__.py
 create mode 100644 leetcode/maximum_depth_of_binary_tree/playground.ipynb
 create mode 100644 leetcode/maximum_depth_of_binary_tree/solution.py
 create mode 100644 leetcode/maximum_depth_of_binary_tree/tests.py

diff --git a/.templates/leetcode/json/maximum_depth_of_binary_tree.json b/.templates/leetcode/json/maximum_depth_of_binary_tree.json
new file mode 100644
index 0000000..d701c4f
--- /dev/null
+++ b/.templates/leetcode/json/maximum_depth_of_binary_tree.json
@@ -0,0 +1,45 @@
+{
+    "problem_name": "maximum_depth_of_binary_tree",
+    "solution_class_name": "Solution",
+    "problem_number": "104",
+    "problem_title": "Maximum Depth of Binary Tree",
+    "difficulty": "Easy",
+    "topics": "Tree, Depth-First Search, Breadth-First Search, Binary Tree",
+    "tags": ["grind-75"],
+    "readme_description": "Given the `root` of a binary tree, return *its maximum depth*.\n\nA binary tree's **maximum depth** is the number of nodes along the longest path from the root node down to the farthest leaf node.",
+    "readme_examples": [
+        {
+            "content": "![Example 1](https://assets.leetcode.com/uploads/2020/11/26/tmp-tree.jpg)\n\n```\nInput: root = [3,9,20,null,null,15,7]\nOutput: 3\n```"
+        },
+        { "content": "```\nInput: root = [1,null,2]\nOutput: 2\n```" }
+    ],
+    "readme_constraints": "- The number of nodes in the tree is in the range `[0, 10^4]`.\n- `-100 <= Node.val <= 100`",
+    "readme_additional": "",
+    "solution_imports": "from leetcode_py import TreeNode",
+    "solution_methods": [
+        {
+            "name": "max_depth",
+            "parameters": "root: TreeNode[int] | None",
+            "return_type": "int",
+            "dummy_return": "0"
+        }
+    ],
+    "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom leetcode_py import TreeNode\nfrom .solution import Solution",
+    "test_class_name": "MaximumDepthOfBinaryTree",
+    "test_helper_methods": [
+        { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }
+    ],
+    "test_methods": [
+        {
+            "name": "test_max_depth",
+            "parametrize": "root_list, expected",
+            "parametrize_typed": "root_list: list[int | None], expected: int",
+            "test_cases": "[([3, 9, 20, None, None, 15, 7], 3), ([1, None, 2], 2), ([], 0)]",
+            "body": "root = TreeNode.from_list(root_list)\nresult = self.solution.max_depth(root)\nassert result == expected"
+        }
+    ],
+    "playground_imports": "from solution import Solution\nfrom leetcode_py import TreeNode",
+    "playground_test_case": "# Example test case\nroot_list = [3, 9, 20, None, None, 15, 7]\nexpected = 3",
+    "playground_execution": "root = TreeNode.from_list(root_list)\nresult = Solution().max_depth(root)\nresult",
+    "playground_assertion": "assert result == expected"
+}
diff --git a/Makefile b/Makefile
index 20cc257..f0b379e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
 PYTHON_VERSION = 3.13
-PROBLEM ?= string_to_integer_atoi
+PROBLEM ?= maximum_depth_of_binary_tree
 FORCE ?= 0
 COMMA := ,
 
diff --git a/leetcode/maximum_depth_of_binary_tree/README.md b/leetcode/maximum_depth_of_binary_tree/README.md
new file mode 100644
index 0000000..4663e24
--- /dev/null
+++ b/leetcode/maximum_depth_of_binary_tree/README.md
@@ -0,0 +1,36 @@
+# Maximum Depth of Binary Tree
+
+**Difficulty:** Easy
+**Topics:** Tree, Depth-First Search, Breadth-First Search, Binary Tree
+**Tags:** grind-75
+
+**LeetCode:** [Problem 104](https://leetcode.com/problems/maximum-depth-of-binary-tree/description/)
+
+## Problem Description
+
+Given the `root` of a binary tree, return _its maximum depth_.
+
+A binary tree's **maximum depth** is the number of nodes along the longest path from the root node down to the farthest leaf node.
+
+## Examples
+
+### Example 1:
+
+![Example 1](https://assets.leetcode.com/uploads/2020/11/26/tmp-tree.jpg)
+
+```
+Input: root = [3,9,20,null,null,15,7]
+Output: 3
+```
+
+### Example 2:
+
+```
+Input: root = [1,null,2]
+Output: 2
+```
+
+## Constraints
+
+- The number of nodes in the tree is in the range `[0, 10^4]`.
+- `-100 <= Node.val <= 100`
diff --git a/leetcode/maximum_depth_of_binary_tree/__init__.py b/leetcode/maximum_depth_of_binary_tree/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/leetcode/maximum_depth_of_binary_tree/playground.ipynb b/leetcode/maximum_depth_of_binary_tree/playground.ipynb
new file mode 100644
index 0000000..19d1eed
--- /dev/null
+++ b/leetcode/maximum_depth_of_binary_tree/playground.ipynb
@@ -0,0 +1,171 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "imports",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from solution import Solution\n",
+    "\n",
+    "from leetcode_py import TreeNode"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "setup",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Example test case\n",
+    "root_list = [3, 9, 20, None, None, 15, 7]\n",
+    "expected = 3"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "execute",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "3"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "root = TreeNode.from_list(root_list)\n",
+    "result = Solution().max_depth(root)\n",
+    "result"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "3d476584",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "\n",
+       "\n",
+       "\n",
+       "\n",
+       "\n",
+       "\n",
+       "\n",
+       "\n",
+       "\n",
+       "0\n",
+       "\n",
+       "3\n",
+       "\n",
+       "\n",
+       "\n",
+       "1\n",
+       "\n",
+       "9\n",
+       "\n",
+       "\n",
+       "\n",
+       "0->1\n",
+       "\n",
+       "\n",
+       "\n",
+       "\n",
+       "\n",
+       "2\n",
+       "\n",
+       "20\n",
+       "\n",
+       "\n",
+       "\n",
+       "0->2\n",
+       "\n",
+       "\n",
+       "\n",
+       "\n",
+       "\n",
+       "3\n",
+       "\n",
+       "15\n",
+       "\n",
+       "\n",
+       "\n",
+       "2->3\n",
+       "\n",
+       "\n",
+       "\n",
+       "\n",
+       "\n",
+       "4\n",
+       "\n",
+       "7\n",
+       "\n",
+       "\n",
+       "\n",
+       "2->4\n",
+       "\n",
+       "\n",
+       "\n",
+       "\n",
+       "\n"
+      ],
+      "text/plain": [
+       "TreeNode([3, 9, 20, None, None, 15, 7])"
+      ]
+     },
+     "execution_count": 5,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "root"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "test",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "assert result == expected"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "leetcode-py-py3.13",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.13.7"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/leetcode/maximum_depth_of_binary_tree/solution.py b/leetcode/maximum_depth_of_binary_tree/solution.py
new file mode 100644
index 0000000..727e907
--- /dev/null
+++ b/leetcode/maximum_depth_of_binary_tree/solution.py
@@ -0,0 +1,14 @@
+from leetcode_py import TreeNode
+
+
+class Solution:
+    # Time: O(n)
+    # Space: O(h)
+    def max_depth(self, root: TreeNode | None) -> int:
+        if not root:
+            return 0
+
+        left_depth = self.max_depth(root.left)
+        right_depth = self.max_depth(root.right)
+
+        return 1 + max(left_depth, right_depth)
diff --git a/leetcode/maximum_depth_of_binary_tree/tests.py b/leetcode/maximum_depth_of_binary_tree/tests.py
new file mode 100644
index 0000000..3bc3acb
--- /dev/null
+++ b/leetcode/maximum_depth_of_binary_tree/tests.py
@@ -0,0 +1,39 @@
+import pytest
+
+from leetcode_py import TreeNode
+from leetcode_py.test_utils import logged_test
+
+from .solution import Solution
+
+
+class TestMaximumDepthOfBinaryTree:
+    def setup_method(self):
+        self.solution = Solution()
+
+    @pytest.mark.parametrize(
+        "root_list, expected",
+        [
+            # Original examples
+            ([3, 9, 20, None, None, 15, 7], 3),
+            ([1, None, 2], 2),
+            ([], 0),
+            # Single node
+            ([1], 1),
+            # Left skewed tree (depth 3)
+            ([1, 2, None, 3], 3),
+            # Right skewed tree (depth 2)
+            ([1, None, 2], 2),
+            # Balanced tree
+            ([1, 2, 3, 4, 5, 6, 7], 3),
+            # Unbalanced tree (left heavy)
+            ([1, 2, 3, 4, 5, None, None, 6, 7], 4),
+            # Two nodes
+            ([1, 2], 2),
+            ([1, None, 2], 2),
+        ],
+    )
+    @logged_test
+    def test_max_depth(self, root_list: list[int | None], expected: int):
+        root = TreeNode.from_list(root_list)
+        result = self.solution.max_depth(root)
+        assert result == expected

From bdd30516488de25de42e03b922a6e3440e0d4756 Mon Sep 17 00:00:00 2001
From: Wisaroot Lertthaweedech 
Date: Fri, 5 Sep 2025 10:24:22 +0700
Subject: [PATCH 6/7] feat: add Add Binary problem

---
 .templates/leetcode/json/add_binary.json | 43 +++++++++++++
 Makefile                                 |  2 +-
 leetcode/add_binary/README.md            | 33 ++++++++++
 leetcode/add_binary/__init__.py          |  0
 leetcode/add_binary/playground.ipynb     | 80 ++++++++++++++++++++++++
 leetcode/add_binary/solution.py          | 26 ++++++++
 leetcode/add_binary/tests.py             | 25 ++++++++
 7 files changed, 208 insertions(+), 1 deletion(-)
 create mode 100644 .templates/leetcode/json/add_binary.json
 create mode 100644 leetcode/add_binary/README.md
 create mode 100644 leetcode/add_binary/__init__.py
 create mode 100644 leetcode/add_binary/playground.ipynb
 create mode 100644 leetcode/add_binary/solution.py
 create mode 100644 leetcode/add_binary/tests.py

diff --git a/.templates/leetcode/json/add_binary.json b/.templates/leetcode/json/add_binary.json
new file mode 100644
index 0000000..e89d7f9
--- /dev/null
+++ b/.templates/leetcode/json/add_binary.json
@@ -0,0 +1,43 @@
+{
+    "problem_name": "add_binary",
+    "solution_class_name": "Solution",
+    "problem_number": "67",
+    "problem_title": "Add Binary",
+    "difficulty": "Easy",
+    "topics": "Math, String, Bit Manipulation, Simulation",
+    "tags": ["grind-75"],
+    "readme_description": "Given two binary strings `a` and `b`, return *their sum as a binary string*.",
+    "readme_examples": [
+        { "content": "```\nInput: a = \"11\", b = \"1\"\nOutput: \"100\"\n```" },
+        { "content": "```\nInput: a = \"1010\", b = \"1011\"\nOutput: \"10101\"\n```" }
+    ],
+    "readme_constraints": "- `1 <= a.length, b.length <= 10^4`\n- `a` and `b` consist only of `'0'` or `'1'` characters.\n- Each string does not contain leading zeros except for the zero itself.",
+    "readme_additional": "",
+    "solution_imports": "",
+    "solution_methods": [
+        {
+            "name": "add_binary",
+            "parameters": "a: str, b: str",
+            "return_type": "str",
+            "dummy_return": "\"\""
+        }
+    ],
+    "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution",
+    "test_class_name": "AddBinary",
+    "test_helper_methods": [
+        { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }
+    ],
+    "test_methods": [
+        {
+            "name": "test_add_binary",
+            "parametrize": "a, b, expected",
+            "parametrize_typed": "a: str, b: str, expected: str",
+            "test_cases": "[('11', '1', '100'), ('1010', '1011', '10101'), ('0', '0', '0'), ('1', '1', '10'), ('1111', '1111', '11110')]",
+            "body": "result = self.solution.add_binary(a, b)\nassert result == expected"
+        }
+    ],
+    "playground_imports": "from solution import Solution",
+    "playground_test_case": "# Example test case\na = '11'\nb = '1'\nexpected = '100'",
+    "playground_execution": "result = Solution().add_binary(a, b)\nresult",
+    "playground_assertion": "assert result == expected"
+}
diff --git a/Makefile b/Makefile
index f0b379e..20fc260 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
 PYTHON_VERSION = 3.13
-PROBLEM ?= maximum_depth_of_binary_tree
+PROBLEM ?= add_binary
 FORCE ?= 0
 COMMA := ,
 
diff --git a/leetcode/add_binary/README.md b/leetcode/add_binary/README.md
new file mode 100644
index 0000000..92224a0
--- /dev/null
+++ b/leetcode/add_binary/README.md
@@ -0,0 +1,33 @@
+# Add Binary
+
+**Difficulty:** Easy
+**Topics:** Math, String, Bit Manipulation, Simulation
+**Tags:** grind-75
+
+**LeetCode:** [Problem 67](https://leetcode.com/problems/add-binary/description/)
+
+## Problem Description
+
+Given two binary strings `a` and `b`, return _their sum as a binary string_.
+
+## Examples
+
+### Example 1:
+
+```
+Input: a = "11", b = "1"
+Output: "100"
+```
+
+### Example 2:
+
+```
+Input: a = "1010", b = "1011"
+Output: "10101"
+```
+
+## Constraints
+
+- `1 <= a.length, b.length <= 10^4`
+- `a` and `b` consist only of `'0'` or `'1'` characters.
+- Each string does not contain leading zeros except for the zero itself.
diff --git a/leetcode/add_binary/__init__.py b/leetcode/add_binary/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/leetcode/add_binary/playground.ipynb b/leetcode/add_binary/playground.ipynb
new file mode 100644
index 0000000..69c0445
--- /dev/null
+++ b/leetcode/add_binary/playground.ipynb
@@ -0,0 +1,80 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "imports",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from solution import Solution"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "setup",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Example test case\n",
+    "a = \"11\"\n",
+    "b = \"1\"\n",
+    "expected = \"100\""
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "execute",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'100'"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "result = Solution().add_binary(a, b)\n",
+    "result"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "test",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "assert result == expected"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "leetcode-py-py3.13",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.13.7"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/leetcode/add_binary/solution.py b/leetcode/add_binary/solution.py
new file mode 100644
index 0000000..36ae857
--- /dev/null
+++ b/leetcode/add_binary/solution.py
@@ -0,0 +1,26 @@
+class Solution:
+    # Time: O(len(a) + len(b))
+    # Space: O(len(a) + len(b))
+    def add_binary(self, a: str, b: str) -> str:
+        # int(a, 2) converts binary string to decimal: int("11", 2) → 3
+        # bin() converts decimal to binary string with prefix: bin(3) → '0b11'
+        # [2:] removes the '0b' prefix to get just binary digits
+        return bin(int(a, 2) + int(b, 2))[2:]
+
+
+# Python Base Conversion:
+#
+# String → Integer (using int() with base parameter):
+# - int("1010", 2) → 10 (binary to decimal)
+# - int("ff", 16) → 255 (hex to decimal)
+# - int("10", 8) → 8 (octal to decimal)
+#
+# Integer → String (conversion functions add prefixes):
+# - bin(10) → '0b1010' (binary with '0b' prefix)
+# - hex(255) → '0xff' (hex with '0x' prefix)
+# - oct(8) → '0o10' (octal with '0o' prefix)
+#
+# These prefixes match Python literal syntax:
+# - 0b1010 = 10, 0xff = 255, 0o10 = 8
+#
+# For string problems, slice off the prefix: bin(n)[2:] gives just the digits.
diff --git a/leetcode/add_binary/tests.py b/leetcode/add_binary/tests.py
new file mode 100644
index 0000000..d8c772a
--- /dev/null
+++ b/leetcode/add_binary/tests.py
@@ -0,0 +1,25 @@
+import pytest
+
+from leetcode_py.test_utils import logged_test
+
+from .solution import Solution
+
+
+class TestAddBinary:
+    def setup_method(self):
+        self.solution = Solution()
+
+    @pytest.mark.parametrize(
+        "a, b, expected",
+        [
+            ("11", "1", "100"),
+            ("1010", "1011", "10101"),
+            ("0", "0", "0"),
+            ("1", "1", "10"),
+            ("1111", "1111", "11110"),
+        ],
+    )
+    @logged_test
+    def test_add_binary(self, a: str, b: str, expected: str):
+        result = self.solution.add_binary(a, b)
+        assert result == expected

From f2b91b1fe044ee8506eca5b50ad484d9b5d7175c Mon Sep 17 00:00:00 2001
From: Wisaroot Lertthaweedech 
Date: Fri, 5 Sep 2025 15:11:22 +0700
Subject: [PATCH 7/7] feat: add Product of Array Except Self

---
 .../json/product_of_array_except_self.json    | 43 ++++++++++
 Makefile                                      |  2 +-
 .../product_of_array_except_self/README.md    | 39 +++++++++
 .../product_of_array_except_self/__init__.py  |  0
 .../playground.ipynb                          | 79 +++++++++++++++++++
 .../product_of_array_except_self/solution.py  | 25 ++++++
 .../product_of_array_except_self/tests.py     | 38 +++++++++
 7 files changed, 225 insertions(+), 1 deletion(-)
 create mode 100644 .templates/leetcode/json/product_of_array_except_self.json
 create mode 100644 leetcode/product_of_array_except_self/README.md
 create mode 100644 leetcode/product_of_array_except_self/__init__.py
 create mode 100644 leetcode/product_of_array_except_self/playground.ipynb
 create mode 100644 leetcode/product_of_array_except_self/solution.py
 create mode 100644 leetcode/product_of_array_except_self/tests.py

diff --git a/.templates/leetcode/json/product_of_array_except_self.json b/.templates/leetcode/json/product_of_array_except_self.json
new file mode 100644
index 0000000..1df2c43
--- /dev/null
+++ b/.templates/leetcode/json/product_of_array_except_self.json
@@ -0,0 +1,43 @@
+{
+    "problem_name": "product_of_array_except_self",
+    "solution_class_name": "Solution",
+    "problem_number": "238",
+    "problem_title": "Product of Array Except Self",
+    "difficulty": "Medium",
+    "topics": "Array, Prefix Sum",
+    "tags": ["grind-75"],
+    "readme_description": "Given an integer array `nums`, return an array `answer` such that `answer[i]` is equal to the product of all the elements of `nums` except `nums[i]`.\n\nThe product of any prefix or suffix of `nums` is guaranteed to fit in a 32-bit integer.\n\nYou must write an algorithm that runs in O(n) time and without using the division operation.",
+    "readme_examples": [
+        { "content": "```\nInput: nums = [1,2,3,4]\nOutput: [24,12,8,6]\n```" },
+        { "content": "```\nInput: nums = [-1,1,0,-3,3]\nOutput: [0,0,9,0,0]\n```" }
+    ],
+    "readme_constraints": "- 2 <= nums.length <= 10^5\n- -30 <= nums[i] <= 30\n- The input is generated such that answer[i] is guaranteed to fit in a 32-bit integer.",
+    "readme_additional": "**Follow up:** Can you solve the problem in O(1) extra space complexity? (The output array does not count as extra space for space complexity analysis.)",
+    "solution_imports": "",
+    "solution_methods": [
+        {
+            "name": "product_except_self",
+            "parameters": "nums: list[int]",
+            "return_type": "list[int]",
+            "dummy_return": "[]"
+        }
+    ],
+    "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution",
+    "test_class_name": "ProductOfArrayExceptSelf",
+    "test_helper_methods": [
+        { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }
+    ],
+    "test_methods": [
+        {
+            "name": "test_product_except_self",
+            "parametrize": "nums, expected",
+            "parametrize_typed": "nums: list[int], expected: list[int]",
+            "test_cases": "[([1, 2, 3, 4], [24, 12, 8, 6]), ([-1, 1, 0, -3, 3], [0, 0, 9, 0, 0]), ([2, 3, 4, 5], [60, 40, 30, 24])]",
+            "body": "result = self.solution.product_except_self(nums)\nassert result == expected"
+        }
+    ],
+    "playground_imports": "from solution import Solution",
+    "playground_test_case": "# Example test case\nnums = [1, 2, 3, 4]\nexpected = [24, 12, 8, 6]",
+    "playground_execution": "result = Solution().product_except_self(nums)\nresult",
+    "playground_assertion": "assert result == expected"
+}
diff --git a/Makefile b/Makefile
index 20fc260..1bd60aa 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
 PYTHON_VERSION = 3.13
-PROBLEM ?= add_binary
+PROBLEM ?= product_of_array_except_self
 FORCE ?= 0
 COMMA := ,
 
diff --git a/leetcode/product_of_array_except_self/README.md b/leetcode/product_of_array_except_self/README.md
new file mode 100644
index 0000000..3f75f03
--- /dev/null
+++ b/leetcode/product_of_array_except_self/README.md
@@ -0,0 +1,39 @@
+# Product of Array Except Self
+
+**Difficulty:** Medium
+**Topics:** Array, Prefix Sum
+**Tags:** grind-75
+
+**LeetCode:** [Problem 238](https://leetcode.com/problems/product-of-array-except-self/description/)
+
+## Problem Description
+
+Given an integer array `nums`, return an array `answer` such that `answer[i]` is equal to the product of all the elements of `nums` except `nums[i]`.
+
+The product of any prefix or suffix of `nums` is guaranteed to fit in a 32-bit integer.
+
+You must write an algorithm that runs in O(n) time and without using the division operation.
+
+## Examples
+
+### Example 1:
+
+```
+Input: nums = [1,2,3,4]
+Output: [24,12,8,6]
+```
+
+### Example 2:
+
+```
+Input: nums = [-1,1,0,-3,3]
+Output: [0,0,9,0,0]
+```
+
+## Constraints
+
+- 2 <= nums.length <= 10^5
+- -30 <= nums[i] <= 30
+- The input is generated such that answer[i] is guaranteed to fit in a 32-bit integer.
+
+**Follow up:** Can you solve the problem in O(1) extra space complexity? (The output array does not count as extra space for space complexity analysis.)
diff --git a/leetcode/product_of_array_except_self/__init__.py b/leetcode/product_of_array_except_self/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/leetcode/product_of_array_except_self/playground.ipynb b/leetcode/product_of_array_except_self/playground.ipynb
new file mode 100644
index 0000000..ebfb399
--- /dev/null
+++ b/leetcode/product_of_array_except_self/playground.ipynb
@@ -0,0 +1,79 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "imports",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from solution import Solution"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "setup",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Example test case\n",
+    "nums = [1, 2, 3, 4]\n",
+    "expected = [24, 12, 8, 6]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "execute",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[24, 12, 8, 6]"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "result = Solution().product_except_self(nums)\n",
+    "result"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "test",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "assert result == expected"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "leetcode-py-py3.13",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.13.7"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/leetcode/product_of_array_except_self/solution.py b/leetcode/product_of_array_except_self/solution.py
new file mode 100644
index 0000000..821e548
--- /dev/null
+++ b/leetcode/product_of_array_except_self/solution.py
@@ -0,0 +1,25 @@
+class Solution:
+    # Time: O(n)
+    # Space: O(1)
+    def product_except_self(self, nums: list[int]) -> list[int]:
+        # Example: nums = [1, 2, 3, 4]
+        # Expected output: [24, 12, 8, 6]
+
+        n = len(nums)
+        result = [1] * n  # [1, 1, 1, 1]
+
+        # Left pass: result[i] = product of all elements to the left of i
+        # nums:   [1, 2, 3, 4]
+        # result: [1, 1, 2, 6] (left products)
+        for i in range(1, n):
+            result[i] = result[i - 1] * nums[i - 1]
+
+        # Right pass: multiply by product of all elements to the right of i
+        # right products: [24, 12, 4, 1]
+        # result: [1*24, 1*12, 2*4, 6*1] = [24, 12, 8, 6]
+        right = 1
+        for i in range(n - 1, -1, -1):
+            result[i] *= right
+            right *= nums[i]
+
+        return result
diff --git a/leetcode/product_of_array_except_self/tests.py b/leetcode/product_of_array_except_self/tests.py
new file mode 100644
index 0000000..e3532e4
--- /dev/null
+++ b/leetcode/product_of_array_except_self/tests.py
@@ -0,0 +1,38 @@
+import pytest
+
+from leetcode_py.test_utils import logged_test
+
+from .solution import Solution
+
+
+class TestProductOfArrayExceptSelf:
+    def setup_method(self):
+        self.solution = Solution()
+
+    @pytest.mark.parametrize(
+        "nums, expected",
+        [
+            ([1, 2, 3, 4], [24, 12, 8, 6]),
+            ([-1, 1, 0, -3, 3], [0, 0, 9, 0, 0]),
+            ([2, 3, 4, 5], [60, 40, 30, 24]),
+            # Edge cases
+            ([1, 1], [1, 1]),  # Minimum length
+            ([5, 2], [2, 5]),  # Two elements
+            ([0, 0], [0, 0]),  # All zeros
+            ([1, 0], [0, 1]),  # Single zero
+            ([0, 1, 2], [2, 0, 0]),  # Zero at start
+            ([1, 2, 0], [0, 0, 2]),  # Zero at end
+            # Negative numbers
+            ([-1, -2], [-2, -1]),
+            ([-1, -2, -3], [6, 3, 2]),
+            ([1, -2, 3], [-6, 3, -2]),
+            # All ones
+            ([1, 1, 1, 1], [1, 1, 1, 1]),
+            # Large numbers
+            ([10, 3, 5, 6, 2], [180, 600, 360, 300, 900]),
+        ],
+    )
+    @logged_test
+    def test_product_except_self(self, nums: list[int], expected: list[int]):
+        result = self.solution.product_except_self(nums)
+        assert result == expected