diff --git a/.github/workflows/ci-test-reproducibility.yml b/.github/workflows/ci-test-reproducibility.yml index 42c71c2..1be9cab 100644 --- a/.github/workflows/ci-test-reproducibility.yml +++ b/.github/workflows/ci-test-reproducibility.yml @@ -35,6 +35,40 @@ jobs: - name: Install dependencies run: poetry install --no-interaction --no-ansi + - name: Cache Graphviz installation + id: cache-graphviz + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + with: + path: ~/graphviz-cache + key: graphviz-installed-${{ runner.os }} + + - name: Install Graphviz + run: | + if [ "${{ steps.cache-graphviz.outputs.cache-hit }}" = "true" ]; then + sudo cp ~/graphviz-cache/bin/* /usr/bin/ 2>/dev/null || true + sudo cp ~/graphviz-cache/lib/* /usr/lib/x86_64-linux-gnu/ 2>/dev/null || true + sudo cp -r ~/graphviz-cache/share/graphviz /usr/share/ 2>/dev/null || true + sudo cp -r ~/graphviz-cache/lib/graphviz /usr/lib/x86_64-linux-gnu/ 2>/dev/null || true + sudo ldconfig + sudo dot -c + else + sudo apt-get update + sudo apt-get install -y graphviz + mkdir -p ~/graphviz-cache/{bin,lib,share} + cp /usr/bin/{dot,neato,twopi,circo,fdp,sfdp,patchwork,osage} ~/graphviz-cache/bin/ 2>/dev/null || true + cp /usr/lib/x86_64-linux-gnu/lib{gvc,cgraph,cdt,pathplan,gvpr,lab-gamut,ann,gts}* ~/graphviz-cache/lib/ 2>/dev/null || true + cp -r /usr/lib/x86_64-linux-gnu/graphviz ~/graphviz-cache/lib/ 2>/dev/null || true + cp -r /usr/share/graphviz ~/graphviz-cache/share/ 2>/dev/null || true + fi + + - name: Check test case count + run: poetry run python .templates/check_test_cases.py --threshold=10 --max=100 + + - name: Backup existing problems + run: | + mkdir -p .cache + cp -r leetcode .cache/ + - name: Delete existing problems run: rm -rf leetcode/*/ @@ -46,3 +80,15 @@ jobs: - name: Run linting to verify reproducibility run: make lint + + - name: Copy solution files from backup + run: | + for problem in .cache/leetcode/*/; do + problem_name=$(basename "$problem") + if [ -f "$problem/solution.py" ] && [ -d "leetcode/$problem_name" ]; then + cp "$problem/solution.py" "leetcode/$problem_name/solution.py" + fi + done + + - name: Run tests + run: make test diff --git a/.templates/check_test_cases.py b/.templates/check_test_cases.py index 4c36ffe..a99a310 100644 --- a/.templates/check_test_cases.py +++ b/.templates/check_test_cases.py @@ -82,10 +82,14 @@ def main( typer.echo(f"Invalid max_results value: {max_results}", err=True) raise typer.Exit(1) - typer.echo(f"Files with ≤{threshold} test cases ({len(filtered_files)} total):") + typer.echo(f"Problems with ≤{threshold} test cases ({len(filtered_files)} total):") for filename, count in filtered_files: typer.echo(f"{filename}: {count} test cases") + # Exit with non-zero code if any files found + if filtered_files: + raise typer.Exit(1) + if __name__ == "__main__": typer.run(main) diff --git a/.templates/leetcode/json/two_sum.json b/.templates/leetcode/json/two_sum.json new file mode 100644 index 0000000..a1c69c3 --- /dev/null +++ b/.templates/leetcode/json/two_sum.json @@ -0,0 +1,63 @@ +{ + "problem_name": "two_sum", + "solution_class_name": "Solution", + "problem_number": "1", + "problem_title": "Two Sum", + "difficulty": "Easy", + "topics": "Array, Hash Table", + "_tags": { "list": ["grind-75"] }, + "readme_description": "Given an array of integers `nums` and an integer `target`, return indices of the two numbers such that they add up to `target`.\n\nYou may assume that each input would have exactly one solution, and you may not use the same element twice.\n\nYou can return the answer in any order.", + "_readme_examples": { + "list": [ + { + "content": "```\nInput: nums = [2,7,11,15], target = 9\nOutput: [0,1]\n```\n**Explanation:** Because nums[0] + nums[1] == 9, we return [0, 1]." + }, + { "content": "```\nInput: nums = [3,2,4], target = 6\nOutput: [1,2]\n```" }, + { "content": "```\nInput: nums = [3,3], target = 6\nOutput: [0,1]\n```" } + ] + }, + "readme_constraints": "- 2 <= nums.length <= 10^4\n- -10^9 <= nums[i] <= 10^9\n- -10^9 <= target <= 10^9\n- Only one valid answer exists.", + "readme_additional": "**Follow-up:** Can you come up with an algorithm that is less than O(n^2) time complexity?", + "helpers_imports": "", + "helpers_content": "", + "helpers_run_name": "two_sum", + "helpers_run_signature": "(solution_class: type, nums: list[int], target: int)", + "helpers_run_body": " implementation = solution_class()\n return implementation.two_sum(nums, target)", + "helpers_assert_name": "two_sum", + "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_two_sum, run_two_sum\nfrom .solution import Solution", + "test_content": "", + "test_class_name": "TwoSum", + "test_class_content": " def setup_method(self):\n self.solution = Solution()", + "_solution_methods": { + "list": [ + { + "name": "two_sum", + "signature": "(self, nums: list[int], target: int) -> list[int]", + "body": " # TODO: Implement two_sum\n return []" + } + ] + }, + "_test_helper_methods": { + "list": [{ "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }] + }, + "_test_methods": { + "list": [ + { + "name": "test_two_sum", + "signature": "(self, nums: list[int], target: int, expected: list[int])", + "parametrize": "nums, target, expected", + "test_cases": "[([2, 7, 11, 15], 9, [0, 1]), ([3, 2, 4], 6, [1, 2]), ([3, 3], 6, [0, 1]), ([2, 5, 5, 11], 10, [1, 2]), ([1, 2, 3, 4, 5], 8, [2, 4]), ([0, 4, 3, 0], 0, [0, 3]), ([-1, -2, -3, -4, -5], -8, [2, 4]), ([1, 3, 4, 2], 6, [2, 3]), ([5, 75, 25], 100, [1, 2]), ([-3, 4, 3, 90], 0, [0, 2]), ([1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 2], 6, [5, 11]), ([2, 1, 9, 4, 4, 56, 90, 3], 8, [3, 4]), ([89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99], 185, [3, 4]), ([-1000000000, 1000000000], 0, [0, 1]), ([0, 1], 1, [0, 1]), ([1, 2], 5, []), ([3, 5, 7], 1, []), ([10, 20, 30], 15, [])]", + "body": " result = run_two_sum(Solution, nums, target)\n assert_two_sum(result, expected)" + } + ] + }, + "playground_imports": "from helpers import run_two_sum, assert_two_sum\nfrom solution import Solution", + "playground_setup": "# Example test case\nnums = [2, 7, 11, 15]\ntarget = 9\nexpected = [0, 1]", + "playground_run": "result = run_two_sum(Solution, nums, target)\nresult", + "playground_assert": "assert_two_sum(result, expected)" +} diff --git a/Makefile b/Makefile index 3bdc248..0f97dae 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= min_stack +PROBLEM ?= two_sum FORCE ?= 0 COMMA := , diff --git a/leetcode/two_sum/README.md b/leetcode/two_sum/README.md new file mode 100644 index 0000000..7f8a41d --- /dev/null +++ b/leetcode/two_sum/README.md @@ -0,0 +1,49 @@ +# Two Sum + +**Difficulty:** Easy +**Topics:** Array, Hash Table +**Tags:** grind-75 + +**LeetCode:** [Problem 1](https://leetcode.com/problems/two-sum/description/) + +## Problem Description + +Given an array of integers `nums` and an integer `target`, return indices of the two numbers such that they add up to `target`. + +You may assume that each input would have exactly one solution, and you may not use the same element twice. + +You can return the answer in any order. + +## Examples + +### Example 1: + +``` +Input: nums = [2,7,11,15], target = 9 +Output: [0,1] +``` + +**Explanation:** Because nums[0] + nums[1] == 9, we return [0, 1]. + +### Example 2: + +``` +Input: nums = [3,2,4], target = 6 +Output: [1,2] +``` + +### Example 3: + +``` +Input: nums = [3,3], target = 6 +Output: [0,1] +``` + +## Constraints + +- 2 <= nums.length <= 10^4 +- -10^9 <= nums[i] <= 10^9 +- -10^9 <= target <= 10^9 +- Only one valid answer exists. + +**Follow-up:** Can you come up with an algorithm that is less than O(n^2) time complexity? diff --git a/leetcode/two_sum/__init__.py b/leetcode/two_sum/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/two_sum/helpers.py b/leetcode/two_sum/helpers.py new file mode 100644 index 0000000..00f7b73 --- /dev/null +++ b/leetcode/two_sum/helpers.py @@ -0,0 +1,8 @@ +def run_two_sum(solution_class: type, nums: list[int], target: int): + implementation = solution_class() + return implementation.two_sum(nums, target) + + +def assert_two_sum(result: list[int], expected: list[int]) -> bool: + assert result == expected + return True diff --git a/leetcode/two_sum/playground.ipynb b/leetcode/two_sum/playground.ipynb new file mode 100644 index 0000000..1a6ec8b --- /dev/null +++ b/leetcode/two_sum/playground.ipynb @@ -0,0 +1,92 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from helpers import assert_two_sum, run_two_sum\n", + "from solution import Solution" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "nums = [2, 7, 11, 15]\n", + "target = 9\n", + "expected = [0, 1]" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "run", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[0, 1]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = run_two_sum(Solution, nums, target)\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "assert", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "assert_two_sum(result, expected)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "leetcode-py-py3.13", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/two_sum/solution.py b/leetcode/two_sum/solution.py new file mode 100644 index 0000000..ea4c593 --- /dev/null +++ b/leetcode/two_sum/solution.py @@ -0,0 +1,12 @@ +class Solution: + + # Time: O(n) + # Space: O(n) + def two_sum(self, nums: list[int], target: int) -> list[int]: + seen: dict[int, int] = {} + for i, num in enumerate(nums): + complement = target - num + if complement in seen: + return [seen[complement], i] + seen[num] = i + return [] diff --git a/leetcode/two_sum/test_solution.py b/leetcode/two_sum/test_solution.py new file mode 100644 index 0000000..75cb3f3 --- /dev/null +++ b/leetcode/two_sum/test_solution.py @@ -0,0 +1,39 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .helpers import assert_two_sum, run_two_sum +from .solution import Solution + + +class TestTwoSum: + def setup_method(self): + self.solution = Solution() + + @logged_test + @pytest.mark.parametrize( + "nums, target, expected", + [ + ([2, 7, 11, 15], 9, [0, 1]), + ([3, 2, 4], 6, [1, 2]), + ([3, 3], 6, [0, 1]), + ([2, 5, 5, 11], 10, [1, 2]), + ([1, 2, 3, 4, 5], 8, [2, 4]), + ([0, 4, 3, 0], 0, [0, 3]), + ([-1, -2, -3, -4, -5], -8, [2, 4]), + ([1, 3, 4, 2], 6, [2, 3]), + ([5, 75, 25], 100, [1, 2]), + ([-3, 4, 3, 90], 0, [0, 2]), + ([1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 2], 6, [5, 11]), + ([2, 1, 9, 4, 4, 56, 90, 3], 8, [3, 4]), + ([89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99], 185, [3, 4]), + ([-1000000000, 1000000000], 0, [0, 1]), + ([0, 1], 1, [0, 1]), + ([1, 2], 5, []), + ([3, 5, 7], 1, []), + ([10, 20, 30], 15, []), + ], + ) + def test_two_sum(self, nums: list[int], target: int, expected: list[int]): + result = run_two_sum(Solution, nums, target) + assert_two_sum(result, expected)