diff --git a/.amazonq/rules/problem-creation.md b/.amazonq/rules/problem-creation.md
index 2fc2a30..a673a25 100644
--- a/.amazonq/rules/problem-creation.md
+++ b/.amazonq/rules/problem-creation.md
@@ -32,13 +32,19 @@ poetry run python .templates/leetcode/scrape.py -s "two-sum"
Required fields for `.templates/leetcode/json/{problem_name}.json`:
+**CRITICAL: Use single quotes for Python strings in playground fields to avoid JSON escaping issues with Jupyter notebooks.**
+
+**JSON Escaping Rules:**
+
+- `playground_test_case`: Use single quotes for string literals (e.g., `s = 'hello'` not `s = "hello"`)
+- `playground_execution`: Use single quotes for string literals
+- `playground_assertion`: Use single quotes for string literals
+- Double quotes in JSON + cookiecutter + Jupyter notebook = triple escaping issues
+
**Reference examples in `.templates/leetcode/examples/` for complete templates:**
-- `basic.json5` - Array, string, number problems
+- `basic.json5` - All standard problems (array, string, tree, linked list, etc.)
- `design.json5` - Data structure design problems (LRU Cache, etc.)
-- `tree.json5` - Binary tree problems
-- `linked_list.json5` - Linked list problems
-- `matrix.json5` - 2D array/matrix problems
````json
{
diff --git a/.templates/leetcode/examples/README.md b/.templates/leetcode/examples/README.md
index 6dbfef8..df7d99b 100644
--- a/.templates/leetcode/examples/README.md
+++ b/.templates/leetcode/examples/README.md
@@ -1,117 +1,41 @@
-# LeetCode Problem Template Examples
+# JSON Template Examples
-This directory contains comprehensive JSON5 template examples for different types of LeetCode problems. These examples serve as references when creating new problems using the universal cookiecutter template.
+This directory contains comprehensive examples for creating LeetCode problem templates.
-## Template Types
+## Files
-### 1. `basic.json5` - Basic Algorithm Problems
+- **`basic.json5`** - Covers all standard problem types:
+ - Array problems (Container With Most Water)
+ - String problems (with JSON escaping notes)
+ - Tree problems (import and parameter examples)
+ - Linked list problems (import and parameter examples)
+ - Matrix problems
+ - Number problems
-**Use for:** Array, string, number, hash table problems
-**Examples:** Container With Most Water, Two Sum, Valid Palindrome
-**Key features:**
+- **`design.json5`** - Data structure design problems:
+ - Custom class names (LRUCache, not Solution)
+ - Multiple methods including `__init__`
+ - Complex test setup with operation sequences
+ - Custom imports
-- Simple `Solution` class with single method
-- Standard test parametrization
-- Basic playground setup
+## Key Differences
-### 2. `tree.json5` - Binary Tree Problems
+### Standard Problems (basic.json5)
-**Use for:** Binary tree, BST, tree traversal problems
-**Examples:** Invert Binary Tree, Maximum Depth, Serialize Tree
-**Key features:**
+- `solution_class_name`: Always "Solution"
+- Single method (usually)
+- Simple test cases with direct assertions
+- Standard imports
-- `TreeNode` imports and conversions
-- `TreeNode.from_list()` and `TreeNode.to_list()` in tests
-- Tree visualization support
+### Design Problems (design.json5)
-### 3. `linked_list.json5` - Linked List Problems
+- `solution_class_name`: Custom class name (e.g., "LRUCache")
+- Multiple methods including constructor
+- Operation sequence testing
+- Import custom class in tests
-**Use for:** Singly/doubly linked list problems
-**Examples:** Reverse Linked List, Merge Lists, Detect Cycle
-**Key features:**
+## Critical Notes
-- `ListNode` imports and conversions
-- `ListNode.from_list()` and `ListNode.to_list()` in tests
-- Arrow visualization support
-
-### 4. `design.json5` - Data Structure Design Problems
-
-**Use for:** Design problems requiring custom classes
-**Examples:** LRU Cache, Implement Trie, Design HashMap
-**Key features:**
-
-- Custom class names (not `Solution`)
-- Multiple methods including `__init__`
-- Complex operation sequence testing
-- Type annotations for complex test logic
-
-### 5. `matrix.json5` - 2D Array/Matrix Problems
-
-**Use for:** Matrix manipulation, 2D array problems
-**Examples:** Spiral Matrix, Rotate Image, Search 2D Matrix
-**Key features:**
-
-- 2D array type annotations (`list[list[int]]`)
-- Visual examples with images
-- Matrix-specific test cases
-
-## Usage Guidelines
-
-### Problem Type Detection
-
-1. **Basic**: Single algorithm, simple input/output
-2. **Tree**: Mentions "tree", "node", uses tree terminology
-3. **Linked List**: Mentions "linked list", "node", list operations
-4. **Design**: "Design", "Implement", multiple operations
-5. **Matrix**: "matrix", "2D array", "grid", visual layout
-
-### Key Template Fields
-
-#### Required Fields
-
-- `problem_name`: snake_case identifier
-- `solution_class_name`: "Solution" or custom class name
-- `problem_number`: LeetCode number as string
-- `problem_title`: Exact LeetCode title
-- `difficulty`: "Easy", "Medium", or "Hard"
-- `topics`: Comma-separated topic string
-- `solution_methods`: Array of method definitions
-
-#### Important Patterns
-
-- **Type Hints**: Use modern syntax (`list[int]`, `dict[str, int]`, `Type | None`)
-- **Method Names**: Always snake_case
-- **Test Cases**: String representation of Python data structures
-- **Imports**: Include necessary helper classes (TreeNode, ListNode)
-
-#### PascalCase Naming Rules
-
-For `solution_class_name` and `test_class_name` properties:
-
-- **Acronyms**: Keep all caps ("LRUCache" not "LruCache")
-- **Roman numerals**: Keep all caps ("ReverseLinkedListII" not "ReverseLinkedListIi")
-- **Common patterns**: "BST", "DFS", "BFS", "API", "URL", "HTML", "JSON", "XML"
-
-### Template Selection Process
-
-1. Identify problem type from description/title
-2. Choose appropriate template from examples
-3. Customize fields for specific problem
-4. Ensure imports match problem requirements
-5. Verify test setup matches data structures used
-
-## Validation
-
-All templates are validated against:
-
-- Cookiecutter template compatibility
-- Linting requirements (black, isort, ruff, mypy)
-- Test framework integration
-- Notebook JSON format compliance
-
-## Notes
-
-- JSON5 format allows comments for documentation
-- All examples are based on working, tested templates
-- Templates are designed for the universal cookiecutter system
-- Examples include both simple and complex problem patterns
+- **JSON Escaping**: Use single quotes for Python strings in playground fields
+- **Type Hints**: Use modern syntax (`list[int]`, `TreeNode | None`)
+- **PascalCase**: Keep acronyms ALL CAPS (LRUCache, ReverseLinkedListII)
diff --git a/.templates/leetcode/examples/basic.json5 b/.templates/leetcode/examples/basic.json5
index c00a02f..391c7c6 100644
--- a/.templates/leetcode/examples/basic.json5
+++ b/.templates/leetcode/examples/basic.json5
@@ -37,13 +37,22 @@
"readme_additional": "", // Optional: additional notes, follow-up questions
// === SOLUTION TEMPLATE ===
- "solution_imports": "", // Empty for basic problems, add imports if needed
+ "solution_imports": "", // Empty for basic problems
+ // For tree: "from leetcode_py import TreeNode"
+ // For linked list: "from leetcode_py import ListNode"
"solution_methods": [
{
"name": "max_area", // snake_case method name
"parameters": "height: list[int]", // Modern Python type hints (list[int], not List[int])
+ // For tree: "root: TreeNode | None"
+ // For linked list: "head: ListNode | None"
+ // For string: "s: str"
"return_type": "int", // Return type annotation
- "dummy_return": "0" // Default return value (auto-set by generator)
+ "dummy_return": "0" // Default return value
+ // For string: "\"\""
+ // For bool: "False"
+ // For list: "[]"
+ // For tree/linked list: "None"
}
],
@@ -68,8 +77,12 @@
],
// === PLAYGROUND NOTEBOOK ===
+ // CRITICAL: Use single quotes for Python strings to avoid JSON escaping issues with Jupyter notebooks
+ // Double quotes in JSON + cookiecutter + Jupyter notebook = triple escaping issues
"playground_imports": "from solution import Solution",
"playground_test_case": "# Example test case\nheight = [1,8,6,2,5,4,8,3,7]\nexpected = 49",
+ // For string problems: "s = 'hello'\nexpected = 'olleh'"
+ // For tree: "root_list = [3,9,20,None,None,15,7]\nroot = TreeNode.from_list(root_list)"
"playground_execution": "result = Solution().max_area(height)\nresult",
"playground_assertion": "assert result == expected"
}
diff --git a/.templates/leetcode/examples/design.json5 b/.templates/leetcode/examples/design.json5
index 9839fc7..dbe2096 100644
--- a/.templates/leetcode/examples/design.json5
+++ b/.templates/leetcode/examples/design.json5
@@ -75,7 +75,8 @@
],
// === PLAYGROUND NOTEBOOK ===
- // IMPORTANT: Design playground uses operation sequences like tests
+ // CRITICAL: Use single quotes for Python strings to avoid JSON escaping issues with Jupyter notebooks
+ // Double quotes in JSON + cookiecutter + Jupyter notebook = triple escaping issues
playground_imports: "from solution import LRUCache",
playground_test_case: "# Example test case\noperations = ['LRUCache', 'put', 'put', 'get', 'put', 'get', 'put', 'get', 'get', 'get']\ninputs = [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]\nexpected = [None, None, None, 1, None, -1, None, -1, 3, 4]",
playground_execution: "cache = None\nresults: list[int | None] = []\nfor i, op in enumerate(operations):\n if op == 'LRUCache':\n cache = LRUCache(inputs[i][0])\n results.append(None)\n elif op == 'get' and cache is not None:\n results.append(cache.get(inputs[i][0]))\n elif op == 'put' and cache is not None:\n cache.put(inputs[i][0], inputs[i][1])\n results.append(None)\nresults",
diff --git a/.templates/leetcode/examples/linked_list.json5 b/.templates/leetcode/examples/linked_list.json5
deleted file mode 100644
index f0a77b2..0000000
--- a/.templates/leetcode/examples/linked_list.json5
+++ /dev/null
@@ -1,78 +0,0 @@
-{
- // Linked List problem template - for linked list problems
- // Example: Reverse Linked List II
- // Key differences: ListNode imports, list-specific test setup
- // NOTE: PascalCase naming - keep acronyms/Roman numerals ALL CAPS (LRUCache, ReverseLinkedListII)
-
- // === PROBLEM IDENTIFICATION ===
- problem_name: "reverse_linked_list_ii", // snake_case: used for directory/file names
- solution_class_name: "Solution", // Always "Solution" for algorithm problems
- problem_number: "92", // LeetCode problem number as string
- problem_title: "Reverse Linked List II", // Exact title from LeetCode
- difficulty: "Medium", // Easy, Medium, Hard
- topics: "Linked List", // Linked list related topics
- tags: ["grind-75"], // Optional: common problem set tags
-
- // === README CONTENT ===
- // IMPORTANT: Preserve rich HTML content from LeetCode including:
- // - Code snippets with backticks: `code`
- // - Bold text: **bold** or bold
- // - Italic text: *italic* or italic
- // - Images:
tags with proper src and styling
- // - HTML formatting:
,
,
, - , etc.
- // - Mathematical expressions and special characters
- readme_description: "Given the `head` of a singly linked list and two integers `left` and `right` where `left <= right`, reverse the nodes of the list from position `left` to position `right`, and return the reversed list.",
-
- readme_examples: [
- {
- content: "```\nInput: head = [1,2,3,4,5], left = 2, right = 4\nOutput: [1,4,3,2,5]\n```",
- },
- {
- content: "```\nInput: head = [5], left = 1, right = 1\nOutput: [5]\n```",
- },
- ],
-
- readme_constraints: "- The number of nodes in the list is n\n- 1 <= n <= 500\n- -500 <= Node.val <= 500\n- 1 <= left <= right <= n",
- readme_additional: "**Follow up:** Could you do it in one pass?", // Optional follow-up questions
-
- // === SOLUTION TEMPLATE ===
- // IMPORTANT: Linked list problems need ListNode import
- solution_imports: "from leetcode_py import ListNode",
- solution_methods: [
- {
- name: "reverse_between", // snake_case method name
- parameters: "head: ListNode[int] | None, left: int, right: int", // Use ListNode[int] | None for nullable parameters
- return_type: "ListNode[int] | None", // Modern union syntax with explicit generic type
- dummy_return: "None", // None for linked list problems
- },
- ],
-
- // === TEST CONFIGURATION ===
- // IMPORTANT: Linked list tests need ListNode import and special test setup
- test_imports: "import pytest\n\nfrom leetcode_py import ListNode\nfrom leetcode_py.test_utils import logged_test\n\nfrom .solution import Solution",
- test_class_name: "ReverseLinkedListII", // PascalCase: TestClassName for pytest class
- test_helper_methods: [
- {
- name: "setup_method",
- parameters: "",
- body: "self.solution = Solution()",
- },
- ],
- test_methods: [
- {
- name: "test_reverse_between",
- parametrize: "head_list, left, right, expected_list", // Use *_list naming for list array inputs
- parametrize_typed: "head_list: list[int], left: int, right: int, expected_list: list[int]",
- test_cases: "[([1, 2, 3, 4, 5], 2, 4, [1, 4, 3, 2, 5]), ([5], 1, 1, [5])]",
- // IMPORTANT: Linked list test body converts arrays to ListNode and compares objects directly
- body: "head = ListNode[int].from_list(head_list)\nexpected = ListNode[int].from_list(expected_list)\nresult = self.solution.reverse_between(head, left, right)\nassert result == expected",
- },
- ],
-
- // === PLAYGROUND NOTEBOOK ===
- // IMPORTANT: Linked list playground needs ListNode import and conversion
- playground_imports: "from solution import Solution\n\nfrom leetcode_py import ListNode",
- playground_test_case: "# Example test case\nhead_list = [1, 2, 3, 4, 5]\nhead = ListNode[int].from_list(head_list)\nleft, right = 2, 4\nexpected = ListNode[int].from_list([1, 4, 3, 2, 5])",
- playground_execution: "result = Solution().reverse_between(head, left, right)\nresult",
- playground_assertion: "assert result == expected",
-}
diff --git a/.templates/leetcode/examples/matrix.json5 b/.templates/leetcode/examples/matrix.json5
deleted file mode 100644
index 63b6884..0000000
--- a/.templates/leetcode/examples/matrix.json5
+++ /dev/null
@@ -1,75 +0,0 @@
-{
- // Matrix problem template - for 2D array/matrix problems
- // Example: Spiral Matrix
- // Key differences: 2D array parameters, often have visual examples with images
- // NOTE: PascalCase naming - keep acronyms/Roman numerals ALL CAPS (LRUCache, ReverseLinkedListII)
-
- // === PROBLEM IDENTIFICATION ===
- "problem_name": "spiral_matrix", // snake_case: used for directory/file names
- "solution_class_name": "Solution", // Always "Solution" for algorithm problems
- "problem_number": "54", // LeetCode problem number as string
- "problem_title": "Spiral Matrix", // Exact title from LeetCode
- "difficulty": "Medium", // Easy, Medium, Hard
- "topics": "Array, Matrix, Simulation", // Matrix-related topics
- "tags": ["grind-75"], // Optional: common problem set tags
-
- // === README CONTENT ===
- // IMPORTANT: Preserve rich HTML content from LeetCode including:
- // - Code snippets with backticks: `code`
- // - Bold text: **bold** or bold
- // - Italic text: *italic* or italic
- // - Images:
tags with proper src and styling
- // - HTML formatting: ,
,
, - , etc.
- // - Mathematical expressions and special characters
- "readme_description": "Given an `m x n` matrix, return all elements of the matrix in spiral order.",
-
- "readme_examples": [
- {
- // IMPORTANT: Matrix problems often have visual diagrams - include images
- "content": "
\\n\\n```\\nInput: matrix = [[1,2,3],[4,5,6],[7,8,9]]\\nOutput: [1,2,3,6,9,8,7,4,5]\\n```"
- },
- {
- "content": "
\\n\\n```\\nInput: matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]\\nOutput: [1,2,3,4,8,12,11,10,9,5,6,7]\\n```"
- }
- ],
-
- "readme_constraints": "- m == matrix.length\\n- n == matrix[i].length\\n- 1 <= m, n <= 10\\n- -100 <= matrix[i][j] <= 100",
- "readme_additional": "",
-
- // === SOLUTION TEMPLATE ===
- "solution_imports": "", // Usually empty for matrix problems
- "solution_methods": [
- {
- "name": "spiral_order", // snake_case method name
- "parameters": "matrix: list[list[int]]", // 2D array type annotation
- "return_type": "list[int]", // Usually returns flattened result
- "dummy_return": "[]" // Empty list for array returns
- }
- ],
-
- // === TEST CONFIGURATION ===
- "test_imports": "import pytest\\nfrom leetcode_py.test_utils import logged_test\\nfrom .solution import Solution",
- "test_class_name": "SpiralMatrix", // PascalCase: TestClassName for pytest class
- "test_helper_methods": [
- {
- "name": "setup_method",
- "parameters": "",
- "body": "self.solution = Solution()"
- }
- ],
- "test_methods": [
- {
- "name": "test_spiral_order",
- "parametrize": "matrix, expected", // Simple matrix, expected pattern
- "parametrize_typed": "matrix: list[list[int]], expected: list[int]",
- "test_cases": "[([[1,2,3],[4,5,6],[7,8,9]], [1,2,3,6,9,8,7,4,5]), ([[1,2,3,4],[5,6,7,8],[9,10,11,12]], [1,2,3,4,8,12,11,10,9,5,6,7])]",
- "body": "result = self.solution.spiral_order(matrix)\\nassert result == expected"
- }
- ],
-
- // === PLAYGROUND NOTEBOOK ===
- "playground_imports": "from solution import Solution",
- "playground_test_case": "# Example test case\\nmatrix = [[1,2,3],[4,5,6],[7,8,9]]\\nexpected = [1,2,3,6,9,8,7,4,5]",
- "playground_execution": "result = Solution().spiral_order(matrix)\\nresult",
- "playground_assertion": "assert result == expected"
-}
diff --git a/.templates/leetcode/examples/tree.json5 b/.templates/leetcode/examples/tree.json5
deleted file mode 100644
index fd5cf13..0000000
--- a/.templates/leetcode/examples/tree.json5
+++ /dev/null
@@ -1,82 +0,0 @@
-{
- // Tree problem template - for binary tree problems
- // Example: Invert Binary Tree
- // Key differences: TreeNode imports, tree-specific test setup
- // NOTE: PascalCase naming - keep acronyms/Roman numerals ALL CAPS (LRUCache, ReverseLinkedListII)
-
- // === PROBLEM IDENTIFICATION ===
- problem_name: "invert_binary_tree", // snake_case: used for directory/file names
- solution_class_name: "Solution", // Always "Solution" for algorithm problems
- problem_number: "226", // LeetCode problem number as string
- problem_title: "Invert Binary Tree", // Exact title from LeetCode
- difficulty: "Easy", // Easy, Medium, Hard
- topics: "Tree, Depth-First Search, Breadth-First Search, Binary Tree", // Tree-related topics
- tags: ["grind-75"], // Optional: common problem set tags
-
- // === README CONTENT ===
- // IMPORTANT: Preserve rich HTML content from LeetCode including:
- // - Code snippets with backticks: `code`
- // - Bold text: **bold** or bold
- // - Italic text: *italic* or italic
- // - Images:
tags with proper src and styling
- // - HTML formatting: ,
,
, - , etc.
- // - Mathematical expressions and special characters
- readme_description: "Given the `root` of a binary tree, invert the tree, and return its root.",
-
- readme_examples: [
- {
- // Tree problems often have visual examples with images
- content: "```\nInput: root = [4,2,7,1,3,6,9]\nOutput: [4,7,2,9,6,3,1]\n```",
- },
- {
- content: "```\nInput: root = [2,1,3]\nOutput: [2,3,1]\n```",
- },
- {
- content: "```\nInput: root = []\nOutput: []\n```",
- },
- ],
-
- readme_constraints: "- The number of nodes in the tree is in the range [0, 100]\n- -100 <= Node.val <= 100",
- readme_additional: "",
-
- // === SOLUTION TEMPLATE ===
- // IMPORTANT: Tree problems need TreeNode import
- solution_imports: "from leetcode_py import TreeNode",
- solution_methods: [
- {
- name: "invert_tree", // snake_case method name
- parameters: "root: TreeNode[int] | None", // Use TreeNode[int] | None for nullable tree parameters
- return_type: "TreeNode[int] | None", // Modern union syntax with explicit generic type
- dummy_return: "None", // None for tree problems
- },
- ],
-
- // === TEST CONFIGURATION ===
- // IMPORTANT: Tree tests need TreeNode import and special test setup
- test_imports: "import pytest\n\nfrom leetcode_py import TreeNode\nfrom leetcode_py.test_utils import logged_test\n\nfrom .solution import Solution",
- test_class_name: "InvertBinaryTree", // PascalCase: TestClassName for pytest class
- test_helper_methods: [
- {
- name: "setup_method",
- parameters: "",
- body: "self.solution = Solution()",
- },
- ],
- test_methods: [
- {
- name: "test_invert_tree",
- parametrize: "root_list, expected_list", // Use *_list naming for tree array inputs
- parametrize_typed: "root_list: list[int | None], expected_list: list[int | None]",
- test_cases: "[([4, 2, 7, 1, 3, 6, 9], [4, 7, 2, 9, 6, 3, 1]), ([2, 1, 3], [2, 3, 1]), ([], [])]",
- // IMPORTANT: Tree test body converts arrays to TreeNode and compares objects directly
- body: "root = TreeNode[int].from_list(root_list)\nexpected = TreeNode[int].from_list(expected_list)\nresult = self.solution.invert_tree(root)\nassert result == expected",
- },
- ],
-
- // === PLAYGROUND NOTEBOOK ===
- // IMPORTANT: Tree playground needs TreeNode import and conversion
- playground_imports: "from solution import Solution\n\nfrom leetcode_py import TreeNode",
- playground_test_case: "# Example test case\nroot_list: list[int | None] = [4, 2, 7, 1, 3, 6, 9]\nroot = TreeNode[int].from_list(root_list)\nexpected = TreeNode[int].from_list([4, 7, 2, 9, 6, 3, 1])",
- playground_execution: "result = Solution().invert_tree(root)\nresult",
- playground_assertion: "assert result == expected",
-}
diff --git a/.templates/leetcode/json/accounts_merge.json b/.templates/leetcode/json/accounts_merge.json
new file mode 100644
index 0000000..9a601d1
--- /dev/null
+++ b/.templates/leetcode/json/accounts_merge.json
@@ -0,0 +1,47 @@
+{
+ "problem_name": "accounts_merge",
+ "solution_class_name": "Solution",
+ "problem_number": "721",
+ "problem_title": "Accounts Merge",
+ "difficulty": "Medium",
+ "topics": "Array, Hash Table, String, Depth-First Search, Breadth-First Search, Union Find, Sorting",
+ "tags": ["grind-75"],
+ "readme_description": "Given a list of `accounts` where each element `accounts[i]` is a list of strings, where the first element `accounts[i][0]` is a name, and the rest of the elements are **emails** representing emails of the account.\n\nNow, we would like to merge these accounts. Two accounts definitely belong to the same person if there is some common email to both accounts. Note that even if two accounts have the same name, they may belong to different people as people could have the same name. A person can have any number of accounts initially, but all of their accounts definitely have the same name.\n\nAfter merging the accounts, return the accounts in the following format: the first element of each account is the name, and the rest of the elements are emails **in sorted order**. The accounts themselves can be returned in **any order**.",
+ "readme_examples": [
+ {
+ "content": "```\nInput: accounts = [[\"John\",\"johnsmith@mail.com\",\"john_newyork@mail.com\"],[\"John\",\"johnsmith@mail.com\",\"john00@mail.com\"],[\"Mary\",\"mary@mail.com\"],[\"John\",\"johnnybravo@mail.com\"]]\nOutput: [[\"John\",\"john00@mail.com\",\"john_newyork@mail.com\",\"johnsmith@mail.com\"],[\"Mary\",\"mary@mail.com\"],[\"John\",\"johnnybravo@mail.com\"]]\n```\n**Explanation:** The first and second John's are the same person as they have the common email \"johnsmith@mail.com\". The third John and Mary are different people as none of their email addresses are used by other accounts."
+ },
+ {
+ "content": "```\nInput: accounts = [[\"Gabe\",\"Gabe0@m.co\",\"Gabe3@m.co\",\"Gabe1@m.co\"],[\"Kevin\",\"Kevin3@m.co\",\"Kevin5@m.co\",\"Kevin0@m.co\"],[\"Ethan\",\"Ethan5@m.co\",\"Ethan4@m.co\",\"Ethan0@m.co\"],[\"Hanzo\",\"Hanzo3@m.co\",\"Hanzo1@m.co\",\"Hanzo0@m.co\"],[\"Fern\",\"Fern5@m.co\",\"Fern1@m.co\",\"Fern0@m.co\"]]\nOutput: [[\"Ethan\",\"Ethan0@m.co\",\"Ethan4@m.co\",\"Ethan5@m.co\"],[\"Gabe\",\"Gabe0@m.co\",\"Gabe1@m.co\",\"Gabe3@m.co\"],[\"Hanzo\",\"Hanzo0@m.co\",\"Hanzo1@m.co\",\"Hanzo3@m.co\"],[\"Kevin\",\"Kevin0@m.co\",\"Kevin3@m.co\",\"Kevin5@m.co\"],[\"Fern\",\"Fern0@m.co\",\"Fern1@m.co\",\"Fern5@m.co\"]]\n```"
+ }
+ ],
+ "readme_constraints": "- `1 <= accounts.length <= 1000`\n- `2 <= accounts[i].length <= 10`\n- `1 <= accounts[i][j].length <= 30`\n- `accounts[i][0]` consists of English letters.\n- `accounts[i][j] (for j > 0)` is a valid email.",
+ "readme_additional": "",
+ "solution_imports": "",
+ "solution_methods": [
+ {
+ "name": "accounts_merge",
+ "parameters": "accounts: list[list[str]]",
+ "return_type": "list[list[str]]",
+ "dummy_return": "[]"
+ }
+ ],
+ "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution",
+ "test_class_name": "AccountsMerge",
+ "test_helper_methods": [
+ { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }
+ ],
+ "test_methods": [
+ {
+ "name": "test_accounts_merge",
+ "parametrize": "accounts, expected",
+ "parametrize_typed": "accounts: list[list[str]], expected: list[list[str]]",
+ "test_cases": "[([[\"John\", \"johnsmith@mail.com\", \"john_newyork@mail.com\"], [\"John\", \"johnsmith@mail.com\", \"john00@mail.com\"], [\"Mary\", \"mary@mail.com\"], [\"John\", \"johnnybravo@mail.com\"]], [[\"John\", \"john00@mail.com\", \"john_newyork@mail.com\", \"johnsmith@mail.com\"], [\"Mary\", \"mary@mail.com\"], [\"John\", \"johnnybravo@mail.com\"]]), ([[\"Gabe\", \"Gabe0@m.co\", \"Gabe3@m.co\", \"Gabe1@m.co\"], [\"Kevin\", \"Kevin3@m.co\", \"Kevin5@m.co\", \"Kevin0@m.co\"], [\"Ethan\", \"Ethan5@m.co\", \"Ethan4@m.co\", \"Ethan0@m.co\"], [\"Hanzo\", \"Hanzo3@m.co\", \"Hanzo1@m.co\", \"Hanzo0@m.co\"], [\"Fern\", \"Fern5@m.co\", \"Fern1@m.co\", \"Fern0@m.co\"]], [[\"Ethan\", \"Ethan0@m.co\", \"Ethan4@m.co\", \"Ethan5@m.co\"], [\"Gabe\", \"Gabe0@m.co\", \"Gabe1@m.co\", \"Gabe3@m.co\"], [\"Hanzo\", \"Hanzo0@m.co\", \"Hanzo1@m.co\", \"Hanzo3@m.co\"], [\"Kevin\", \"Kevin0@m.co\", \"Kevin3@m.co\", \"Kevin5@m.co\"], [\"Fern\", \"Fern0@m.co\", \"Fern1@m.co\", \"Fern5@m.co\"]])]",
+ "body": "result = self.solution.accounts_merge(accounts)\n# Sort both result and expected for comparison since order doesn't matter\nresult_sorted = [sorted(account) for account in sorted(result)]\nexpected_sorted = [sorted(account) for account in sorted(expected)]\nassert result_sorted == expected_sorted"
+ }
+ ],
+ "playground_imports": "from solution import Solution",
+ "playground_test_case": "# Example test case\naccounts = [[\"John\", \"johnsmith@mail.com\", \"john_newyork@mail.com\"], [\"John\", \"johnsmith@mail.com\", \"john00@mail.com\"], [\"Mary\", \"mary@mail.com\"], [\"John\", \"johnnybravo@mail.com\"]]\nexpected = [[\"John\", \"john00@mail.com\", \"john_newyork@mail.com\", \"johnsmith@mail.com\"], [\"Mary\", \"mary@mail.com\"], [\"John\", \"johnnybravo@mail.com\"]]",
+ "playground_execution": "result = Solution().accounts_merge(accounts)\nresult",
+ "playground_assertion": "# Sort for comparison since order doesn't matter\nresult_sorted = [sorted(account) for account in sorted(result)]\nexpected_sorted = [sorted(account) for account in sorted(expected)]\nassert result_sorted == expected_sorted"
+}
diff --git a/.templates/leetcode/json/longest_palindromic_substring.json b/.templates/leetcode/json/longest_palindromic_substring.json
new file mode 100644
index 0000000..8ee3e31
--- /dev/null
+++ b/.templates/leetcode/json/longest_palindromic_substring.json
@@ -0,0 +1,45 @@
+{
+ "problem_name": "longest_palindromic_substring",
+ "solution_class_name": "Solution",
+ "problem_number": "5",
+ "problem_title": "Longest Palindromic Substring",
+ "difficulty": "Medium",
+ "topics": "Two Pointers, String, Dynamic Programming",
+ "tags": ["grind-75"],
+ "readme_description": "Given a string `s`, return the longest palindromic substring in `s`.",
+ "readme_examples": [
+ {
+ "content": "```\nInput: s = \"babad\"\nOutput: \"bab\"\n```\n**Explanation:** \"aba\" is also a valid answer."
+ },
+ { "content": "```\nInput: s = \"cbbd\"\nOutput: \"bb\"\n```" }
+ ],
+ "readme_constraints": "- `1 <= s.length <= 1000`\n- `s` consist of only digits and English letters.",
+ "readme_additional": "",
+ "solution_imports": "",
+ "solution_methods": [
+ {
+ "name": "longest_palindrome",
+ "parameters": "s: 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": "LongestPalindromicSubstring",
+ "test_helper_methods": [
+ { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }
+ ],
+ "test_methods": [
+ {
+ "name": "test_longest_palindrome",
+ "parametrize": "s, expected",
+ "parametrize_typed": "s: str, expected: set[str]",
+ "test_cases": "[('babad', {'bab', 'aba'}), ('cbbd', {'bb'}), ('a', {'a'}), ('ac', {'a', 'c'}), ('racecar', {'racecar'}), ('abcdef', {'a', 'b', 'c', 'd', 'e', 'f'}), ('aabbaa', {'aabbaa'}), ('abacabad', {'abacaba'}), ('noon', {'noon'}), ('abccba', {'abccba'}), ('', {''}), ('aa', {'aa'}), ('aba', {'aba'}), ('abcba', {'abcba'}), ('forgeeksskeegfor', {'geeksskeeg'}), ('bananas', {'anana'}), ('abcdefghijklmnopqrstuvwxyz', {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'})]",
+ "body": "result = self.solution.longest_palindrome(s)\nassert result in expected"
+ }
+ ],
+ "playground_imports": "from solution import Solution",
+ "playground_test_case": "# Example test case\ns = 'babad'\nexpected = {'bab', 'aba'}",
+ "playground_execution": "result = Solution().longest_palindrome(s)\nresult",
+ "playground_assertion": "assert result in expected"
+}
diff --git a/Makefile b/Makefile
index d49ed4d..0f337a9 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
PYTHON_VERSION = 3.13
-PROBLEM ?= k_closest_points_to_origin
+PROBLEM ?= longest_palindromic_substring
FORCE ?= 0
sync_submodules:
diff --git a/leetcode/accounts_merge/README.md b/leetcode/accounts_merge/README.md
new file mode 100644
index 0000000..53a64a3
--- /dev/null
+++ b/leetcode/accounts_merge/README.md
@@ -0,0 +1,41 @@
+# Accounts Merge
+
+**Difficulty:** Medium
+**Topics:** Array, Hash Table, String, Depth-First Search, Breadth-First Search, Union Find, Sorting
+**Tags:** grind-75
+
+**LeetCode:** [Problem 721](https://leetcode.com/problems/accounts-merge/description/)
+
+## Problem Description
+
+Given a list of `accounts` where each element `accounts[i]` is a list of strings, where the first element `accounts[i][0]` is a name, and the rest of the elements are **emails** representing emails of the account.
+
+Now, we would like to merge these accounts. Two accounts definitely belong to the same person if there is some common email to both accounts. Note that even if two accounts have the same name, they may belong to different people as people could have the same name. A person can have any number of accounts initially, but all of their accounts definitely have the same name.
+
+After merging the accounts, return the accounts in the following format: the first element of each account is the name, and the rest of the elements are emails **in sorted order**. The accounts themselves can be returned in **any order**.
+
+## Examples
+
+### Example 1:
+
+```
+Input: accounts = [["John","johnsmith@mail.com","john_newyork@mail.com"],["John","johnsmith@mail.com","john00@mail.com"],["Mary","mary@mail.com"],["John","johnnybravo@mail.com"]]
+Output: [["John","john00@mail.com","john_newyork@mail.com","johnsmith@mail.com"],["Mary","mary@mail.com"],["John","johnnybravo@mail.com"]]
+```
+
+**Explanation:** The first and second John's are the same person as they have the common email "johnsmith@mail.com". The third John and Mary are different people as none of their email addresses are used by other accounts.
+
+### Example 2:
+
+```
+Input: accounts = [["Gabe","Gabe0@m.co","Gabe3@m.co","Gabe1@m.co"],["Kevin","Kevin3@m.co","Kevin5@m.co","Kevin0@m.co"],["Ethan","Ethan5@m.co","Ethan4@m.co","Ethan0@m.co"],["Hanzo","Hanzo3@m.co","Hanzo1@m.co","Hanzo0@m.co"],["Fern","Fern5@m.co","Fern1@m.co","Fern0@m.co"]]
+Output: [["Ethan","Ethan0@m.co","Ethan4@m.co","Ethan5@m.co"],["Gabe","Gabe0@m.co","Gabe1@m.co","Gabe3@m.co"],["Hanzo","Hanzo0@m.co","Hanzo1@m.co","Hanzo3@m.co"],["Kevin","Kevin0@m.co","Kevin3@m.co","Kevin5@m.co"],["Fern","Fern0@m.co","Fern1@m.co","Fern5@m.co"]]
+```
+
+## Constraints
+
+- `1 <= accounts.length <= 1000`
+- `2 <= accounts[i].length <= 10`
+- `1 <= accounts[i][j].length <= 30`
+- `accounts[i][0]` consists of English letters.
+- `accounts[i][j] (for j > 0)` is a valid email.
diff --git a/leetcode/accounts_merge/__init__.py b/leetcode/accounts_merge/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/leetcode/accounts_merge/playground.ipynb b/leetcode/accounts_merge/playground.ipynb
new file mode 100644
index 0000000..46a7e86
--- /dev/null
+++ b/leetcode/accounts_merge/playground.ipynb
@@ -0,0 +1,93 @@
+{
+ "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",
+ "accounts = [\n",
+ " [\"John\", \"johnsmith@mail.com\", \"john_newyork@mail.com\"],\n",
+ " [\"John\", \"johnsmith@mail.com\", \"john00@mail.com\"],\n",
+ " [\"Mary\", \"mary@mail.com\"],\n",
+ " [\"John\", \"johnnybravo@mail.com\"],\n",
+ "]\n",
+ "expected = [\n",
+ " [\"John\", \"john00@mail.com\", \"john_newyork@mail.com\", \"johnsmith@mail.com\"],\n",
+ " [\"Mary\", \"mary@mail.com\"],\n",
+ " [\"John\", \"johnnybravo@mail.com\"],\n",
+ "]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "execute",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[['John', 'john00@mail.com', 'john_newyork@mail.com', 'johnsmith@mail.com'],\n",
+ " ['Mary', 'mary@mail.com'],\n",
+ " ['John', 'johnnybravo@mail.com']]"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "result = Solution().accounts_merge(accounts)\n",
+ "result"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "test",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Sort for comparison since order doesn't matter\n",
+ "result_sorted = [sorted(account) for account in sorted(result)]\n",
+ "expected_sorted = [sorted(account) for account in sorted(expected)]\n",
+ "assert result_sorted == expected_sorted"
+ ]
+ }
+ ],
+ "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/accounts_merge/solution.py b/leetcode/accounts_merge/solution.py
new file mode 100644
index 0000000..dd59d46
--- /dev/null
+++ b/leetcode/accounts_merge/solution.py
@@ -0,0 +1,34 @@
+class Solution:
+ # Time: O(N * M) where N is accounts, M is max emails per account
+ # Space: O(N * M)
+ def accounts_merge(self, accounts: list[list[str]]) -> list[list[str]]:
+ email_to_accounts: dict[str, list[int]] = {}
+
+ for i, account in enumerate(accounts):
+ for email in account[1:]:
+ if email not in email_to_accounts:
+ email_to_accounts[email] = []
+ email_to_accounts[email].append(i)
+
+ visited: set[int] = set()
+ result = []
+
+ def dfs(account_idx: int, emails: set[str]) -> None:
+ if account_idx in visited:
+ return
+ visited.add(account_idx)
+
+ for email in accounts[account_idx][1:]:
+ emails.add(email)
+ for neighbor_idx in email_to_accounts[email]:
+ dfs(neighbor_idx, emails)
+
+ for i in range(len(accounts)):
+ if i in visited:
+ continue
+
+ emails: set[str] = set()
+ dfs(i, emails)
+ result.append([accounts[i][0]] + sorted(emails))
+
+ return result
diff --git a/leetcode/accounts_merge/tests.py b/leetcode/accounts_merge/tests.py
new file mode 100644
index 0000000..a201047
--- /dev/null
+++ b/leetcode/accounts_merge/tests.py
@@ -0,0 +1,112 @@
+import pytest
+
+from leetcode_py.test_utils import logged_test
+
+from .solution import Solution
+
+
+class TestAccountsMerge:
+ def setup_method(self):
+ self.solution = Solution()
+
+ @pytest.mark.parametrize(
+ "accounts, expected",
+ [
+ (
+ [
+ ["John", "johnsmith@mail.com", "john_newyork@mail.com"],
+ ["John", "johnsmith@mail.com", "john00@mail.com"],
+ ["Mary", "mary@mail.com"],
+ ["John", "johnnybravo@mail.com"],
+ ],
+ [
+ ["John", "john00@mail.com", "john_newyork@mail.com", "johnsmith@mail.com"],
+ ["Mary", "mary@mail.com"],
+ ["John", "johnnybravo@mail.com"],
+ ],
+ ),
+ (
+ [
+ ["Gabe", "Gabe0@m.co", "Gabe3@m.co", "Gabe1@m.co"],
+ ["Kevin", "Kevin3@m.co", "Kevin5@m.co", "Kevin0@m.co"],
+ ["Ethan", "Ethan5@m.co", "Ethan4@m.co", "Ethan0@m.co"],
+ ["Hanzo", "Hanzo3@m.co", "Hanzo1@m.co", "Hanzo0@m.co"],
+ ["Fern", "Fern5@m.co", "Fern1@m.co", "Fern0@m.co"],
+ ],
+ [
+ ["Ethan", "Ethan0@m.co", "Ethan4@m.co", "Ethan5@m.co"],
+ ["Gabe", "Gabe0@m.co", "Gabe1@m.co", "Gabe3@m.co"],
+ ["Hanzo", "Hanzo0@m.co", "Hanzo1@m.co", "Hanzo3@m.co"],
+ ["Kevin", "Kevin0@m.co", "Kevin3@m.co", "Kevin5@m.co"],
+ ["Fern", "Fern0@m.co", "Fern1@m.co", "Fern5@m.co"],
+ ],
+ ),
+ # Single account
+ (
+ [["Alice", "alice@mail.com"]],
+ [["Alice", "alice@mail.com"]],
+ ),
+ # No merging needed - all separate
+ (
+ [
+ ["Alice", "alice@mail.com"],
+ ["Bob", "bob@mail.com"],
+ ["Charlie", "charlie@mail.com"],
+ ],
+ [
+ ["Alice", "alice@mail.com"],
+ ["Bob", "bob@mail.com"],
+ ["Charlie", "charlie@mail.com"],
+ ],
+ ),
+ # Chain merging - A->B->C
+ (
+ [
+ ["John", "john1@mail.com", "john2@mail.com"],
+ ["John", "john2@mail.com", "john3@mail.com"],
+ ["John", "john3@mail.com", "john4@mail.com"],
+ ],
+ [["John", "john1@mail.com", "john2@mail.com", "john3@mail.com", "john4@mail.com"]],
+ ),
+ # Multiple emails per account with complex merging
+ (
+ [
+ ["David", "david1@mail.com", "david2@mail.com", "david3@mail.com"],
+ ["David", "david3@mail.com", "david4@mail.com"],
+ ["Sarah", "sarah@mail.com"],
+ ["David", "david5@mail.com", "david1@mail.com"],
+ ],
+ [
+ [
+ "David",
+ "david1@mail.com",
+ "david2@mail.com",
+ "david3@mail.com",
+ "david4@mail.com",
+ "david5@mail.com",
+ ],
+ ["Sarah", "sarah@mail.com"],
+ ],
+ ),
+ # Same name different people
+ (
+ [
+ ["John", "john1@mail.com"],
+ ["John", "john2@mail.com"],
+ ["John", "john3@mail.com"],
+ ],
+ [
+ ["John", "john1@mail.com"],
+ ["John", "john2@mail.com"],
+ ["John", "john3@mail.com"],
+ ],
+ ),
+ ],
+ )
+ @logged_test
+ def test_accounts_merge(self, accounts: list[list[str]], expected: list[list[str]]):
+ result = self.solution.accounts_merge(accounts)
+ # Sort both result and expected for comparison since order doesn't matter
+ result_sorted = [sorted(account) for account in sorted(result)]
+ expected_sorted = [sorted(account) for account in sorted(expected)]
+ assert result_sorted == expected_sorted
diff --git a/leetcode/longest_palindromic_substring/README.md b/leetcode/longest_palindromic_substring/README.md
new file mode 100644
index 0000000..29669ab
--- /dev/null
+++ b/leetcode/longest_palindromic_substring/README.md
@@ -0,0 +1,34 @@
+# Longest Palindromic Substring
+
+**Difficulty:** Medium
+**Topics:** Two Pointers, String, Dynamic Programming
+**Tags:** grind-75
+
+**LeetCode:** [Problem 5](https://leetcode.com/problems/longest-palindromic-substring/description/)
+
+## Problem Description
+
+Given a string `s`, return the longest palindromic substring in `s`.
+
+## Examples
+
+### Example 1:
+
+```
+Input: s = "babad"
+Output: "bab"
+```
+
+**Explanation:** "aba" is also a valid answer.
+
+### Example 2:
+
+```
+Input: s = "cbbd"
+Output: "bb"
+```
+
+## Constraints
+
+- `1 <= s.length <= 1000`
+- `s` consist of only digits and English letters.
diff --git a/leetcode/longest_palindromic_substring/__init__.py b/leetcode/longest_palindromic_substring/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/leetcode/longest_palindromic_substring/playground.ipynb b/leetcode/longest_palindromic_substring/playground.ipynb
new file mode 100644
index 0000000..8100878
--- /dev/null
+++ b/leetcode/longest_palindromic_substring/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 = \"babad\"\n",
+ "expected = {\"bab\", \"aba\"}"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "execute",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'bab'"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "result = Solution().longest_palindrome(s)\n",
+ "result"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "test",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "assert result in 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/longest_palindromic_substring/solution.py b/leetcode/longest_palindromic_substring/solution.py
new file mode 100644
index 0000000..b673d49
--- /dev/null
+++ b/leetcode/longest_palindromic_substring/solution.py
@@ -0,0 +1,52 @@
+class Solution:
+ # Time: O(n^2)
+ # Space: O(1)
+ def longest_palindrome(self, s: str) -> str:
+ start = 0
+ max_len = 0
+
+ for i in range(len(s)):
+ # Odd length palindromes (center at i)
+ len1 = self.expand(s, i, i)
+ # Even length palindromes (center between i and i+1)
+ len2 = self.expand(s, i, i + 1)
+
+ curr_len = max(len1, len2)
+ if curr_len > max_len:
+ max_len = curr_len
+ start = i - (curr_len - 1) // 2
+
+ return s[start : start + max_len]
+
+ @staticmethod
+ def expand(s: str, left: int, right: int) -> int:
+ while left >= 0 and right < len(s) and s[left] == s[right]:
+ left -= 1
+ right += 1
+ return right - left - 1
+
+
+class SolutionManacher:
+ # Time: O(n)
+ # Space: O(n)
+ def longest_palindrome(self, s: str) -> str:
+ t = "#".join("^{}$".format(s))
+ n = len(t)
+ p = [0] * n
+ center = right = 0
+ for i in range(1, n - 1):
+ mirror_value = 2 * center - i
+ p[i] = min(right - i, p[mirror_value]) if right > i else 0
+
+ while t[i + 1 + p[i]] == t[i - 1 - p[i]]:
+ p[i] += 1
+
+ if i + p[i] > right:
+ center, right = i, i + p[i]
+
+ max_len = max(p)
+ center_index = p.index(max_len)
+
+ # Map back to original string: (center_index - max_len) // 2
+ start = (center_index - max_len) // 2
+ return s[start : start + max_len]
diff --git a/leetcode/longest_palindromic_substring/tests.py b/leetcode/longest_palindromic_substring/tests.py
new file mode 100644
index 0000000..7992302
--- /dev/null
+++ b/leetcode/longest_palindromic_substring/tests.py
@@ -0,0 +1,74 @@
+import pytest
+
+from leetcode_py.test_utils import logged_test
+
+from .solution import Solution, SolutionManacher
+
+
+class TestLongestPalindromicSubstring:
+ def setup_method(self):
+ self.solution = Solution()
+ self.solution_manacher = SolutionManacher()
+
+ @pytest.mark.parametrize(
+ "solution_class",
+ [Solution, SolutionManacher],
+ )
+ @pytest.mark.parametrize(
+ "s, expected",
+ [
+ ("babad", {"bab", "aba"}),
+ ("cbbd", {"bb"}),
+ ("a", {"a"}),
+ ("ac", {"a", "c"}),
+ ("racecar", {"racecar"}),
+ ("abcdef", {"a", "b", "c", "d", "e", "f"}),
+ ("aabbaa", {"aabbaa"}),
+ ("abacabad", {"abacaba"}),
+ ("aaaaaaaa", {"aaaaaaaa"}),
+ ("noon", {"noon"}),
+ ("abccba", {"abccba"}),
+ ("", {""}),
+ ("aa", {"aa"}),
+ ("aba", {"aba"}),
+ ("abcba", {"abcba"}),
+ ("forgeeksskeegfor", {"geeksskeeg"}),
+ ("bananas", {"anana"}),
+ (
+ "abcdefghijklmnopqrstuvwxyz",
+ {
+ "a",
+ "b",
+ "c",
+ "d",
+ "e",
+ "f",
+ "g",
+ "h",
+ "i",
+ "j",
+ "k",
+ "l",
+ "m",
+ "n",
+ "o",
+ "p",
+ "q",
+ "r",
+ "s",
+ "t",
+ "u",
+ "v",
+ "w",
+ "x",
+ "y",
+ "z",
+ },
+ ),
+ ],
+ )
+ @logged_test
+ def test_longest_palindrome(self, solution_class, s: str, expected: set[str]):
+ solution = solution_class()
+ result = solution.longest_palindrome(s)
+ assert result in expected
diff --git a/tests/data_structures/test_graph_node.py b/tests/data_structures/test_graph_node.py
index 704f189..492a7dd 100644
--- a/tests/data_structures/test_graph_node.py
+++ b/tests/data_structures/test_graph_node.py
@@ -159,3 +159,84 @@ def test_cycle_handling(self) -> None:
# Recreate and compare
recreated = GraphNode.from_adjacency_list(adj_list)
assert node1 == recreated
+
+ @pytest.mark.parametrize(
+ "original, clone, expected",
+ [
+ (None, None, False), # None case
+ (GraphNode(1), None, False), # Clone is None
+ ],
+ )
+ def test_is_clone_edge_cases(self, original, clone, expected) -> None:
+ if original is None:
+ # Can't call is_clone on None
+ assert True
+ else:
+ assert original.is_clone(clone) == expected
+
+ def test_is_clone_same_object(self) -> None:
+ # Same object should not be a clone
+ node = GraphNode(1)
+ assert not node.is_clone(node)
+
+ def test_is_clone_different_structure(self) -> None:
+ # Different structures should not be clones
+ node1 = GraphNode(1)
+ node2 = GraphNode(2)
+ assert not node1.is_clone(node2)
+
+ def test_is_clone_proper_clone(self) -> None:
+ # Create original graph: 1-2
+ original1 = GraphNode(1)
+ original2 = GraphNode(2)
+ original1.neighbors = [original2]
+ original2.neighbors = [original1]
+
+ # Create clone: 1-2 (different objects)
+ clone1 = GraphNode(1)
+ clone2 = GraphNode(2)
+ clone1.neighbors = [clone2]
+ clone2.neighbors = [clone1]
+
+ assert original1.is_clone(clone1)
+
+ def test_is_clone_complex_graph(self) -> None:
+ # Create triangle: 1-2-3-1
+ original1 = GraphNode(1)
+ original2 = GraphNode(2)
+ original3 = GraphNode(3)
+ original1.neighbors = [original2, original3]
+ original2.neighbors = [original1, original3]
+ original3.neighbors = [original1, original2]
+
+ # Create clone
+ clone1 = GraphNode(1)
+ clone2 = GraphNode(2)
+ clone3 = GraphNode(3)
+ clone1.neighbors = [clone2, clone3]
+ clone2.neighbors = [clone1, clone3]
+ clone3.neighbors = [clone1, clone2]
+
+ assert original1.is_clone(clone1)
+
+ def test_repr_html(self) -> None:
+ # Test _repr_html_ method
+ node1 = GraphNode(1)
+ node2 = GraphNode(2)
+ node1.neighbors = [node2]
+ node2.neighbors = [node1]
+
+ result = node1._repr_html_()
+ assert isinstance(result, str)
+ # Should return SVG content from graphviz
+
+ def test_from_adjacency_list_invalid_neighbor(self) -> None:
+ # Test adjacency list with invalid neighbor reference
+ adj_list = [[2, 5], [1]] # Node 1 references non-existent node 5
+ result = GraphNode.from_adjacency_list(adj_list)
+ assert result is not None
+ assert result.val == 1
+ # Should only have valid neighbors
+ neighbor_vals = [n.val for n in result.neighbors]
+ assert 2 in neighbor_vals
+ assert 5 not in neighbor_vals # Invalid neighbor should be skipped
diff --git a/tests/data_structures/test_list_node.py b/tests/data_structures/test_list_node.py
index 233e20f..f2e2795 100644
--- a/tests/data_structures/test_list_node.py
+++ b/tests/data_structures/test_list_node.py
@@ -163,3 +163,72 @@ def test_equality_with_cycles(self) -> None:
# Test cyclic vs non-cyclic
linear_node = ListNode.from_list([1, 2])
assert node1 != linear_node
+
+ def test_to_list_with_cycle(self) -> None:
+ # Test to_list breaks on cycle detection
+ node1 = ListNode(1)
+ node2 = ListNode(2)
+ node1.next = node2
+ node2.next = node1 # Create cycle
+
+ result = node1.to_list()
+ assert result == [1, 2] # Should stop at cycle
+
+ def test_str_long_list(self) -> None:
+ # Test long list truncation
+ long_list = list(range(1001)) # More than 1000 items
+ node = ListNode.from_list(long_list)
+ assert node is not None
+ result = str(node)
+ assert "... (long list)" in result
+
+ def test_repr_html_no_graphviz(self, monkeypatch) -> None:
+ # Test _repr_html_ fallback when graphviz not available
+ node = ListNode.from_list([1, 2, 3])
+ assert node is not None
+
+ # Mock ImportError for graphviz
+ def mock_import(name, *args):
+ if name == "graphviz":
+ raise ImportError("No module named 'graphviz'")
+ return __import__(name, *args)
+
+ monkeypatch.setattr("builtins.__import__", mock_import)
+ result = node._repr_html_()
+ assert "
" in result
+ assert "1 -> 2 -> 3" in result
+
+ def test_repr_html_with_graphviz(self) -> None:
+ # Test _repr_html_ with graphviz available
+ node = ListNode.from_list([1, 2])
+ assert node is not None
+
+ try:
+ import importlib.util
+
+ if importlib.util.find_spec("graphviz") is None:
+ pytest.skip("graphviz not available")
+
+ result = node._repr_html_()
+ assert isinstance(result, str)
+ # Should return SVG content
+ except ImportError:
+ pytest.skip("graphviz not available")
+
+ def test_repr_html_with_cycle(self) -> None:
+ # Test _repr_html_ handles cycles
+ node1 = ListNode(1)
+ node2 = ListNode(2)
+ node1.next = node2
+ node2.next = node1 # Create cycle
+
+ try:
+ import importlib.util
+
+ if importlib.util.find_spec("graphviz") is None:
+ pytest.skip("graphviz not available")
+
+ result = node1._repr_html_()
+ assert isinstance(result, str)
+ except ImportError:
+ pytest.skip("graphviz not available")