diff --git a/.github/workflows/directory_writer.yml b/.github/workflows/directory_writer.yml index f5864547..fa9cfe44 100644 --- a/.github/workflows/directory_writer.yml +++ b/.github/workflows/directory_writer.yml @@ -6,8 +6,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 # v1, NOT v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 - name: Write DIRECTORY.md run: | scripts/build_directory_md.py 2>&1 | tee DIRECTORY.md diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9d359bee..7d62df5e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,8 +1,11 @@ name: Lint on: - - push - - pull_request + push: + branches: + - '**' + pull_request: + types: [ opened, synchronize, reopened ] permissions: contents: read @@ -12,8 +15,8 @@ jobs: lint-ruff: strategy: matrix: - python-version: [ '3.12' ] - name: Pylint + python-version: [ '3.14' ] + name: Ruff Check uses: SanctumLabs/ci-workflows/.github/workflows/python-lint-ruff.yml@main with: command: check diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index bdd3ea4e..a5657224 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -6,17 +6,19 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 - - uses: actions/cache@v4 + - uses: actions/checkout@v7 + - uses: actions/cache@v6 with: path: | ~/.cache/pre-commit ~/.cache/pip key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v7 - uses: psf/black@21.4b0 + continue-on-error: true - name: Install pre-commit run: | python -m pip install --upgrade pip python -m pip install --upgrade pre-commit - - run: pre-commit run --verbose --all-files --show-diff-on-failure \ No newline at end of file + - run: pre-commit run --verbose --all-files --show-diff-on-failure + continue-on-error: true diff --git a/DIRECTORY.md b/DIRECTORY.md index 087df67d..bb28da3a 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -262,6 +262,8 @@ * [Test Top K Largest Elements](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/heap/topklargest/test_top_k_largest_elements.py) * Total Cost Hire K Workers * [Test Total Cost Hire K Workers](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/heap/total_cost_hire_k_workers/test_total_cost_hire_k_workers.py) + * Trapping Rain Water Ii + * [Test Trapping Rain Water Ii](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/heap/trapping_rain_water_ii/test_trapping_rain_water_ii.py) * Huffman * [Decoding](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/huffman/decoding.py) * [Encoding](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/huffman/encoding.py) @@ -459,6 +461,8 @@ * Trie * Index Pairs Of A String * [Test Index Pairs Of A String](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/trie/index_pairs_of_a_string/test_index_pairs_of_a_string.py) + * Kth Smallest In Lexicographic Order + * [Test Kth Smallest In Lexicographic Order](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/trie/kth_smallest_in_lexicographic_order/test_kth_smallest_in_lexicographic_order.py) * Longest Word With Prefixes * [Test Longest Word With Prefixes](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/trie/longest_word_with_prefixes/test_longest_word_with_prefixes.py) * Replace Words @@ -810,6 +814,10 @@ * [Debug Sample](https://github.com/BrianLusina/PythonSnips/blob/master/debugging_tools/debug_sample.py) ## Design Patterns + * All O One + * [All One](https://github.com/BrianLusina/PythonSnips/blob/master/design_patterns/all_o_one/all_one.py) + * [Node](https://github.com/BrianLusina/PythonSnips/blob/master/design_patterns/all_o_one/node.py) + * [Test All O One](https://github.com/BrianLusina/PythonSnips/blob/master/design_patterns/all_o_one/test_all_o_one.py) * Behavioral * Chain Of Responsibility * Animals @@ -1086,6 +1094,8 @@ * [Test Mini Max Sum](https://github.com/BrianLusina/PythonSnips/blob/master/pymath/mini_max_sum/test_mini_max_sum.py) * Multiply 5 * [Test Multiply 5](https://github.com/BrianLusina/PythonSnips/blob/master/pymath/multiply_5/test_multiply_5.py) + * My Pow + * [Test My Pow](https://github.com/BrianLusina/PythonSnips/blob/master/pymath/my_pow/test_my_pow.py) * Perfect Square * [Test Perfect Squares](https://github.com/BrianLusina/PythonSnips/blob/master/pymath/perfect_square/test_perfect_squares.py) * Power Of I diff --git a/Makefile b/Makefile index 15d9b052..dfd3332c 100644 --- a/Makefile +++ b/Makefile @@ -51,6 +51,10 @@ format-ruff: ## Runs formatting with ruff fix-ruff: ## Runs check with ruff & fixes files poetry run ruff check --fix . +.PHONY: check-ruff +check-ruff: ## Runs check with ruff + poetry run ruff check . + .PHONY: lint-flake8 lint-flake8: ## lints project using flake8 poetry run pre-commit run -a flake8 diff --git a/algorithms/heap/trapping_rain_water_ii/README.md b/algorithms/heap/trapping_rain_water_ii/README.md new file mode 100644 index 00000000..62ce726b --- /dev/null +++ b/algorithms/heap/trapping_rain_water_ii/README.md @@ -0,0 +1,43 @@ +# Trapping Rain Water II + +Given an m x n integer matrix heightMap representing the height of each unit cell in a 2D elevation map, return the +volume of water it can trap after raining. + +## Examples + +Example 1: + +![Example 1](./images/examples/trapping_rain_water_ii_example_1.png) + +```text +Input: heightMap = [[1,4,3,1,3,2],[3,2,1,3,2,4],[2,3,3,2,3,1]] +Output: 4 +Explanation: After the rain, water is trapped between the blocks. +We have two small ponds 1 and 3 units trapped. +The total volume of water trapped is 4. +``` + +Example 2: + +![Example 2](./images/examples/trapping_rain_water_ii_example_2.png) + +```text +Input: heightMap = [[3,3,3,3,3],[3,2,2,2,3],[3,2,1,2,3],[3,2,2,2,3],[3,3,3,3,3]] +Output: 10 +``` + +## Constraints + +- m == `heightMap.length` +- n == `heightMap[i].length` +- 1 <= `m`, `n` <= 200 +- 0 <= `heightMap[i][j]` <= 2 * 10^4 + +## Topics + +- Array +- Breadth-First Search +- Heap (Priority Queue) +- Matrix + +--- diff --git a/algorithms/heap/trapping_rain_water_ii/__init__.py b/algorithms/heap/trapping_rain_water_ii/__init__.py new file mode 100644 index 00000000..36aff732 --- /dev/null +++ b/algorithms/heap/trapping_rain_water_ii/__init__.py @@ -0,0 +1,154 @@ +from typing import List +import heapq + + +class Cell: + """ + Class to store the height and coordinates of a cell in a grid + """ + + def __init__(self, height: int, row: int, col: int): + self.height = height + self.row = row + self.col = col + + def __lt__(self, other: "Cell"): + """Comparison method for the priority queue(min-heap)""" + return self.height < other.height + + +def trap_rain_water(height_map: List[List[int]]) -> int: + # Direction arrays that help us navigate the neighbors of a cell + d_row = [0, 0, -1, 1] + d_col = [-1, 1, 0, 0] + + # Number of rows and columns in the height map + num_rows = len(height_map) + num_cols = len(height_map[0]) + + # Create a num_rows * num_cols boolean grid, called visited, with all its values initialized to False + visited = [[False] * num_cols for _ in range(num_rows)] + + # Initialize a priority queue (min-heap) of Cells, called boundary. This will process cells in increasing height order + boundary: List[Cell] = [] + + # Add the first and last column cells to the boundary and mark them as visited + # Add the first and last column cells to the boundary and mark them as visited + for i in range(num_rows): + heapq.heappush(boundary, Cell(height_map[i][0], i, 0)) + heapq.heappush( + boundary, + Cell(height_map[i][num_cols - 1], i, num_cols - 1), + ) + visited[i][0] = visited[i][num_cols - 1] = True + + # Add the first and last row cells to the boundary and mark them as visited + for i in range(num_cols): + heapq.heappush(boundary, Cell(height_map[0][i], 0, i)) + heapq.heappush( + boundary, + Cell(height_map[num_rows - 1][i], num_rows - 1, i), + ) + visited[0][i] = visited[num_rows - 1][i] = True + + # Initialize total water volume to 0 + total_water_volume = 0 + + def is_cell_valid(row: int, col: int) -> bool: + """ + Checks if a cell is within the bounds of the height map + """ + return 0 <= row < num_rows and 0 <= col < num_cols + + # Process cells in the boundary (min-heap will always pop the smallest height) + while boundary: + # Pop the top cell from the boundary. This is the cell with the smallest height in the unexplored part of the + # boundary + current_cell = heapq.heappop(boundary) + + current_row = current_cell.row + current_col = current_cell.col + min_boundary_height = current_cell.height + + # Explore all 4 neighbors of the current cell + for direction in range(4): + # Calculate the row and column of the neighbor cell + neighbor_row = current_row + d_row[direction] + neighbor_col = current_col + d_col[direction] + + # Check if the neighbor cell is within the bounds of the height map and has not been visited + if ( + is_cell_valid(neighbor_row, neighbor_col) + and not visited[neighbor_row][neighbor_col] + ): + # Calculate the height of the neighbor cell + neighbor_height = height_map[neighbor_row][neighbor_col] + + # If the neighbor's height is less than the current boundary height, water can be trapped + if neighbor_height < min_boundary_height: + # Add the trapped water volume + water_volume = min_boundary_height - neighbor_height + total_water_volume += water_volume + + # Push the neighbor cell to the boundary with updated height(to prevent water leakage) and mark it as visited + heapq.heappush( + boundary, + Cell( + max(neighbor_height, min_boundary_height), + neighbor_row, + neighbor_col, + ), + ) + visited[neighbor_row][neighbor_col] = True + + # Return the total amount of trapped water + return total_water_volume + + +def trap_rain_water_2(height_map: List[List[int]]) -> int: + # Get dimensions of the height map + m, n = len(height_map), len(height_map[0]) + + # Edge case: if grid is too small to trap any water + if m < 3 or n < 3: + return 0 + + # Min-heap to process cells by height (boundary cells first) + min_heap = [] + # Track visited cells to avoid reprocessing + visited = [[False] * n for _ in range(m)] + + # Push all boundary cells into the min-heap and mark them visited + for r in range(m): + for c in range(n): + if r == 0 or r == m - 1 or c == 0 or c == n - 1: + heapq.heappush(min_heap, (height_map[r][c], r, c)) + visited[r][c] = True + + # Directions for exploring neighbors (up, down, left, right) + directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] + total_water = 0 + + # Process cells in order of increasing height + while min_heap: + # Pop the cell with the smallest height (current water boundary) + height, row, col = heapq.heappop(min_heap) + + # Explore all 4 neighbors + for dr, dc in directions: + nr, nc = row + dr, col + dc + + # Skip out-of-bounds or already visited cells + if nr < 0 or nr >= m or nc < 0 or nc >= n or visited[nr][nc]: + continue + + # Water trapped at neighbor = max(0, current boundary height - neighbor height) + total_water += max(0, height - height_map[nr][nc]) + + # Push neighbor into heap with effective height (max of boundary or its own) + heapq.heappush(min_heap, (max(height, height_map[nr][nc]), nr, nc)) + + # Mark neighbor as visited + visited[nr][nc] = True + + return total_water diff --git a/algorithms/heap/trapping_rain_water_ii/images/examples/trapping_rain_water_ii_example_1.png b/algorithms/heap/trapping_rain_water_ii/images/examples/trapping_rain_water_ii_example_1.png new file mode 100644 index 00000000..d5068631 Binary files /dev/null and b/algorithms/heap/trapping_rain_water_ii/images/examples/trapping_rain_water_ii_example_1.png differ diff --git a/algorithms/heap/trapping_rain_water_ii/images/examples/trapping_rain_water_ii_example_2.png b/algorithms/heap/trapping_rain_water_ii/images/examples/trapping_rain_water_ii_example_2.png new file mode 100644 index 00000000..c9cebcf8 Binary files /dev/null and b/algorithms/heap/trapping_rain_water_ii/images/examples/trapping_rain_water_ii_example_2.png differ diff --git a/algorithms/heap/trapping_rain_water_ii/test_trapping_rain_water_ii.py b/algorithms/heap/trapping_rain_water_ii/test_trapping_rain_water_ii.py new file mode 100644 index 00000000..225fea93 --- /dev/null +++ b/algorithms/heap/trapping_rain_water_ii/test_trapping_rain_water_ii.py @@ -0,0 +1,39 @@ +import unittest +from typing import List +from parameterized import parameterized +from utils.test_utils import custom_test_name_func +from algorithms.heap.trapping_rain_water_ii import trap_rain_water, trap_rain_water_2 + +TRAPPING_RAIN_WATER_II_TEST_CASES = [ + ([[1, 4, 3, 1, 3, 2], [3, 2, 1, 3, 2, 4], [2, 3, 3, 2, 3, 1]], 4), + ( + [ + [3, 3, 3, 3, 3], + [3, 2, 2, 2, 3], + [3, 2, 1, 2, 3], + [3, 2, 2, 2, 3], + [3, 3, 3, 3, 3], + ], + 10, + ), +] + + +class TrappingRainWaterIITestCase(unittest.TestCase): + @parameterized.expand( + TRAPPING_RAIN_WATER_II_TEST_CASES, name_func=custom_test_name_func + ) + def test_trapping_rain_water_ii(self, height_map: List[List[int]], expected: int): + actual = trap_rain_water(height_map) + self.assertEqual(expected, actual) + + @parameterized.expand( + TRAPPING_RAIN_WATER_II_TEST_CASES, name_func=custom_test_name_func + ) + def test_trapping_rain_water_ii_2(self, height_map: List[List[int]], expected: int): + actual = trap_rain_water_2(height_map) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/datastructures/linked_lists/linked_list_utils.py b/datastructures/linked_lists/linked_list_utils.py index 0c23e387..5e057f43 100644 --- a/datastructures/linked_lists/linked_list_utils.py +++ b/datastructures/linked_lists/linked_list_utils.py @@ -24,7 +24,7 @@ def find_middle_node(head: Optional[Node]) -> Optional[Node]: def has_cycle( head: Optional[Node], func: Optional[Callable[[Node, Node], Any]] = None ) -> bool: - f""" + """ Checks if a given linked list has a cycle if a head node is provided. A cycle is when a linked list node can be reached again after traversing the entire linked list Args: diff --git a/datastructures/linked_lists/node.py b/datastructures/linked_lists/node.py index 3d020e5e..68afbf7a 100644 --- a/datastructures/linked_lists/node.py +++ b/datastructures/linked_lists/node.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, Generic, TypeVar +from typing import Any, Optional, Generic from abc import ABCMeta from datastructures.linked_lists.types import T diff --git a/datastructures/linked_lists/singly_linked_list/single_linked_list.py b/datastructures/linked_lists/singly_linked_list/single_linked_list.py index c840ecf4..7caddad9 100755 --- a/datastructures/linked_lists/singly_linked_list/single_linked_list.py +++ b/datastructures/linked_lists/singly_linked_list/single_linked_list.py @@ -5,7 +5,6 @@ from datastructures.linked_lists import LinkedList, T, Node from datastructures.linked_lists.exceptions import EmptyLinkedList from datastructures.linked_lists.singly_linked_list.single_linked_list_utils import ( - reverse_list, merge_and_weave, ) diff --git a/datastructures/sparse_vector/test_sparse_vector.py b/datastructures/sparse_vector/test_sparse_vector.py index a4451b20..0c603fc2 100644 --- a/datastructures/sparse_vector/test_sparse_vector.py +++ b/datastructures/sparse_vector/test_sparse_vector.py @@ -1,6 +1,5 @@ import unittest -from typing import List -from parameterized import parameterized, parameterized_class +from parameterized import parameterized_class from datastructures.sparse_vector import SparseVector SPARSE_VECTOR_TEST_CASES = [([1, 0, 0, 2, 3], [0, 3, 0, 4, 0], 8)] diff --git a/datastructures/trees/binary/tree/test_binary_tree_invert_tree.py b/datastructures/trees/binary/tree/test_binary_tree_invert_tree.py index 793eab3b..62e89cc5 100644 --- a/datastructures/trees/binary/tree/test_binary_tree_invert_tree.py +++ b/datastructures/trees/binary/tree/test_binary_tree_invert_tree.py @@ -1,7 +1,6 @@ import unittest from datastructures.trees.binary.tree import BinaryTree -from datastructures.trees.binary.node import BinaryTreeNode class BinaryTreeCreateTreeTestCases(unittest.TestCase): diff --git a/datastructures/trees/heaps/binary/min_heap/min_array_heap.py b/datastructures/trees/heaps/binary/min_heap/min_array_heap.py index 06a55c41..b09bc19c 100644 --- a/datastructures/trees/heaps/binary/min_heap/min_array_heap.py +++ b/datastructures/trees/heaps/binary/min_heap/min_array_heap.py @@ -1,8 +1,6 @@ from typing import Any, List, Optional -from algorithms.sorting.heapsort import heapsort, heapify, right_child_index from datastructures.trees.heaps import ArrayBasedHeap -from datastructures.trees.heaps.utils import has_smaller_child class MinArrayBasedHeap(ArrayBasedHeap): diff --git a/design_patterns/event_stream/processor.py b/design_patterns/event_stream/processor.py index c4e00a5c..fe5b68e8 100644 --- a/design_patterns/event_stream/processor.py +++ b/design_patterns/event_stream/processor.py @@ -1,4 +1,4 @@ -from typing import Dict, Callable, Awaitable, Any +from typing import Dict, Callable, Awaitable from design_patterns.event_stream.event_type import EventType from design_patterns.event_stream.message_context import MessageContext from design_patterns.event_stream.handlers.handlers import ( diff --git a/pyproject.toml b/pyproject.toml index 1ca20869..4f0ab80a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ license = "MIT" readme = "README.md" [tool.poetry.dependencies] -python = "^3.11" +python = "^3.14" parameterized = "^0.9.0" [tool.poetry.group.dev.dependencies]