diff --git a/.amazonq/rules/development-rules.md b/.amazonq/rules/development-rules.md index aae1892..d7f1080 100644 --- a/.amazonq/rules/development-rules.md +++ b/.amazonq/rules/development-rules.md @@ -18,6 +18,20 @@ - Test all: `make test` - Beautiful logging with loguru +### Multiple Solution Classes Pattern + +When implementing multiple approaches (e.g., Solution, SolutionMath), use parametrized testing: + +```python +@pytest.mark.parametrize("solution_class", [Solution, SolutionMath]) +@pytest.mark.parametrize("input_params, expected", test_cases) +def test_method(self, solution_class, input_params, expected): + result = run_helper(solution_class, *input_params) + assert_helper(result, expected) +``` + +This pattern tests all solution approaches with the same test cases, ensuring consistency across implementations. + ## File Structure Each problem has: diff --git a/.amazonq/rules/test-case-enhancement.md b/.amazonq/rules/test-case-enhancement.md index 11b5019..8b341d7 100644 --- a/.amazonq/rules/test-case-enhancement.md +++ b/.amazonq/rules/test-case-enhancement.md @@ -2,7 +2,7 @@ ## Simple Enhancement Workflow -When user requests test case enhancement: +When user requests test case enhancement or **test reproducibility verification**: ### 1. Problem Resolution @@ -69,6 +69,19 @@ make p-gen PROBLEM={problem_name} FORCE=1 make p-lint PROBLEM={problem_name} ``` +## Test Reproducibility Verification + +Use this same workflow when CI tests fail due to reproducibility issues: + +**Process Name**: Test Reproducibility Verification + +**When to Use**: + +- CI test failures in reproducibility checks +- Inconsistent test results between environments +- Missing edge cases causing coverage gaps +- Need to ensure 100% code coverage + ## Success Criteria - All tests pass with enhanced test cases @@ -76,3 +89,4 @@ make p-lint PROBLEM={problem_name} - Original solution code preserved - **Enhanced test cases in final test_solution.py** - JSON template updated for future regeneration +- **100% code coverage including edge cases** diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23a8453..f080465 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: - repo: local hooks: - id: sync-submodules - name: Sync git submodules + name: sync git submodules entry: bash -c 'output=$(git submodule update --init --recursive --remote 2>&1); [ -z "$output" ]' language: system always_run: true diff --git a/.templates/leetcode/json/accounts_merge.json b/.templates/leetcode/json/accounts_merge.json index bcbddb6..ec92c0d 100644 --- a/.templates/leetcode/json/accounts_merge.json +++ b/.templates/leetcode/json/accounts_merge.json @@ -5,47 +5,60 @@ "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.", + "_tags": { "list": ["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": { + "list": [ + { + "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": "", "helpers_imports": "", + "helpers_content": "", "helpers_run_name": "accounts_merge", "helpers_run_signature": "(solution_class: type, accounts: list[list[str]])", "helpers_run_body": " implementation = solution_class()\n return implementation.accounts_merge(accounts)", "helpers_assert_name": "accounts_merge", "helpers_assert_signature": "(result: list[list[str]], expected: list[list[str]]) -> bool", "helpers_assert_body": " # Sort both result and expected 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\n return True", - "solution_methods": [ - { - "name": "accounts_merge", - "signature": "(self, accounts: list[list[str]]) -> list[list[str]]", - "body": " # TODO: Implement accounts_merge\n return []" - } - ], + "solution_imports": "", + "solution_contents": "", + "solution_class_content": "", "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .helpers import assert_accounts_merge, run_accounts_merge\nfrom .solution import Solution", "test_content": "", "test_class_name": "AccountsMerge", "test_class_content": " def setup_method(self):\n self.solution = Solution()", - "test_helper_methods": [], - "test_methods": [ - { - "name": "test_accounts_merge", - "signature": "(self, accounts: list[list[str]], expected: list[list[str]])", - "parametrize": "accounts, expected", - "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\"]]), ([[\"John\", \"john@mail.com\"]], [[\"John\", \"john@mail.com\"]]), ([[\"John\", \"john1@mail.com\"], [\"John\", \"john2@mail.com\"], [\"John\", \"john3@mail.com\"]], [[\"John\", \"john1@mail.com\"], [\"John\", \"john2@mail.com\"], [\"John\", \"john3@mail.com\"]]), ([[\"John\", \"a@mail.com\", \"b@mail.com\"], [\"John\", \"b@mail.com\", \"c@mail.com\"], [\"John\", \"d@mail.com\"]], [[\"John\", \"a@mail.com\", \"b@mail.com\", \"c@mail.com\"], [\"John\", \"d@mail.com\"]]), ([[\"Alice\", \"alice@mail.com\", \"alice1@mail.com\"], [\"Alice\", \"alice2@mail.com\", \"alice3@mail.com\"], [\"Alice\", \"alice1@mail.com\", \"alice2@mail.com\"]], [[\"Alice\", \"alice1@mail.com\", \"alice2@mail.com\", \"alice3@mail.com\", \"alice@mail.com\"]]), ([[\"Bob\", \"bob@mail.com\"], [\"Bob\", \"bob@mail.com\"]], [[\"Bob\", \"bob@mail.com\"]]), ([[\"David\", \"david1@mail.com\", \"david2@mail.com\"], [\"David\", \"david3@mail.com\"], [\"David\", \"david2@mail.com\", \"david4@mail.com\"]], [[\"David\", \"david1@mail.com\", \"david2@mail.com\", \"david4@mail.com\"], [\"David\", \"david3@mail.com\"]]), ([[\"Alex\", \"alex@mail.com\"], [\"Alex\", \"alex@mail.com\", \"alex2@mail.com\"], [\"Alex\", \"alex3@mail.com\"]], [[\"Alex\", \"alex2@mail.com\", \"alex@mail.com\"], [\"Alex\", \"alex3@mail.com\"]]), ([[\"Tom\", \"tom1@mail.com\"], [\"Tom\", \"tom2@mail.com\"], [\"Tom\", \"tom3@mail.com\"], [\"Tom\", \"tom4@mail.com\"]], [[\"Tom\", \"tom1@mail.com\"], [\"Tom\", \"tom2@mail.com\"], [\"Tom\", \"tom3@mail.com\"], [\"Tom\", \"tom4@mail.com\"]]), ([[\"Sam\", \"sam@mail.com\", \"sam1@mail.com\", \"sam2@mail.com\"]], [[\"Sam\", \"sam@mail.com\", \"sam1@mail.com\", \"sam2@mail.com\"]])]", - "body": " result = run_accounts_merge(Solution, accounts)\n assert_accounts_merge(result, expected)" - } - ], + "_solution_methods": { + "list": [ + { + "name": "accounts_merge", + "signature": "(self, accounts: list[list[str]]) -> list[list[str]]", + "body": " # TODO: Implement accounts_merge\n return []" + } + ] + }, + "_test_helper_methods": { + "list": [{ "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }] + }, + "_test_methods": { + "list": [ + { + "name": "test_accounts_merge", + "signature": "(self, accounts: list[list[str]], expected: list[list[str]])", + "parametrize": "accounts, expected", + "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']]), ([['Alice', 'alice@mail.com']], [['Alice', 'alice@mail.com']]), ([['Bob', 'bob1@mail.com'], ['Bob', 'bob2@mail.com']], [['Bob', 'bob1@mail.com'], ['Bob', 'bob2@mail.com']]), ([['Alice', 'alice@mail.com', 'alice2@mail.com'], ['Alice', 'alice2@mail.com', 'alice3@mail.com']], [['Alice', 'alice2@mail.com', 'alice3@mail.com', 'alice@mail.com']]), ([['A', 'a@mail.com', 'b@mail.com'], ['B', 'b@mail.com', 'c@mail.com'], ['C', 'c@mail.com', 'd@mail.com']], [['A', 'a@mail.com', 'b@mail.com', 'c@mail.com', 'd@mail.com']]), ([['David', 'david@mail.com'], ['David', 'david@mail.com']], [['David', 'david@mail.com']]), ([['Alex', 'alex1@mail.com'], ['Bob', 'bob1@mail.com'], ['Charlie', 'charlie1@mail.com']], [['Alex', 'alex1@mail.com'], ['Bob', 'bob1@mail.com'], ['Charlie', 'charlie1@mail.com']]), ([['John', 'john1@mail.com', 'john2@mail.com'], ['John', 'john3@mail.com'], ['Jane', 'jane1@mail.com']], [['John', 'john1@mail.com', 'john2@mail.com'], ['John', 'john3@mail.com'], ['Jane', 'jane1@mail.com']]), ([['User', 'user@mail.com', 'user1@mail.com'], ['User', 'user2@mail.com', 'user@mail.com'], ['User', 'user3@mail.com', 'user1@mail.com']], [['User', 'user1@mail.com', 'user2@mail.com', 'user3@mail.com', 'user@mail.com']]), ([['Test', 'test1@mail.com'], ['Test', 'test2@mail.com'], ['Test', 'test1@mail.com', 'test3@mail.com']], [['Test', 'test2@mail.com'], ['Test', 'test1@mail.com', 'test3@mail.com']]), ([['Name', 'a@mail.com', 'b@mail.com', 'c@mail.com'], ['Name', 'd@mail.com', 'e@mail.com'], ['Name', 'c@mail.com', 'f@mail.com']], [['Name', 'd@mail.com', 'e@mail.com'], ['Name', 'a@mail.com', 'b@mail.com', 'c@mail.com', 'f@mail.com']])]", + "body": " result = run_accounts_merge(Solution, accounts)\n assert_accounts_merge(result, expected)" + } + ] + }, "playground_imports": "from helpers import run_accounts_merge, assert_accounts_merge\nfrom solution import Solution", - "playground_setup": "# 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_setup": "# 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_run": "result = run_accounts_merge(Solution, accounts)\nresult", "playground_assert": "assert_accounts_merge(result, expected)" } diff --git a/.templates/leetcode/json/construct_binary_tree_from_preorder_and_inorder_traversal.json b/.templates/leetcode/json/construct_binary_tree_from_preorder_and_inorder_traversal.json new file mode 100644 index 0000000..e7e7716 --- /dev/null +++ b/.templates/leetcode/json/construct_binary_tree_from_preorder_and_inorder_traversal.json @@ -0,0 +1,62 @@ +{ + "problem_name": "construct_binary_tree_from_preorder_and_inorder_traversal", + "solution_class_name": "Solution", + "problem_number": "105", + "problem_title": "Construct Binary Tree from Preorder and Inorder Traversal", + "difficulty": "Medium", + "topics": "Array, Hash Table, Divide and Conquer, Tree, Binary Tree", + "_tags": { "list": ["grind-75"] }, + "readme_description": "Given two integer arrays `preorder` and `inorder` where `preorder` is the preorder traversal of a binary tree and `inorder` is the inorder traversal of the same tree, construct and return the binary tree.", + "_readme_examples": { + "list": [ + { + "content": "![Example 1](https://assets.leetcode.com/uploads/2021/02/19/tree.jpg)\n\n```\nInput: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]\nOutput: [3,9,20,null,null,15,7]\n```" + }, + { "content": "```\nInput: preorder = [-1], inorder = [-1]\nOutput: [-1]\n```" } + ] + }, + "readme_constraints": "- 1 <= preorder.length <= 3000\n- inorder.length == preorder.length\n- -3000 <= preorder[i], inorder[i] <= 3000\n- preorder and inorder consist of unique values.\n- Each value of inorder also appears in preorder.\n- preorder is guaranteed to be the preorder traversal of the tree.\n- inorder is guaranteed to be the inorder traversal of the tree.", + "readme_additional": "", + "helpers_imports": "from leetcode_py import TreeNode", + "helpers_content": "", + "helpers_run_name": "build_tree", + "helpers_run_signature": "(solution_class: type, preorder: list[int], inorder: list[int])", + "helpers_run_body": " implementation = solution_class()\n return implementation.build_tree(preorder, inorder)", + "helpers_assert_name": "build_tree", + "helpers_assert_signature": "(result: TreeNode | None, expected_list: list[int | None]) -> bool", + "helpers_assert_body": " expected = TreeNode.from_list(expected_list) if expected_list else None\n assert result == expected\n return True", + "solution_imports": "from leetcode_py import TreeNode", + "solution_contents": "", + "solution_class_content": "", + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .helpers import assert_build_tree, run_build_tree\nfrom .solution import Solution", + "test_content": "", + "test_class_name": "ConstructBinaryTreeFromPreorderAndInorderTraversal", + "test_class_content": " def setup_method(self):\n self.solution = Solution()", + "_solution_methods": { + "list": [ + { + "name": "build_tree", + "signature": "(self, preorder: list[int], inorder: list[int]) -> TreeNode | None", + "body": " # TODO: Implement build_tree\n return None" + } + ] + }, + "_test_helper_methods": { + "list": [{ "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }] + }, + "_test_methods": { + "list": [ + { + "name": "test_build_tree", + "signature": "(self, preorder: list[int], inorder: list[int], expected_list: list[int | None])", + "parametrize": "preorder, inorder, expected_list", + "test_cases": "[([], [], []), ([1], [1], [1]), ([3, 9, 20, 15, 7], [9, 3, 15, 20, 7], [3, 9, 20, None, None, 15, 7]), ([-1], [-1], [-1]), ([1, 2], [2, 1], [1, 2]), ([1, 2], [1, 2], [1, None, 2]), ([1, 2, 3], [2, 1, 3], [1, 2, 3]), ([1, 2, 4, 5, 3, 6], [4, 2, 5, 1, 6, 3], [1, 2, 3, 4, 5, 6]), ([1, 2, 3, 4], [1, 2, 3, 4], [1, None, 2, None, 3, None, 4]), ([4, 3, 2, 1], [1, 2, 3, 4], [4, 3, None, 2, None, 1]), ([10, 5, 1, 7, 40, 50], [1, 5, 7, 10, 40, 50], [10, 5, 40, 1, 7, None, 50]), ([1, 3, 2], [1, 2, 3], [1, None, 3, 2]), ([2, 1, 3], [1, 2, 3], [2, 1, 3]), ([5, 3, 2, 1, 4, 6, 7], [1, 2, 3, 4, 5, 6, 7], [5, 3, 6, 2, 4, None, 7, 1]), ([7, 3, 2, 1, 5, 4, 6, 10, 9, 11], [1, 2, 3, 4, 5, 6, 7, 9, 10, 11], [7, 3, 10, 2, 5, 9, 11, 1, None, 4, 6]), ([-3000, -2999, -2998], [-2998, -2999, -3000], [-3000, -2999, None, -2998])]", + "body": " result = run_build_tree(Solution, preorder, inorder)\n assert_build_tree(result, expected_list)" + } + ] + }, + "playground_imports": "from helpers import run_build_tree, assert_build_tree\nfrom solution import Solution\nfrom leetcode_py import TreeNode", + "playground_setup": "# Example test case\npreorder = [3, 9, 20, 15, 7]\ninorder = [9, 3, 15, 20, 7]\nexpected_list = [3, 9, 20, None, None, 15, 7]", + "playground_run": "result = run_build_tree(Solution, preorder, inorder)\nresult", + "playground_assert": "assert_build_tree(result, expected_list)" +} diff --git a/.templates/leetcode/json/diagonal_traverse.json b/.templates/leetcode/json/diagonal_traverse.json new file mode 100644 index 0000000..df17575 --- /dev/null +++ b/.templates/leetcode/json/diagonal_traverse.json @@ -0,0 +1,62 @@ +{ + "problem_name": "diagonal_traverse", + "solution_class_name": "Solution", + "problem_number": "498", + "problem_title": "Diagonal Traverse", + "difficulty": "Medium", + "topics": "Array, Matrix, Simulation", + "_tags": { "list": [] }, + "readme_description": "Given an `m x n` matrix `mat`, return *an array of all the elements of the array in a diagonal order*.", + "_readme_examples": { + "list": [ + { + "content": "![Diagonal Traverse](https://assets.leetcode.com/uploads/2021/04/10/diag1-grid.jpg)\n\n```\nInput: mat = [[1,2,3],[4,5,6],[7,8,9]]\nOutput: [1,2,4,7,5,3,6,8,9]\n```" + }, + { "content": "```\nInput: mat = [[1,2],[3,4]]\nOutput: [1,2,3,4]\n```" } + ] + }, + "readme_constraints": "- `m == mat.length`\n- `n == mat[i].length`\n- `1 <= m, n <= 10^4`\n- `1 <= m * n <= 10^4`\n- `-10^5 <= mat[i][j] <= 10^5`", + "readme_additional": "", + "helpers_imports": "", + "helpers_content": "", + "helpers_run_name": "find_diagonal_order", + "helpers_run_signature": "(solution_class: type, mat: list[list[int]])", + "helpers_run_body": " implementation = solution_class()\n return implementation.find_diagonal_order(mat)", + "helpers_assert_name": "find_diagonal_order", + "helpers_assert_signature": "(result: list[int], expected: list[int]) -> bool", + "helpers_assert_body": " assert result == expected\n return True", + "solution_imports": "", + "solution_contents": "", + "solution_class_content": "", + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .helpers import assert_find_diagonal_order, run_find_diagonal_order\nfrom .solution import Solution", + "test_content": "", + "test_class_name": "DiagonalTraverse", + "test_class_content": " def setup_method(self):\n self.solution = Solution()", + "_solution_methods": { + "list": [ + { + "name": "find_diagonal_order", + "signature": "(self, mat: list[list[int]]) -> list[int]", + "body": " # TODO: Implement find_diagonal_order\n return []" + } + ] + }, + "_test_helper_methods": { + "list": [{ "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }] + }, + "_test_methods": { + "list": [ + { + "name": "test_find_diagonal_order", + "signature": "(self, mat: list[list[int]], expected: list[int])", + "parametrize": "mat, expected", + "test_cases": "[([[1, 2, 3], [4, 5, 6], [7, 8, 9]], [1, 2, 4, 7, 5, 3, 6, 8, 9]), ([[1, 2], [3, 4]], [1, 2, 3, 4]), ([[1]], [1]), ([[1, 2, 3]], [1, 2, 3]), ([[1], [2], [3]], [1, 2, 3]), ([[1, 2, 3, 4], [5, 6, 7, 8]], [1, 2, 5, 6, 3, 4, 7, 8]), ([[1, 2], [3, 4], [5, 6]], [1, 2, 3, 5, 4, 6]), ([[1, 2, 3, 4, 5]], [1, 2, 3, 4, 5]), ([[1], [2], [3], [4], [5]], [1, 2, 3, 4, 5]), ([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]], [1, 2, 5, 9, 6, 3, 4, 7, 10, 11, 8, 12]), ([[-1, 0, 1], [2, -3, 4]], [-1, 0, 2, -3, 1, 4]), ([[100]], [100])]", + "body": " result = run_find_diagonal_order(Solution, mat)\n assert_find_diagonal_order(result, expected)" + } + ] + }, + "playground_imports": "from helpers import run_find_diagonal_order, assert_find_diagonal_order\nfrom solution import Solution", + "playground_setup": "# Example test case\nmat = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]\nexpected = [1, 2, 4, 7, 5, 3, 6, 8, 9]", + "playground_run": "result = run_find_diagonal_order(Solution, mat)\nresult", + "playground_assert": "assert_find_diagonal_order(result, expected)" +} diff --git a/.templates/leetcode/json/find_all_anagrams_in_a_string.json b/.templates/leetcode/json/find_all_anagrams_in_a_string.json new file mode 100644 index 0000000..fa9cd1a --- /dev/null +++ b/.templates/leetcode/json/find_all_anagrams_in_a_string.json @@ -0,0 +1,64 @@ +{ + "problem_name": "find_all_anagrams_in_a_string", + "solution_class_name": "Solution", + "problem_number": "438", + "problem_title": "Find All Anagrams in a String", + "difficulty": "Medium", + "topics": "Hash Table, String, Sliding Window", + "_tags": { "list": ["grind-75"] }, + "readme_description": "Given two strings `s` and `p`, return an array of all the start indices of `p`'s anagrams in `s`. You may return the answer in any order.\n\nAn **anagram** is a word or phrase formed by rearranging the letters of a different word or phrase, typically using all the original letters exactly once.", + "_readme_examples": { + "list": [ + { + "content": "```\nInput: s = \"cbaebabacd\", p = \"abc\"\nOutput: [0,6]\n```\n**Explanation:**\nThe substring with start index = 0 is \"cba\", which is an anagram of \"abc\".\nThe substring with start index = 6 is \"bac\", which is an anagram of \"abc\"." + }, + { + "content": "```\nInput: s = \"abab\", p = \"ab\"\nOutput: [0,1,2]\n```\n**Explanation:**\nThe substring with start index = 0 is \"ab\", which is an anagram of \"ab\".\nThe substring with start index = 1 is \"ba\", which is an anagram of \"ab\".\nThe substring with start index = 2 is \"ab\", which is an anagram of \"ab\"." + } + ] + }, + "readme_constraints": "- 1 <= s.length, p.length <= 3 * 10^4\n- s and p consist of lowercase English letters.", + "readme_additional": "", + "helpers_imports": "", + "helpers_content": "", + "helpers_run_name": "find_anagrams", + "helpers_run_signature": "(solution_class: type, s: str, p: str)", + "helpers_run_body": " implementation = solution_class()\n return implementation.find_anagrams(s, p)", + "helpers_assert_name": "find_anagrams", + "helpers_assert_signature": "(result: list[int], expected: list[int]) -> bool", + "helpers_assert_body": " assert sorted(result) == sorted(expected)\n return True", + "solution_imports": "", + "solution_contents": "", + "solution_class_content": "", + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .helpers import assert_find_anagrams, run_find_anagrams\nfrom .solution import Solution", + "test_content": "", + "test_class_name": "FindAllAnagramsInAString", + "test_class_content": " def setup_method(self):\n self.solution = Solution()", + "_solution_methods": { + "list": [ + { + "name": "find_anagrams", + "signature": "(self, s: str, p: str) -> list[int]", + "body": " # TODO: Implement find_anagrams\n return []" + } + ] + }, + "_test_helper_methods": { + "list": [{ "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }] + }, + "_test_methods": { + "list": [ + { + "name": "test_find_anagrams", + "signature": "(self, s: str, p: str, expected: list[int])", + "parametrize": "s, p, expected", + "test_cases": "[('cbaebabacd', 'abc', [0, 6]), ('abab', 'ab', [0, 1, 2]), ('a', 'aa', []), ('aa', 'aa', [0]), ('abcdefg', 'xyz', []), ('aab', 'ab', [1]), ('aaab', 'ab', [2]), ('baa', 'aa', [1]), ('abacabad', 'aaab', []), ('ababacb', 'abc', [3, 4]), ('abaacbabc', 'abc', [3, 4, 6]), ('abab', 'abab', [0])]", + "body": " result = run_find_anagrams(Solution, s, p)\n assert_find_anagrams(result, expected)" + } + ] + }, + "playground_imports": "from helpers import run_find_anagrams, assert_find_anagrams\nfrom solution import Solution", + "playground_setup": "# Example test case\ns = 'cbaebabacd'\np = 'abc'\nexpected = [0, 6]", + "playground_run": "result = run_find_anagrams(Solution, s, p)\nresult", + "playground_assert": "assert_find_anagrams(result, expected)" +} diff --git a/.templates/leetcode/json/letter_combinations_of_a_phone_number.json b/.templates/leetcode/json/letter_combinations_of_a_phone_number.json new file mode 100644 index 0000000..990afaf --- /dev/null +++ b/.templates/leetcode/json/letter_combinations_of_a_phone_number.json @@ -0,0 +1,63 @@ +{ + "problem_name": "letter_combinations_of_a_phone_number", + "solution_class_name": "Solution", + "problem_number": "17", + "problem_title": "Letter Combinations of a Phone Number", + "difficulty": "Medium", + "topics": "Hash Table, String, Backtracking", + "_tags": { "list": ["grind-75"] }, + "readme_description": "Given a string containing digits from `2-9` inclusive, return all possible letter combinations that the number could represent. Return the answer in **any order**.\n\nA mapping of digits to letters (just like on the telephone buttons) is given below. Note that 1 does not map to any letters.", + "_readme_examples": { + "list": [ + { + "content": "![Phone Keypad](https://assets.leetcode.com/uploads/2022/03/15/1200px-telephone-keypad2svg.png)\n\n```\nInput: digits = \"23\"\nOutput: [\"ad\",\"ae\",\"af\",\"bd\",\"be\",\"bf\",\"cd\",\"ce\",\"cf\"]\n```" + }, + { "content": "```\nInput: digits = \"\"\nOutput: []\n```" }, + { "content": "```\nInput: digits = \"2\"\nOutput: [\"a\",\"b\",\"c\"]\n```" } + ] + }, + "readme_constraints": "- `0 <= digits.length <= 4`\n- `digits[i]` is a digit in the range `['2', '9']`.", + "readme_additional": "", + "helpers_imports": "", + "helpers_content": "", + "helpers_run_name": "letter_combinations", + "helpers_run_signature": "(solution_class: type, digits: str)", + "helpers_run_body": " implementation = solution_class()\n return implementation.letter_combinations(digits)", + "helpers_assert_name": "letter_combinations", + "helpers_assert_signature": "(result: list[str], expected: list[str]) -> bool", + "helpers_assert_body": " result.sort()\n expected.sort()\n assert result == expected\n return True", + "solution_imports": "", + "solution_contents": "", + "solution_class_content": "", + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .helpers import assert_letter_combinations, run_letter_combinations\nfrom .solution import Solution", + "test_content": "", + "test_class_name": "LetterCombinationsOfAPhoneNumber", + "test_class_content": " def setup_method(self):\n self.solution = Solution()", + "_solution_methods": { + "list": [ + { + "name": "letter_combinations", + "signature": "(self, digits: str) -> list[str]", + "body": " # TODO: Implement letter_combinations\n return []" + } + ] + }, + "_test_helper_methods": { + "list": [{ "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }] + }, + "_test_methods": { + "list": [ + { + "name": "test_letter_combinations", + "signature": "(self, digits: str, expected: list[str])", + "parametrize": "digits, expected", + "test_cases": "[('23', ['ad', 'ae', 'af', 'bd', 'be', 'bf', 'cd', 'ce', 'cf']), ('', []), ('2', ['a', 'b', 'c']), ('234', ['adg', 'adh', 'adi', 'aeg', 'aeh', 'aei', 'afg', 'afh', 'afi', 'bdg', 'bdh', 'bdi', 'beg', 'beh', 'bei', 'bfg', 'bfh', 'bfi', 'cdg', 'cdh', 'cdi', 'ceg', 'ceh', 'cei', 'cfg', 'cfh', 'cfi']), ('7', ['p', 'q', 'r', 's']), ('9', ['w', 'x', 'y', 'z']), ('79', ['pw', 'px', 'py', 'pz', 'qw', 'qx', 'qy', 'qz', 'rw', 'rx', 'ry', 'rz', 'sw', 'sx', 'sy', 'sz']), ('22', ['aa', 'ab', 'ac', 'ba', 'bb', 'bc', 'ca', 'cb', 'cc']), ('3456', ['dgjm', 'dgjn', 'dgjo', 'dgkm', 'dgkn', 'dgko', 'dglm', 'dgln', 'dglo', 'dhjm', 'dhjn', 'dhjo', 'dhkm', 'dhkn', 'dhko', 'dhlm', 'dhln', 'dhlo', 'dijm', 'dijn', 'dijo', 'dikm', 'dikn', 'diko', 'dilm', 'diln', 'dilo', 'egjm', 'egjn', 'egjo', 'egkm', 'egkn', 'egko', 'eglm', 'egln', 'eglo', 'ehjm', 'ehjn', 'ehjo', 'ehkm', 'ehkn', 'ehko', 'ehlm', 'ehln', 'ehlo', 'eijm', 'eijn', 'eijo', 'eikm', 'eikn', 'eiko', 'eilm', 'eiln', 'eilo', 'fgjm', 'fgjn', 'fgjo', 'fgkm', 'fgkn', 'fgko', 'fglm', 'fgln', 'fglo', 'fhjm', 'fhjn', 'fhjo', 'fhkm', 'fhkn', 'fhko', 'fhlm', 'fhln', 'fhlo', 'fijm', 'fijn', 'fijo', 'fikm', 'fikn', 'fiko', 'film', 'filn', 'filo']), ('25', ['aj', 'ak', 'al', 'bj', 'bk', 'bl', 'cj', 'ck', 'cl']), ('78', ['pt', 'pu', 'pv', 'qt', 'qu', 'qv', 'rt', 'ru', 'rv', 'st', 'su', 'sv']), ('89', ['tw', 'tx', 'ty', 'tz', 'uw', 'ux', 'uy', 'uz', 'vw', 'vx', 'vy', 'vz'])]", + "body": " result = run_letter_combinations(Solution, digits)\n assert_letter_combinations(result, expected)" + } + ] + }, + "playground_imports": "from helpers import run_letter_combinations, assert_letter_combinations\nfrom solution import Solution", + "playground_setup": "# Example test case\ndigits = '23'\nexpected = ['ad', 'ae', 'af', 'bd', 'be', 'bf', 'cd', 'ce', 'cf']", + "playground_run": "result = run_letter_combinations(Solution, digits)\nresult", + "playground_assert": "assert_letter_combinations(result, expected)" +} diff --git a/.templates/leetcode/json/subsets.json b/.templates/leetcode/json/subsets.json new file mode 100644 index 0000000..a30f10a --- /dev/null +++ b/.templates/leetcode/json/subsets.json @@ -0,0 +1,62 @@ +{ + "problem_name": "subsets", + "solution_class_name": "Solution", + "problem_number": "78", + "problem_title": "Subsets", + "difficulty": "Medium", + "topics": "Array, Backtracking, Bit Manipulation", + "_tags": { "list": ["grind-75"] }, + "readme_description": "Given an integer array `nums` of **unique** elements, return *all possible* subsets (the power set).\n\nThe solution set **must not** contain duplicate subsets. Return the solution in **any order**.", + "_readme_examples": { + "list": [ + { + "content": "```\nInput: nums = [1,2,3]\nOutput: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]\n```" + }, + { "content": "```\nInput: nums = [0]\nOutput: [[],[0]]\n```" } + ] + }, + "readme_constraints": "- 1 <= nums.length <= 10\n- -10 <= nums[i] <= 10\n- All the numbers of nums are unique.", + "readme_additional": "", + "helpers_imports": "", + "helpers_content": "", + "helpers_run_name": "subsets", + "helpers_run_signature": "(solution_class: type, nums: list[int])", + "helpers_run_body": " implementation = solution_class()\n return implementation.subsets(nums)", + "helpers_assert_name": "subsets", + "helpers_assert_signature": "(result: list[list[int]], expected: list[list[int]]) -> bool", + "helpers_assert_body": " # Sort both result and expected for comparison since order doesn't matter\n result_sorted = [sorted(subset) for subset in sorted(result)]\n expected_sorted = [sorted(subset) for subset in sorted(expected)]\n assert result_sorted == expected_sorted\n return True", + "solution_imports": "", + "solution_contents": "", + "solution_class_content": "", + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .helpers import assert_subsets, run_subsets\nfrom .solution import Solution", + "test_content": "", + "test_class_name": "Subsets", + "test_class_content": " def setup_method(self):\n self.solution = Solution()", + "_solution_methods": { + "list": [ + { + "name": "subsets", + "signature": "(self, nums: list[int]) -> list[list[int]]", + "body": " # TODO: Implement subsets\n return []" + } + ] + }, + "_test_helper_methods": { + "list": [{ "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }] + }, + "_test_methods": { + "list": [ + { + "name": "test_subsets", + "signature": "(self, nums: list[int], expected: list[list[int]])", + "parametrize": "nums, expected", + "test_cases": "[([1, 2, 3], [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]), ([0], [[], [0]]), ([1], [[], [1]]), ([1, 2], [[], [1], [2], [1, 2]]), ([4, 1, 0], [[], [4], [4, 1], [4, 1, 0], [4, 0], [1], [1, 0], [0]]), ([-1, 0, 1], [[], [-1], [-1, 0], [-1, 0, 1], [-1, 1], [0], [0, 1], [1]]), ([5], [[], [5]]), ([2, 1, 3], [[], [2], [2, 1], [2, 1, 3], [2, 3], [1], [1, 3], [3]]), ([10], [[], [10]]), ([-10, 10], [[], [-10], [-10, 10], [10]]), ([1, 2, 3, 4], [[], [1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 4], [1, 3], [1, 3, 4], [1, 4], [2], [2, 3], [2, 3, 4], [2, 4], [3], [3, 4], [4]]), ([5, 2], [[], [5], [5, 2], [2]])]", + "body": " result = run_subsets(Solution, nums)\n assert_subsets(result, expected)" + } + ] + }, + "playground_imports": "from helpers import run_subsets, assert_subsets\nfrom solution import Solution", + "playground_setup": "# Example test case\nnums = [1, 2, 3]\nexpected = [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]", + "playground_run": "result = run_subsets(Solution, nums)\nresult", + "playground_assert": "assert_subsets(result, expected)" +} diff --git a/.templates/leetcode/json/unique_paths.json b/.templates/leetcode/json/unique_paths.json new file mode 100644 index 0000000..001900d --- /dev/null +++ b/.templates/leetcode/json/unique_paths.json @@ -0,0 +1,64 @@ +{ + "problem_name": "unique_paths", + "solution_class_name": "Solution", + "problem_number": "62", + "problem_title": "Unique Paths", + "difficulty": "Medium", + "topics": "Math, Dynamic Programming, Combinatorics", + "_tags": { "list": ["grind-75"] }, + "readme_description": "There is a robot on an `m x n` grid. The robot is initially located at the **top-left corner** (i.e., `grid[0][0]`). The robot tries to move to the **bottom-right corner** (i.e., `grid[m - 1][n - 1]`). The robot can only move either down or right at any point in time.\n\nGiven the two integers `m` and `n`, return *the number of possible unique paths that the robot can take to reach the bottom-right corner*.\n\nThe test cases are generated so that the answer will be less than or equal to `2 * 10^9`.", + "_readme_examples": { + "list": [ + { + "content": "![Example 1](https://assets.leetcode.com/uploads/2018/10/22/robot_maze.png)\n\n```\nInput: m = 3, n = 7\nOutput: 28\n```" + }, + { + "content": "```\nInput: m = 3, n = 2\nOutput: 3\n```\n**Explanation:** From the top-left corner, there are a total of 3 ways to reach the bottom-right corner:\n1. Right -> Down -> Down\n2. Down -> Down -> Right\n3. Down -> Right -> Down" + } + ] + }, + "readme_constraints": "- 1 <= m, n <= 100", + "readme_additional": "", + "helpers_imports": "", + "helpers_content": "", + "helpers_run_name": "unique_paths", + "helpers_run_signature": "(solution_class: type, m: int, n: int)", + "helpers_run_body": " implementation = solution_class()\n return implementation.unique_paths(m, n)", + "helpers_assert_name": "unique_paths", + "helpers_assert_signature": "(result: int, expected: int) -> bool", + "helpers_assert_body": " assert result == expected\n return True", + "solution_imports": "", + "solution_contents": "", + "solution_class_content": "", + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .helpers import assert_unique_paths, run_unique_paths\nfrom .solution import Solution", + "test_content": "", + "test_class_name": "UniquePaths", + "test_class_content": " def setup_method(self):\n self.solution = Solution()", + "_solution_methods": { + "list": [ + { + "name": "unique_paths", + "signature": "(self, m: int, n: int) -> int", + "body": " # TODO: Implement unique_paths\n return 0" + } + ] + }, + "_test_helper_methods": { + "list": [{ "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }] + }, + "_test_methods": { + "list": [ + { + "name": "test_unique_paths", + "signature": "(self, m: int, n: int, expected: int)", + "parametrize": "m, n, expected", + "test_cases": "[(3, 7, 28), (3, 2, 3), (1, 1, 1), (1, 10, 1), (10, 1, 1), (2, 2, 2), (3, 3, 6), (4, 4, 20), (5, 5, 70), (2, 3, 3), (3, 4, 10), (4, 5, 35), (6, 3, 21), (7, 3, 28), (10, 10, 48620)]", + "body": " result = run_unique_paths(Solution, m, n)\n assert_unique_paths(result, expected)" + } + ] + }, + "playground_imports": "from helpers import run_unique_paths, assert_unique_paths\nfrom solution import Solution", + "playground_setup": "# Example test case\nm = 3\nn = 7\nexpected = 28", + "playground_run": "result = run_unique_paths(Solution, m, n)\nresult", + "playground_assert": "assert_unique_paths(result, expected)" +} diff --git a/.templates/leetcode/json/word_search.json b/.templates/leetcode/json/word_search.json new file mode 100644 index 0000000..776a490 --- /dev/null +++ b/.templates/leetcode/json/word_search.json @@ -0,0 +1,67 @@ +{ + "problem_name": "word_search", + "solution_class_name": "Solution", + "problem_number": "79", + "problem_title": "Word Search", + "difficulty": "Medium", + "topics": "Array, String, Backtracking, Depth-First Search, Matrix", + "_tags": { "list": ["grind-75"] }, + "readme_description": "Given an `m x n` grid of characters `board` and a string `word`, return `true` *if* `word` *exists in the grid*.\n\nThe word can be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically neighboring. The same letter cell may not be used more than once.", + "_readme_examples": { + "list": [ + { + "content": "![Word Search Example 1](https://assets.leetcode.com/uploads/2020/11/04/word2.jpg)\n\n```\nInput: board = [[\"A\",\"B\",\"C\",\"E\"],[\"S\",\"F\",\"C\",\"S\"],[\"A\",\"D\",\"E\",\"E\"]], word = \"ABCCED\"\nOutput: true\n```" + }, + { + "content": "![Word Search Example 2](https://assets.leetcode.com/uploads/2020/11/04/word-1.jpg)\n\n```\nInput: board = [[\"A\",\"B\",\"C\",\"E\"],[\"S\",\"F\",\"C\",\"S\"],[\"A\",\"D\",\"E\",\"E\"]], word = \"SEE\"\nOutput: true\n```" + }, + { + "content": "![Word Search Example 3](https://assets.leetcode.com/uploads/2020/10/15/word3.jpg)\n\n```\nInput: board = [[\"A\",\"B\",\"C\",\"E\"],[\"S\",\"F\",\"C\",\"S\"],[\"A\",\"D\",\"E\",\"E\"]], word = \"ABCB\"\nOutput: false\n```" + } + ] + }, + "readme_constraints": "- `m == board.length`\n- `n = board[i].length`\n- `1 <= m, n <= 6`\n- `1 <= word.length <= 15`\n- `board` and `word` consists of only lowercase and uppercase English letters.", + "readme_additional": "**Follow up:** Could you use search pruning to make your solution faster with a larger `board`?", + "helpers_imports": "", + "helpers_content": "", + "helpers_run_name": "exist", + "helpers_run_signature": "(solution_class: type, board: list[list[str]], word: str)", + "helpers_run_body": " implementation = solution_class()\n return implementation.exist(board, word)", + "helpers_assert_name": "exist", + "helpers_assert_signature": "(result: bool, expected: bool) -> bool", + "helpers_assert_body": " assert result == expected\n return True", + "solution_imports": "", + "solution_contents": "", + "solution_class_content": "", + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .helpers import assert_exist, run_exist\nfrom .solution import Solution", + "test_content": "", + "test_class_name": "WordSearch", + "test_class_content": " def setup_method(self):\n self.solution = Solution()", + "_solution_methods": { + "list": [ + { + "name": "exist", + "signature": "(self, board: list[list[str]], word: str) -> bool", + "body": " # TODO: Implement exist\n return False" + } + ] + }, + "_test_helper_methods": { + "list": [{ "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }] + }, + "_test_methods": { + "list": [ + { + "name": "test_exist", + "signature": "(self, board: list[list[str]], word: str, expected: bool)", + "parametrize": "board, word, expected", + "test_cases": "[([['A', 'B', 'C', 'E'], ['S', 'F', 'C', 'S'], ['A', 'D', 'E', 'E']], 'ABCCED', True), ([['A', 'B', 'C', 'E'], ['S', 'F', 'C', 'S'], ['A', 'D', 'E', 'E']], 'SEE', True), ([['A', 'B', 'C', 'E'], ['S', 'F', 'C', 'S'], ['A', 'D', 'E', 'E']], 'ABCB', False), ([['A']], 'A', True), ([['A']], 'B', False), ([['A', 'B'], ['C', 'D']], 'ACDB', True), ([['A', 'B'], ['C', 'D']], 'ABDC', True), ([['A', 'B'], ['C', 'D']], 'ABCD', False), ([['C', 'A', 'A'], ['A', 'A', 'A'], ['B', 'C', 'D']], 'AAB', True), ([['A', 'B', 'C', 'E'], ['S', 'F', 'C', 'S'], ['A', 'D', 'E', 'E']], 'ABCESEEEFS', False), ([['A', 'A', 'A', 'A', 'A', 'A'], ['A', 'A', 'A', 'A', 'A', 'A'], ['A', 'A', 'A', 'A', 'A', 'A'], ['A', 'A', 'A', 'A', 'A', 'A'], ['A', 'A', 'A', 'A', 'A', 'A'], ['A', 'A', 'A', 'A', 'A', 'A']], 'AAAAAAAAAAAAAAB', False), ([['A', 'B', 'C', 'E'], ['S', 'F', 'C', 'S'], ['A', 'D', 'E', 'E']], 'SFCS', True)]", + "body": " result = run_exist(Solution, board, word)\n assert_exist(result, expected)" + } + ] + }, + "playground_imports": "from helpers import run_exist, assert_exist\nfrom solution import Solution", + "playground_setup": "# Example test case\nboard = [['A', 'B', 'C', 'E'], ['S', 'F', 'C', 'S'], ['A', 'D', 'E', 'E']]\nword = 'ABCCED'\nexpected = True", + "playground_run": "result = run_exist(Solution, board, word)\nresult", + "playground_assert": "assert_exist(result, expected)" +} diff --git a/Makefile b/Makefile index 377ff6d..c6eeb6a 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= two_sum +PROBLEM ?= find_all_anagrams_in_a_string FORCE ?= 0 COMMA := , diff --git a/leetcode/accounts_merge/README.md b/leetcode/accounts_merge/README.md index 53a64a3..dd3f61e 100644 --- a/leetcode/accounts_merge/README.md +++ b/leetcode/accounts_merge/README.md @@ -8,11 +8,11 @@ ## 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. +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**. +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 @@ -34,8 +34,8 @@ Output: [["Ethan","Ethan0@m.co","Ethan4@m.co","Ethan5@m.co"],["Gabe","Gabe0@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. +- 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/test_solution.py b/leetcode/accounts_merge/test_solution.py index fc73296..8f18c66 100644 --- a/leetcode/accounts_merge/test_solution.py +++ b/leetcode/accounts_merge/test_solution.py @@ -43,64 +43,69 @@ def setup_method(self): ["Fern", "Fern0@m.co", "Fern1@m.co", "Fern5@m.co"], ], ), - ([["John", "john@mail.com"]], [["John", "john@mail.com"]]), + ([["Alice", "alice@mail.com"]], [["Alice", "alice@mail.com"]]), ( - [["John", "john1@mail.com"], ["John", "john2@mail.com"], ["John", "john3@mail.com"]], - [["John", "john1@mail.com"], ["John", "john2@mail.com"], ["John", "john3@mail.com"]], + [["Bob", "bob1@mail.com"], ["Bob", "bob2@mail.com"]], + [["Bob", "bob1@mail.com"], ["Bob", "bob2@mail.com"]], ), ( [ - ["John", "a@mail.com", "b@mail.com"], - ["John", "b@mail.com", "c@mail.com"], - ["John", "d@mail.com"], + ["Alice", "alice@mail.com", "alice2@mail.com"], + ["Alice", "alice2@mail.com", "alice3@mail.com"], ], - [["John", "a@mail.com", "b@mail.com", "c@mail.com"], ["John", "d@mail.com"]], + [["Alice", "alice2@mail.com", "alice3@mail.com", "alice@mail.com"]], ), ( [ - ["Alice", "alice@mail.com", "alice1@mail.com"], - ["Alice", "alice2@mail.com", "alice3@mail.com"], - ["Alice", "alice1@mail.com", "alice2@mail.com"], + ["A", "a@mail.com", "b@mail.com"], + ["B", "b@mail.com", "c@mail.com"], + ["C", "c@mail.com", "d@mail.com"], ], - [["Alice", "alice1@mail.com", "alice2@mail.com", "alice3@mail.com", "alice@mail.com"]], + [["A", "a@mail.com", "b@mail.com", "c@mail.com", "d@mail.com"]], + ), + ([["David", "david@mail.com"], ["David", "david@mail.com"]], [["David", "david@mail.com"]]), + ( + [["Alex", "alex1@mail.com"], ["Bob", "bob1@mail.com"], ["Charlie", "charlie1@mail.com"]], + [["Alex", "alex1@mail.com"], ["Bob", "bob1@mail.com"], ["Charlie", "charlie1@mail.com"]], ), - ([["Bob", "bob@mail.com"], ["Bob", "bob@mail.com"]], [["Bob", "bob@mail.com"]]), ( [ - ["David", "david1@mail.com", "david2@mail.com"], - ["David", "david3@mail.com"], - ["David", "david2@mail.com", "david4@mail.com"], + ["John", "john1@mail.com", "john2@mail.com"], + ["John", "john3@mail.com"], + ["Jane", "jane1@mail.com"], ], [ - ["David", "david1@mail.com", "david2@mail.com", "david4@mail.com"], - ["David", "david3@mail.com"], + ["John", "john1@mail.com", "john2@mail.com"], + ["John", "john3@mail.com"], + ["Jane", "jane1@mail.com"], ], ), ( [ - ["Alex", "alex@mail.com"], - ["Alex", "alex@mail.com", "alex2@mail.com"], - ["Alex", "alex3@mail.com"], + ["User", "user@mail.com", "user1@mail.com"], + ["User", "user2@mail.com", "user@mail.com"], + ["User", "user3@mail.com", "user1@mail.com"], ], - [["Alex", "alex2@mail.com", "alex@mail.com"], ["Alex", "alex3@mail.com"]], + [["User", "user1@mail.com", "user2@mail.com", "user3@mail.com", "user@mail.com"]], ), ( [ - ["Tom", "tom1@mail.com"], - ["Tom", "tom2@mail.com"], - ["Tom", "tom3@mail.com"], - ["Tom", "tom4@mail.com"], - ], - [ - ["Tom", "tom1@mail.com"], - ["Tom", "tom2@mail.com"], - ["Tom", "tom3@mail.com"], - ["Tom", "tom4@mail.com"], + ["Test", "test1@mail.com"], + ["Test", "test2@mail.com"], + ["Test", "test1@mail.com", "test3@mail.com"], ], + [["Test", "test2@mail.com"], ["Test", "test1@mail.com", "test3@mail.com"]], ), ( - [["Sam", "sam@mail.com", "sam1@mail.com", "sam2@mail.com"]], - [["Sam", "sam@mail.com", "sam1@mail.com", "sam2@mail.com"]], + [ + ["Name", "a@mail.com", "b@mail.com", "c@mail.com"], + ["Name", "d@mail.com", "e@mail.com"], + ["Name", "c@mail.com", "f@mail.com"], + ], + [ + ["Name", "d@mail.com", "e@mail.com"], + ["Name", "a@mail.com", "b@mail.com", "c@mail.com", "f@mail.com"], + ], ), ], ) diff --git a/leetcode/construct_binary_tree_from_preorder_and_inorder_traversal/README.md b/leetcode/construct_binary_tree_from_preorder_and_inorder_traversal/README.md new file mode 100644 index 0000000..1fff676 --- /dev/null +++ b/leetcode/construct_binary_tree_from_preorder_and_inorder_traversal/README.md @@ -0,0 +1,39 @@ +# Construct Binary Tree from Preorder and Inorder Traversal + +**Difficulty:** Medium +**Topics:** Array, Hash Table, Divide and Conquer, Tree, Binary Tree +**Tags:** grind-75 + +**LeetCode:** [Problem 105](https://leetcode.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/description/) + +## Problem Description + +Given two integer arrays `preorder` and `inorder` where `preorder` is the preorder traversal of a binary tree and `inorder` is the inorder traversal of the same tree, construct and return the binary tree. + +## Examples + +### Example 1: + +![Example 1](https://assets.leetcode.com/uploads/2021/02/19/tree.jpg) + +``` +Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7] +Output: [3,9,20,null,null,15,7] +``` + +### Example 2: + +``` +Input: preorder = [-1], inorder = [-1] +Output: [-1] +``` + +## Constraints + +- 1 <= preorder.length <= 3000 +- inorder.length == preorder.length +- -3000 <= preorder[i], inorder[i] <= 3000 +- preorder and inorder consist of unique values. +- Each value of inorder also appears in preorder. +- preorder is guaranteed to be the preorder traversal of the tree. +- inorder is guaranteed to be the inorder traversal of the tree. diff --git a/leetcode/construct_binary_tree_from_preorder_and_inorder_traversal/__init__.py b/leetcode/construct_binary_tree_from_preorder_and_inorder_traversal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/construct_binary_tree_from_preorder_and_inorder_traversal/helpers.py b/leetcode/construct_binary_tree_from_preorder_and_inorder_traversal/helpers.py new file mode 100644 index 0000000..10c4f04 --- /dev/null +++ b/leetcode/construct_binary_tree_from_preorder_and_inorder_traversal/helpers.py @@ -0,0 +1,12 @@ +from leetcode_py import TreeNode + + +def run_build_tree(solution_class: type, preorder: list[int], inorder: list[int]): + implementation = solution_class() + return implementation.build_tree(preorder, inorder) + + +def assert_build_tree(result: TreeNode | None, expected_list: list[int | None]) -> bool: + expected = TreeNode.from_list(expected_list) if expected_list else None + assert result == expected + return True diff --git a/leetcode/construct_binary_tree_from_preorder_and_inorder_traversal/playground.py b/leetcode/construct_binary_tree_from_preorder_and_inorder_traversal/playground.py new file mode 100644 index 0000000..fa4baf4 --- /dev/null +++ b/leetcode/construct_binary_tree_from_preorder_and_inorder_traversal/playground.py @@ -0,0 +1,30 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.17.3 +# kernelspec: +# display_name: leetcode-py-py3.13 +# language: python +# name: python3 +# --- + +# %% +from helpers import assert_build_tree, run_build_tree +from solution import Solution + +# %% +# Example test case +preorder = [3, 9, 20, 15, 7] +inorder = [9, 3, 15, 20, 7] +expected_list = [3, 9, 20, None, None, 15, 7] + +# %% +result = run_build_tree(Solution, preorder, inorder) +result + +# %% +assert_build_tree(result, expected_list) diff --git a/leetcode/construct_binary_tree_from_preorder_and_inorder_traversal/solution.py b/leetcode/construct_binary_tree_from_preorder_and_inorder_traversal/solution.py new file mode 100644 index 0000000..8dce67c --- /dev/null +++ b/leetcode/construct_binary_tree_from_preorder_and_inorder_traversal/solution.py @@ -0,0 +1,59 @@ +from leetcode_py import TreeNode + + +class Solution: + """ + Construct Binary Tree from Preorder and Inorder Traversal + + Algorithm Explanation: + - Preorder: Root -> Left -> Right (first element is always root) + - Inorder: Left -> Root -> Right (root splits left/right subtrees) + + Example: preorder=[3,9,20,15,7], inorder=[9,3,15,20,7] + + Step 1: Root = 3 (first in preorder) + Find 3 in inorder at index 1 + Left subtree: inorder[0:1] = [9] + Right subtree: inorder[2:] = [15,20,7] + + Step 2: Build left subtree with preorder=[9], inorder=[9] + Root = 9, no children + + Step 3: Build right subtree with preorder=[20,15,7], inorder=[15,20,7] + Root = 20, left=[15], right=[7] + + Final tree: + 3 + / \ + 9 20 + / \ + 15 7 + """ + + # Time: O(n) - hashmap lookup O(1) for each of n nodes + # Space: O(n) - hashmap + recursion stack + def build_tree(self, preorder: list[int], inorder: list[int]) -> TreeNode | None: + if not preorder or not inorder: + return None + + inorder_map = {val: i for i, val in enumerate(inorder)} + self.preorder_index = 0 + + def build(left: int, right: int) -> TreeNode | None: + # left, right: boundaries in inorder array for current subtree + if left > right: + return None + + root_val = preorder[self.preorder_index] + self.preorder_index += 1 + root = TreeNode(root_val) + + mid = inorder_map[root_val] # root position in inorder + # Left subtree: inorder[left:mid-1] + root.left = build(left, mid - 1) + # Right subtree: inorder[mid+1:right] + root.right = build(mid + 1, right) + + return root + + return build(0, len(inorder) - 1) diff --git a/leetcode/construct_binary_tree_from_preorder_and_inorder_traversal/test_solution.py b/leetcode/construct_binary_tree_from_preorder_and_inorder_traversal/test_solution.py new file mode 100644 index 0000000..5f08fe6 --- /dev/null +++ b/leetcode/construct_binary_tree_from_preorder_and_inorder_traversal/test_solution.py @@ -0,0 +1,41 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .helpers import assert_build_tree, run_build_tree +from .solution import Solution + + +class TestConstructBinaryTreeFromPreorderAndInorderTraversal: + def setup_method(self): + self.solution = Solution() + + @logged_test + @pytest.mark.parametrize( + "preorder, inorder, expected_list", + [ + ([], [], []), + ([1], [1], [1]), + ([3, 9, 20, 15, 7], [9, 3, 15, 20, 7], [3, 9, 20, None, None, 15, 7]), + ([-1], [-1], [-1]), + ([1, 2], [2, 1], [1, 2]), + ([1, 2], [1, 2], [1, None, 2]), + ([1, 2, 3], [2, 1, 3], [1, 2, 3]), + ([1, 2, 4, 5, 3, 6], [4, 2, 5, 1, 6, 3], [1, 2, 3, 4, 5, 6]), + ([1, 2, 3, 4], [1, 2, 3, 4], [1, None, 2, None, 3, None, 4]), + ([4, 3, 2, 1], [1, 2, 3, 4], [4, 3, None, 2, None, 1]), + ([10, 5, 1, 7, 40, 50], [1, 5, 7, 10, 40, 50], [10, 5, 40, 1, 7, None, 50]), + ([1, 3, 2], [1, 2, 3], [1, None, 3, 2]), + ([2, 1, 3], [1, 2, 3], [2, 1, 3]), + ([5, 3, 2, 1, 4, 6, 7], [1, 2, 3, 4, 5, 6, 7], [5, 3, 6, 2, 4, None, 7, 1]), + ( + [7, 3, 2, 1, 5, 4, 6, 10, 9, 11], + [1, 2, 3, 4, 5, 6, 7, 9, 10, 11], + [7, 3, 10, 2, 5, 9, 11, 1, None, 4, 6], + ), + ([-3000, -2999, -2998], [-2998, -2999, -3000], [-3000, -2999, None, -2998]), + ], + ) + def test_build_tree(self, preorder: list[int], inorder: list[int], expected_list: list[int | None]): + result = run_build_tree(Solution, preorder, inorder) + assert_build_tree(result, expected_list) diff --git a/leetcode/diagonal_traverse/README.md b/leetcode/diagonal_traverse/README.md new file mode 100644 index 0000000..e988eef --- /dev/null +++ b/leetcode/diagonal_traverse/README.md @@ -0,0 +1,37 @@ +# Diagonal Traverse + +**Difficulty:** Medium +**Topics:** Array, Matrix, Simulation +**Tags:** + +**LeetCode:** [Problem 498](https://leetcode.com/problems/diagonal-traverse/description/) + +## Problem Description + +Given an `m x n` matrix `mat`, return _an array of all the elements of the array in a diagonal order_. + +## Examples + +### Example 1: + +![Diagonal Traverse](https://assets.leetcode.com/uploads/2021/04/10/diag1-grid.jpg) + +``` +Input: mat = [[1,2,3],[4,5,6],[7,8,9]] +Output: [1,2,4,7,5,3,6,8,9] +``` + +### Example 2: + +``` +Input: mat = [[1,2],[3,4]] +Output: [1,2,3,4] +``` + +## Constraints + +- `m == mat.length` +- `n == mat[i].length` +- `1 <= m, n <= 10^4` +- `1 <= m * n <= 10^4` +- `-10^5 <= mat[i][j] <= 10^5` diff --git a/leetcode/diagonal_traverse/__init__.py b/leetcode/diagonal_traverse/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/diagonal_traverse/helpers.py b/leetcode/diagonal_traverse/helpers.py new file mode 100644 index 0000000..e828640 --- /dev/null +++ b/leetcode/diagonal_traverse/helpers.py @@ -0,0 +1,8 @@ +def run_find_diagonal_order(solution_class: type, mat: list[list[int]]): + implementation = solution_class() + return implementation.find_diagonal_order(mat) + + +def assert_find_diagonal_order(result: list[int], expected: list[int]) -> bool: + assert result == expected + return True diff --git a/leetcode/diagonal_traverse/playground.py b/leetcode/diagonal_traverse/playground.py new file mode 100644 index 0000000..ffdfc99 --- /dev/null +++ b/leetcode/diagonal_traverse/playground.py @@ -0,0 +1,29 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.17.3 +# kernelspec: +# display_name: leetcode-py-py3.13 +# language: python +# name: python3 +# --- + +# %% +from helpers import assert_find_diagonal_order, run_find_diagonal_order +from solution import Solution + +# %% +# Example test case +mat = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] +expected = [1, 2, 4, 7, 5, 3, 6, 8, 9] + +# %% +result = run_find_diagonal_order(Solution, mat) +result + +# %% +assert_find_diagonal_order(result, expected) diff --git a/leetcode/diagonal_traverse/solution.py b/leetcode/diagonal_traverse/solution.py new file mode 100644 index 0000000..e3f8ecb --- /dev/null +++ b/leetcode/diagonal_traverse/solution.py @@ -0,0 +1,70 @@ +class Solution: + """ + Diagonal Traverse Pattern: + + Matrix with coordinates: d = i+j (diagonal index): + 1(0,0) 2(0,1) 3(0,2) d=0: 1(0,0) ↗ + 4(1,0) 5(1,1) 6(1,2) d=1: 2(0,1), 4(1,0) ↙ + 7(2,0) 8(2,1) 9(2,2) d=2: 3(0,2), 5(1,1), 7(2,0) ↗ + d=3: 6(1,2), 8(2,1) ↙ + d=4: 9(2,2) ↗ + + 'd' = diagonal number = sum of row+col indices (i+j) + Each diagonal contains elements where i+j equals the same value + + Result: [1,2,4,7,5,3,6,8,9] + """ + + # Time: O(m*n) + # Space: O(1) + def find_diagonal_order(self, mat: list[list[int]]) -> list[int]: + m, n = len(mat), len(mat[0]) + result = [] + + for d in range(m + n - 1): + if d % 2 == 0: # up-right diagonal + for i in range(min(d, m - 1), max(-1, d - n), -1): + result.append(mat[i][d - i]) + else: # down-left diagonal + for i in range(max(0, d - n + 1), min(d + 1, m)): + result.append(mat[i][d - i]) + + return result + + +class SolutionRowShift: + """ + Row-shift approach: shift each row to align diagonals into columns + + Original matrix: After shifting rows (col-row=actual_col): + 1 2 3 col=0 col=1 col=2 col=3 col=4 + 4 5 6 1 2 3 + 7 8 9 4 5 6 + 7 8 9 + ↑ ↓ ↑ ↓ ↑ + + Each row is shifted right by its row index, creating vertical columns + from the original diagonals. Then alternate traversal direction. + + Traverse: 1 → 2,4 → 7,5,3 → 6,8 → 9 + """ + + # Time: O(m*n) + # Space: O(1) + def find_diagonal_order(self, mat: list[list[int]]) -> list[int]: + m, n = len(mat), len(mat[0]) + result = [] + + for col in range(m + n - 1): + if col % 2 == 1: # upward + for row in range(m): + i = col - row + if 0 <= i < n: + result.append(mat[row][i]) + else: # downward + for row in range(m - 1, -1, -1): + i = col - row + if 0 <= i < n: + result.append(mat[row][i]) + + return result diff --git a/leetcode/diagonal_traverse/test_solution.py b/leetcode/diagonal_traverse/test_solution.py new file mode 100644 index 0000000..a57c110 --- /dev/null +++ b/leetcode/diagonal_traverse/test_solution.py @@ -0,0 +1,34 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .helpers import assert_find_diagonal_order, run_find_diagonal_order +from .solution import Solution, SolutionRowShift + + +class TestDiagonalTraverse: + def setup_method(self): + self.solution = Solution() + + @logged_test + @pytest.mark.parametrize("solution_class", [Solution, SolutionRowShift]) + @pytest.mark.parametrize( + "mat, expected", + [ + ([[1, 2, 3], [4, 5, 6], [7, 8, 9]], [1, 2, 4, 7, 5, 3, 6, 8, 9]), + ([[1, 2], [3, 4]], [1, 2, 3, 4]), + ([[1]], [1]), + ([[1, 2, 3]], [1, 2, 3]), + ([[1], [2], [3]], [1, 2, 3]), + ([[1, 2, 3, 4], [5, 6, 7, 8]], [1, 2, 5, 6, 3, 4, 7, 8]), + ([[1, 2], [3, 4], [5, 6]], [1, 2, 3, 5, 4, 6]), + ([[1, 2, 3, 4, 5]], [1, 2, 3, 4, 5]), + ([[1], [2], [3], [4], [5]], [1, 2, 3, 4, 5]), + ([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]], [1, 2, 5, 9, 6, 3, 4, 7, 10, 11, 8, 12]), + ([[-1, 0, 1], [2, -3, 4]], [-1, 0, 2, -3, 1, 4]), + ([[100]], [100]), + ], + ) + def test_find_diagonal_order(self, solution_class, mat: list[list[int]], expected: list[int]): + result = run_find_diagonal_order(solution_class, mat) + assert_find_diagonal_order(result, expected) diff --git a/leetcode/find_all_anagrams_in_a_string/README.md b/leetcode/find_all_anagrams_in_a_string/README.md new file mode 100644 index 0000000..d84fab3 --- /dev/null +++ b/leetcode/find_all_anagrams_in_a_string/README.md @@ -0,0 +1,43 @@ +# Find All Anagrams in a String + +**Difficulty:** Medium +**Topics:** Hash Table, String, Sliding Window +**Tags:** grind-75 + +**LeetCode:** [Problem 438](https://leetcode.com/problems/find-all-anagrams-in-a-string/description/) + +## Problem Description + +Given two strings `s` and `p`, return an array of all the start indices of `p`'s anagrams in `s`. You may return the answer in any order. + +An **anagram** is a word or phrase formed by rearranging the letters of a different word or phrase, typically using all the original letters exactly once. + +## Examples + +### Example 1: + +``` +Input: s = "cbaebabacd", p = "abc" +Output: [0,6] +``` + +**Explanation:** +The substring with start index = 0 is "cba", which is an anagram of "abc". +The substring with start index = 6 is "bac", which is an anagram of "abc". + +### Example 2: + +``` +Input: s = "abab", p = "ab" +Output: [0,1,2] +``` + +**Explanation:** +The substring with start index = 0 is "ab", which is an anagram of "ab". +The substring with start index = 1 is "ba", which is an anagram of "ab". +The substring with start index = 2 is "ab", which is an anagram of "ab". + +## Constraints + +- 1 <= s.length, p.length <= 3 \* 10^4 +- s and p consist of lowercase English letters. diff --git a/leetcode/find_all_anagrams_in_a_string/__init__.py b/leetcode/find_all_anagrams_in_a_string/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/find_all_anagrams_in_a_string/helpers.py b/leetcode/find_all_anagrams_in_a_string/helpers.py new file mode 100644 index 0000000..5836a99 --- /dev/null +++ b/leetcode/find_all_anagrams_in_a_string/helpers.py @@ -0,0 +1,8 @@ +def run_find_anagrams(solution_class: type, s: str, p: str): + implementation = solution_class() + return implementation.find_anagrams(s, p) + + +def assert_find_anagrams(result: list[int], expected: list[int]) -> bool: + assert sorted(result) == sorted(expected) + return True diff --git a/leetcode/find_all_anagrams_in_a_string/playground.py b/leetcode/find_all_anagrams_in_a_string/playground.py new file mode 100644 index 0000000..42f330b --- /dev/null +++ b/leetcode/find_all_anagrams_in_a_string/playground.py @@ -0,0 +1,30 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.17.3 +# kernelspec: +# display_name: leetcode-py-py3.13 +# language: python +# name: python3 +# --- + +# %% +from helpers import assert_find_anagrams, run_find_anagrams +from solution import Solution + +# %% +# Example test case +s = "cbaebabacd" +p = "abc" +expected = [0, 6] + +# %% +result = run_find_anagrams(Solution, s, p) +result + +# %% +assert_find_anagrams(result, expected) diff --git a/leetcode/find_all_anagrams_in_a_string/solution.py b/leetcode/find_all_anagrams_in_a_string/solution.py new file mode 100644 index 0000000..f26a54f --- /dev/null +++ b/leetcode/find_all_anagrams_in_a_string/solution.py @@ -0,0 +1,56 @@ +from collections import Counter + + +class Solution: + """ + Sliding Window with Character Frequency Counting + + Algorithm: + 1. Count character frequencies in pattern p + 2. Use sliding window of size len(p) on string s + 3. Maintain frequency count of current window + 4. When frequencies match, record start index + + ASCII Visualization: + s = "cbaebabacd", p = "abc" (need: a=1, b=1, c=1) + + Window positions: + [cba]ebabacd -> {c:1, b:1, a:1} ✓ matches -> index 0 + c[bae]babacd -> {b:1, a:1, e:1} ✗ + cb[aeb]abacd -> {a:1, e:1, b:1} ✗ + cba[eba]bacd -> {e:1, b:1, a:1} ✗ + cbae[bab]acd -> {b:2, a:1} ✗ + cbaeb[aba]cd -> {a:2, b:1} ✗ + cbaeba[bac]d -> {b:1, a:1, c:1} ✓ matches -> index 6 + """ + + # Time: O(n) where n is length of s + # Space: O(1) - at most 26 lowercase letters + def find_anagrams(self, s: str, p: str) -> list[int]: + if len(p) > len(s): + return [] + + result = [] + p_count = Counter(p) + window_count = Counter(s[: len(p)]) + + # Check first window + if window_count == p_count: + result.append(0) + + # Slide window + for i in range(len(p), len(s)): + # Add new character + window_count[s[i]] += 1 + + # Remove old character + left_char = s[i - len(p)] + window_count[left_char] -= 1 + if window_count[left_char] == 0: + del window_count[left_char] + + # Check if current window is anagram + if window_count == p_count: + result.append(i - len(p) + 1) + + return result diff --git a/leetcode/find_all_anagrams_in_a_string/test_solution.py b/leetcode/find_all_anagrams_in_a_string/test_solution.py new file mode 100644 index 0000000..3782888 --- /dev/null +++ b/leetcode/find_all_anagrams_in_a_string/test_solution.py @@ -0,0 +1,33 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .helpers import assert_find_anagrams, run_find_anagrams +from .solution import Solution + + +class TestFindAllAnagramsInAString: + def setup_method(self): + self.solution = Solution() + + @logged_test + @pytest.mark.parametrize( + "s, p, expected", + [ + ("cbaebabacd", "abc", [0, 6]), + ("abab", "ab", [0, 1, 2]), + ("a", "aa", []), + ("aa", "aa", [0]), + ("abcdefg", "xyz", []), + ("aab", "ab", [1]), + ("aaab", "ab", [2]), + ("baa", "aa", [1]), + ("abacabad", "aaab", []), + ("ababacb", "abc", [3, 4]), + ("abaacbabc", "abc", [3, 4, 6]), + ("abab", "abab", [0]), + ], + ) + def test_find_anagrams(self, s: str, p: str, expected: list[int]): + result = run_find_anagrams(Solution, s, p) + assert_find_anagrams(result, expected) diff --git a/leetcode/letter_combinations_of_a_phone_number/README.md b/leetcode/letter_combinations_of_a_phone_number/README.md new file mode 100644 index 0000000..43f0064 --- /dev/null +++ b/leetcode/letter_combinations_of_a_phone_number/README.md @@ -0,0 +1,43 @@ +# Letter Combinations of a Phone Number + +**Difficulty:** Medium +**Topics:** Hash Table, String, Backtracking +**Tags:** grind-75 + +**LeetCode:** [Problem 17](https://leetcode.com/problems/letter-combinations-of-a-phone-number/description/) + +## Problem Description + +Given a string containing digits from `2-9` inclusive, return all possible letter combinations that the number could represent. Return the answer in **any order**. + +A mapping of digits to letters (just like on the telephone buttons) is given below. Note that 1 does not map to any letters. + +## Examples + +### Example 1: + +![Phone Keypad](https://assets.leetcode.com/uploads/2022/03/15/1200px-telephone-keypad2svg.png) + +``` +Input: digits = "23" +Output: ["ad","ae","af","bd","be","bf","cd","ce","cf"] +``` + +### Example 2: + +``` +Input: digits = "" +Output: [] +``` + +### Example 3: + +``` +Input: digits = "2" +Output: ["a","b","c"] +``` + +## Constraints + +- `0 <= digits.length <= 4` +- `digits[i]` is a digit in the range `['2', '9']`. diff --git a/leetcode/letter_combinations_of_a_phone_number/__init__.py b/leetcode/letter_combinations_of_a_phone_number/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/letter_combinations_of_a_phone_number/helpers.py b/leetcode/letter_combinations_of_a_phone_number/helpers.py new file mode 100644 index 0000000..b35cae8 --- /dev/null +++ b/leetcode/letter_combinations_of_a_phone_number/helpers.py @@ -0,0 +1,10 @@ +def run_letter_combinations(solution_class: type, digits: str): + implementation = solution_class() + return implementation.letter_combinations(digits) + + +def assert_letter_combinations(result: list[str], expected: list[str]) -> bool: + result.sort() + expected.sort() + assert result == expected + return True diff --git a/leetcode/letter_combinations_of_a_phone_number/playground.py b/leetcode/letter_combinations_of_a_phone_number/playground.py new file mode 100644 index 0000000..3dcbf29 --- /dev/null +++ b/leetcode/letter_combinations_of_a_phone_number/playground.py @@ -0,0 +1,29 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.17.3 +# kernelspec: +# display_name: leetcode-py-py3.13 +# language: python +# name: python3 +# --- + +# %% +from helpers import assert_letter_combinations, run_letter_combinations +from solution import Solution + +# %% +# Example test case +digits = "23" +expected = ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"] + +# %% +result = run_letter_combinations(Solution, digits) +result + +# %% +assert_letter_combinations(result, expected) diff --git a/leetcode/letter_combinations_of_a_phone_number/solution.py b/leetcode/letter_combinations_of_a_phone_number/solution.py new file mode 100644 index 0000000..b198a2e --- /dev/null +++ b/leetcode/letter_combinations_of_a_phone_number/solution.py @@ -0,0 +1,31 @@ +class Solution: + + # Time: O(4^n) + # Space: O(4^n) + def letter_combinations(self, digits: str) -> list[str]: + if not digits: + return [] + + phone = { + "2": "abc", + "3": "def", + "4": "ghi", + "5": "jkl", + "6": "mno", + "7": "pqrs", + "8": "tuv", + "9": "wxyz", + } + + result = [] + + def backtrack(i: int, path: str) -> None: + if i == len(digits): + result.append(path) + return + + for letter in phone[digits[i]]: + backtrack(i + 1, path + letter) + + backtrack(0, "") + return result diff --git a/leetcode/letter_combinations_of_a_phone_number/test_solution.py b/leetcode/letter_combinations_of_a_phone_number/test_solution.py new file mode 100644 index 0000000..c28498c --- /dev/null +++ b/leetcode/letter_combinations_of_a_phone_number/test_solution.py @@ -0,0 +1,169 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .helpers import assert_letter_combinations, run_letter_combinations +from .solution import Solution + + +class TestLetterCombinationsOfAPhoneNumber: + def setup_method(self): + self.solution = Solution() + + @logged_test + @pytest.mark.parametrize( + "digits, expected", + [ + ("23", ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]), + ("", []), + ("2", ["a", "b", "c"]), + ( + "234", + [ + "adg", + "adh", + "adi", + "aeg", + "aeh", + "aei", + "afg", + "afh", + "afi", + "bdg", + "bdh", + "bdi", + "beg", + "beh", + "bei", + "bfg", + "bfh", + "bfi", + "cdg", + "cdh", + "cdi", + "ceg", + "ceh", + "cei", + "cfg", + "cfh", + "cfi", + ], + ), + ("7", ["p", "q", "r", "s"]), + ("9", ["w", "x", "y", "z"]), + ( + "79", + [ + "pw", + "px", + "py", + "pz", + "qw", + "qx", + "qy", + "qz", + "rw", + "rx", + "ry", + "rz", + "sw", + "sx", + "sy", + "sz", + ], + ), + ("22", ["aa", "ab", "ac", "ba", "bb", "bc", "ca", "cb", "cc"]), + ( + "3456", + [ + "dgjm", + "dgjn", + "dgjo", + "dgkm", + "dgkn", + "dgko", + "dglm", + "dgln", + "dglo", + "dhjm", + "dhjn", + "dhjo", + "dhkm", + "dhkn", + "dhko", + "dhlm", + "dhln", + "dhlo", + "dijm", + "dijn", + "dijo", + "dikm", + "dikn", + "diko", + "dilm", + "diln", + "dilo", + "egjm", + "egjn", + "egjo", + "egkm", + "egkn", + "egko", + "eglm", + "egln", + "eglo", + "ehjm", + "ehjn", + "ehjo", + "ehkm", + "ehkn", + "ehko", + "ehlm", + "ehln", + "ehlo", + "eijm", + "eijn", + "eijo", + "eikm", + "eikn", + "eiko", + "eilm", + "eiln", + "eilo", + "fgjm", + "fgjn", + "fgjo", + "fgkm", + "fgkn", + "fgko", + "fglm", + "fgln", + "fglo", + "fhjm", + "fhjn", + "fhjo", + "fhkm", + "fhkn", + "fhko", + "fhlm", + "fhln", + "fhlo", + "fijm", + "fijn", + "fijo", + "fikm", + "fikn", + "fiko", + "film", + "filn", + "filo", + ], + ), + ("25", ["aj", "ak", "al", "bj", "bk", "bl", "cj", "ck", "cl"]), + ("78", ["pt", "pu", "pv", "qt", "qu", "qv", "rt", "ru", "rv", "st", "su", "sv"]), + ("89", ["tw", "tx", "ty", "tz", "uw", "ux", "uy", "uz", "vw", "vx", "vy", "vz"]), + ], + ) + def test_letter_combinations(self, digits: str, expected: list[str]): + result = run_letter_combinations(Solution, digits) + assert_letter_combinations(result, expected) diff --git a/leetcode/subsets/README.md b/leetcode/subsets/README.md new file mode 100644 index 0000000..a28b3e0 --- /dev/null +++ b/leetcode/subsets/README.md @@ -0,0 +1,35 @@ +# Subsets + +**Difficulty:** Medium +**Topics:** Array, Backtracking, Bit Manipulation +**Tags:** grind-75 + +**LeetCode:** [Problem 78](https://leetcode.com/problems/subsets/description/) + +## Problem Description + +Given an integer array `nums` of **unique** elements, return _all possible_ subsets (the power set). + +The solution set **must not** contain duplicate subsets. Return the solution in **any order**. + +## Examples + +### Example 1: + +``` +Input: nums = [1,2,3] +Output: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]] +``` + +### Example 2: + +``` +Input: nums = [0] +Output: [[],[0]] +``` + +## Constraints + +- 1 <= nums.length <= 10 +- -10 <= nums[i] <= 10 +- All the numbers of nums are unique. diff --git a/leetcode/subsets/__init__.py b/leetcode/subsets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/subsets/helpers.py b/leetcode/subsets/helpers.py new file mode 100644 index 0000000..a28c5a3 --- /dev/null +++ b/leetcode/subsets/helpers.py @@ -0,0 +1,11 @@ +def run_subsets(solution_class: type, nums: list[int]): + implementation = solution_class() + return implementation.subsets(nums) + + +def assert_subsets(result: list[list[int]], expected: list[list[int]]) -> bool: + # Sort both result and expected for comparison since order doesn't matter + result_sorted = [sorted(subset) for subset in sorted(result)] + expected_sorted = [sorted(subset) for subset in sorted(expected)] + assert result_sorted == expected_sorted + return True diff --git a/leetcode/subsets/playground.py b/leetcode/subsets/playground.py new file mode 100644 index 0000000..c7f57bb --- /dev/null +++ b/leetcode/subsets/playground.py @@ -0,0 +1,29 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.17.3 +# kernelspec: +# display_name: leetcode-py-py3.13 +# language: python +# name: python3 +# --- + +# %% +from helpers import assert_subsets, run_subsets +from solution import Solution + +# %% +# Example test case +nums = [1, 2, 3] +expected = [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]] + +# %% +result = run_subsets(Solution, nums) +result + +# %% +assert_subsets(result, expected) diff --git a/leetcode/subsets/solution.py b/leetcode/subsets/solution.py new file mode 100644 index 0000000..3b9d8a2 --- /dev/null +++ b/leetcode/subsets/solution.py @@ -0,0 +1,16 @@ +class Solution: + + # Time: O(2^n) + # Space: O(2^n) + def subsets(self, nums: list[int]) -> list[list[int]]: + result = [] + + def backtrack(start: int, path: list[int]) -> None: + result.append(path[:]) + for i in range(start, len(nums)): + path.append(nums[i]) + backtrack(i + 1, path) + path.pop() + + backtrack(0, []) + return result diff --git a/leetcode/subsets/test_solution.py b/leetcode/subsets/test_solution.py new file mode 100644 index 0000000..6efd136 --- /dev/null +++ b/leetcode/subsets/test_solution.py @@ -0,0 +1,53 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .helpers import assert_subsets, run_subsets +from .solution import Solution + + +class TestSubsets: + def setup_method(self): + self.solution = Solution() + + @logged_test + @pytest.mark.parametrize( + "nums, expected", + [ + ([1, 2, 3], [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]), + ([0], [[], [0]]), + ([1], [[], [1]]), + ([1, 2], [[], [1], [2], [1, 2]]), + ([4, 1, 0], [[], [4], [4, 1], [4, 1, 0], [4, 0], [1], [1, 0], [0]]), + ([-1, 0, 1], [[], [-1], [-1, 0], [-1, 0, 1], [-1, 1], [0], [0, 1], [1]]), + ([5], [[], [5]]), + ([2, 1, 3], [[], [2], [2, 1], [2, 1, 3], [2, 3], [1], [1, 3], [3]]), + ([10], [[], [10]]), + ([-10, 10], [[], [-10], [-10, 10], [10]]), + ( + [1, 2, 3, 4], + [ + [], + [1], + [1, 2], + [1, 2, 3], + [1, 2, 3, 4], + [1, 2, 4], + [1, 3], + [1, 3, 4], + [1, 4], + [2], + [2, 3], + [2, 3, 4], + [2, 4], + [3], + [3, 4], + [4], + ], + ), + ([5, 2], [[], [5], [5, 2], [2]]), + ], + ) + def test_subsets(self, nums: list[int], expected: list[list[int]]): + result = run_subsets(Solution, nums) + assert_subsets(result, expected) diff --git a/leetcode/unique_paths/README.md b/leetcode/unique_paths/README.md new file mode 100644 index 0000000..d40c811 --- /dev/null +++ b/leetcode/unique_paths/README.md @@ -0,0 +1,43 @@ +# Unique Paths + +**Difficulty:** Medium +**Topics:** Math, Dynamic Programming, Combinatorics +**Tags:** grind-75 + +**LeetCode:** [Problem 62](https://leetcode.com/problems/unique-paths/description/) + +## Problem Description + +There is a robot on an `m x n` grid. The robot is initially located at the **top-left corner** (i.e., `grid[0][0]`). The robot tries to move to the **bottom-right corner** (i.e., `grid[m - 1][n - 1]`). The robot can only move either down or right at any point in time. + +Given the two integers `m` and `n`, return _the number of possible unique paths that the robot can take to reach the bottom-right corner_. + +The test cases are generated so that the answer will be less than or equal to `2 * 10^9`. + +## Examples + +### Example 1: + +![Example 1](https://assets.leetcode.com/uploads/2018/10/22/robot_maze.png) + +``` +Input: m = 3, n = 7 +Output: 28 +``` + +### Example 2: + +``` +Input: m = 3, n = 2 +Output: 3 +``` + +**Explanation:** From the top-left corner, there are a total of 3 ways to reach the bottom-right corner: + +1. Right -> Down -> Down +2. Down -> Down -> Right +3. Down -> Right -> Down + +## Constraints + +- 1 <= m, n <= 100 diff --git a/leetcode/unique_paths/__init__.py b/leetcode/unique_paths/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/unique_paths/helpers.py b/leetcode/unique_paths/helpers.py new file mode 100644 index 0000000..c0ead78 --- /dev/null +++ b/leetcode/unique_paths/helpers.py @@ -0,0 +1,8 @@ +def run_unique_paths(solution_class: type, m: int, n: int): + implementation = solution_class() + return implementation.unique_paths(m, n) + + +def assert_unique_paths(result: int, expected: int) -> bool: + assert result == expected + return True diff --git a/leetcode/unique_paths/playground.py b/leetcode/unique_paths/playground.py new file mode 100644 index 0000000..e70351b --- /dev/null +++ b/leetcode/unique_paths/playground.py @@ -0,0 +1,30 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.17.3 +# kernelspec: +# display_name: leetcode-py-py3.13 +# language: python +# name: python3 +# --- + +# %% +from helpers import assert_unique_paths, run_unique_paths +from solution import Solution + +# %% +# Example test case +m = 3 +n = 7 +expected = 28 + +# %% +result = run_unique_paths(Solution, m, n) +result + +# %% +assert_unique_paths(result, expected) diff --git a/leetcode/unique_paths/solution.py b/leetcode/unique_paths/solution.py new file mode 100644 index 0000000..0a3ddb9 --- /dev/null +++ b/leetcode/unique_paths/solution.py @@ -0,0 +1,29 @@ +class Solution: + # Dynamic Programming + # Time: O(m * n) + # Space: O(min(m, n)) + def unique_paths(self, m: int, n: int) -> int: + if m > n: + m, n = n, m + dp = [1] * m + for _ in range(1, n): + for j in range(1, m): + dp[j] += dp[j - 1] + return dp[m - 1] + + +class SolutionMath: + + # Math solution: C(m+n-2, m-1) = (m+n-2)! / ((m-1)! * (n-1)!) + # Time: O(min(m, n)) + # Space: O(1) + def unique_paths(self, m: int, n: int) -> int: + # Total moves: (m-1) right + (n-1) down = m+n-2 + # Choose (m-1) positions for right moves out of (m+n-2) total + if m > n: + m, n = n, m # Optimize for smaller factorial + + result = 1 + for i in range(m - 1): + result = result * (n + i) // (i + 1) + return result diff --git a/leetcode/unique_paths/test_solution.py b/leetcode/unique_paths/test_solution.py new file mode 100644 index 0000000..b66ad67 --- /dev/null +++ b/leetcode/unique_paths/test_solution.py @@ -0,0 +1,34 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .helpers import assert_unique_paths, run_unique_paths +from .solution import Solution, SolutionMath + + +class TestUniquePaths: + @logged_test + @pytest.mark.parametrize("solution_class", [Solution, SolutionMath]) + @pytest.mark.parametrize( + "m, n, expected", + [ + (3, 7, 28), + (3, 2, 3), + (1, 1, 1), + (1, 10, 1), + (10, 1, 1), + (2, 2, 2), + (3, 3, 6), + (4, 4, 20), + (5, 5, 70), + (2, 3, 3), + (3, 4, 10), + (4, 5, 35), + (6, 3, 21), + (7, 3, 28), + (10, 10, 48620), + ], + ) + def test_unique_paths(self, solution_class, m: int, n: int, expected: int): + result = run_unique_paths(solution_class, m, n) + assert_unique_paths(result, expected) diff --git a/leetcode/word_search/README.md b/leetcode/word_search/README.md new file mode 100644 index 0000000..8ceb82b --- /dev/null +++ b/leetcode/word_search/README.md @@ -0,0 +1,52 @@ +# Word Search + +**Difficulty:** Medium +**Topics:** Array, String, Backtracking, Depth-First Search, Matrix +**Tags:** grind-75 + +**LeetCode:** [Problem 79](https://leetcode.com/problems/word-search/description/) + +## Problem Description + +Given an `m x n` grid of characters `board` and a string `word`, return `true` _if_ `word` _exists in the grid_. + +The word can be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically neighboring. The same letter cell may not be used more than once. + +## Examples + +### Example 1: + +![Word Search Example 1](https://assets.leetcode.com/uploads/2020/11/04/word2.jpg) + +``` +Input: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED" +Output: true +``` + +### Example 2: + +![Word Search Example 2](https://assets.leetcode.com/uploads/2020/11/04/word-1.jpg) + +``` +Input: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE" +Output: true +``` + +### Example 3: + +![Word Search Example 3](https://assets.leetcode.com/uploads/2020/10/15/word3.jpg) + +``` +Input: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB" +Output: false +``` + +## Constraints + +- `m == board.length` +- `n = board[i].length` +- `1 <= m, n <= 6` +- `1 <= word.length <= 15` +- `board` and `word` consists of only lowercase and uppercase English letters. + +**Follow up:** Could you use search pruning to make your solution faster with a larger `board`? diff --git a/leetcode/word_search/__init__.py b/leetcode/word_search/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/word_search/helpers.py b/leetcode/word_search/helpers.py new file mode 100644 index 0000000..e1d096d --- /dev/null +++ b/leetcode/word_search/helpers.py @@ -0,0 +1,8 @@ +def run_exist(solution_class: type, board: list[list[str]], word: str): + implementation = solution_class() + return implementation.exist(board, word) + + +def assert_exist(result: bool, expected: bool) -> bool: + assert result == expected + return True diff --git a/leetcode/word_search/playground.py b/leetcode/word_search/playground.py new file mode 100644 index 0000000..5e69458 --- /dev/null +++ b/leetcode/word_search/playground.py @@ -0,0 +1,30 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.17.3 +# kernelspec: +# display_name: leetcode-py-py3.13 +# language: python +# name: python3 +# --- + +# %% +from helpers import assert_exist, run_exist +from solution import Solution + +# %% +# Example test case +board = [["A", "B", "C", "E"], ["S", "F", "C", "S"], ["A", "D", "E", "E"]] +word = "ABCCED" +expected = True + +# %% +result = run_exist(Solution, board, word) +result + +# %% +assert_exist(result, expected) diff --git a/leetcode/word_search/solution.py b/leetcode/word_search/solution.py new file mode 100644 index 0000000..0b5a659 --- /dev/null +++ b/leetcode/word_search/solution.py @@ -0,0 +1,42 @@ +from collections import Counter + + +class Solution: + + # Time: O(m*n*4^L) where L is word length + # Space: O(L) + def exist(self, board: list[list[str]], word: str) -> bool: + + m, n = len(board), len(board[0]) + + # Early pruning: check if board has enough characters + board_counter = Counter(ch for row in board for ch in row) + word_counter = Counter(word) + for ch in word_counter: + if board_counter[ch] < word_counter[ch]: + return False + + # Optimization: start from less frequent end + if board_counter[word[0]] > board_counter[word[-1]]: + word = word[::-1] + + def dfs(i: int, j: int, k: int) -> bool: + if k == len(word): + return True + if i < 0 or i >= m or j < 0 or j >= n or board[i][j] != word[k]: + return False + + temp = board[i][j] + board[i][j] = "#" + for di, dj in [(1, 0), (-1, 0), (0, 1), (0, -1)]: + if dfs(i + di, j + dj, k + 1): + board[i][j] = temp + return True + board[i][j] = temp + return False + + for i in range(m): + for j in range(n): + if dfs(i, j, 0): + return True + return False diff --git a/leetcode/word_search/test_solution.py b/leetcode/word_search/test_solution.py new file mode 100644 index 0000000..5e1151d --- /dev/null +++ b/leetcode/word_search/test_solution.py @@ -0,0 +1,44 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .helpers import assert_exist, run_exist +from .solution import Solution + + +class TestWordSearch: + def setup_method(self): + self.solution = Solution() + + @logged_test + @pytest.mark.parametrize( + "board, word, expected", + [ + ([["A", "B", "C", "E"], ["S", "F", "C", "S"], ["A", "D", "E", "E"]], "ABCCED", True), + ([["A", "B", "C", "E"], ["S", "F", "C", "S"], ["A", "D", "E", "E"]], "SEE", True), + ([["A", "B", "C", "E"], ["S", "F", "C", "S"], ["A", "D", "E", "E"]], "ABCB", False), + ([["A"]], "A", True), + ([["A"]], "B", False), + ([["A", "B"], ["C", "D"]], "ACDB", True), + ([["A", "B"], ["C", "D"]], "ABDC", True), + ([["A", "B"], ["C", "D"]], "ABCD", False), + ([["C", "A", "A"], ["A", "A", "A"], ["B", "C", "D"]], "AAB", True), + ([["A", "B", "C", "E"], ["S", "F", "C", "S"], ["A", "D", "E", "E"]], "ABCESEEEFS", False), + ( + [ + ["A", "A", "A", "A", "A", "A"], + ["A", "A", "A", "A", "A", "A"], + ["A", "A", "A", "A", "A", "A"], + ["A", "A", "A", "A", "A", "A"], + ["A", "A", "A", "A", "A", "A"], + ["A", "A", "A", "A", "A", "A"], + ], + "AAAAAAAAAAAAAAB", + False, + ), + ([["A", "B", "C", "E"], ["S", "F", "C", "S"], ["A", "D", "E", "E"]], "SFCS", True), + ], + ) + def test_exist(self, board: list[list[str]], word: str, expected: bool): + result = run_exist(Solution, board, word) + assert_exist(result, expected) diff --git a/pyproject.toml b/pyproject.toml index 7306c4a..7debb1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,3 +76,6 @@ python_files = ["tests.py", "test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] addopts = "-v --tb=short" + +[tool.coverage.run] +omit = ["**/playground.py"]