diff --git a/GALLERY.md b/GALLERY.md index 1d09a54f..01152c21 100644 --- a/GALLERY.md +++ b/GALLERY.md @@ -49,6 +49,7 @@ This gallery shows examples from all available datasets using their default conf - [tower_of_hanoi](#tower_of_hanoi) - [word_ladder](#word_ladder) - [group_anagrams](#group_anagrams) +- [spiral_matrix](#spiral_matrix) - [word_sequence_reversal](#word_sequence_reversal) - [word_sorting](#word_sorting) - [zebra_puzzles](#zebra_puzzles) @@ -2294,6 +2295,127 @@ Metadata: {'words': ['eagerest', 'granitite', 'helium', 'nizam', 'nazim', 'strip -------------------------------------------------- ``` +### spiral_matrix +Print elements of a matrix in spiral order + +Default configuration +```python +max_rows: int = 10 # Maximum number of rows in the matrix +max_cols: int = 10 # Maximum number of columns in the matrix +size = 500 +``` + +``` +Sample 1: +Question: Given a matrix, your job is to generate a list of elements in spiral order. + +Example: + +Input: +1 2 3 4 +5 6 7 8 +9 10 11 12 + +Output: 1 2 3 4 8 12 11 10 9 5 6 7 + +For the matrix below, what is the list of elements in spiral order? +2 +1 +0 + +Answer: 2 1 0 + +Metadata: {'matrix': [[2], [1], [0]], 'solution': [2, 1, 0]} + +-------------------------------------------------- + +Sample 2: +Question: Given a matrix, your job is to generate a list of elements in spiral order. + +Example: + +Input: +1 2 3 4 +5 6 7 8 +9 10 11 12 + +Output: 1 2 3 4 8 12 11 10 9 5 6 7 + +For the matrix below, what is the list of elements in spiral order? +1 0 2 + +Answer: 1 0 2 + +Metadata: {'matrix': [[1, 0, 2]], 'solution': [1, 0, 2]} + +-------------------------------------------------- + +Sample 3: +Question: Given a matrix, your job is to generate a list of elements in spiral order. + +Example: + +Input: +1 2 3 4 +5 6 7 8 +9 10 11 12 + +Output: 1 2 3 4 8 12 11 10 9 5 6 7 + +For the matrix below, what is the list of elements in spiral order? +1 +0 + +Answer: 1 0 + +Metadata: {'matrix': [[1], [0]], 'solution': [1, 0]} + +-------------------------------------------------- + +Sample 4: +Question: Given a matrix, your job is to generate a list of elements in spiral order. + +Example: + +Input: +1 2 3 4 +5 6 7 8 +9 10 11 12 + +Output: 1 2 3 4 8 12 11 10 9 5 6 7 + +For the matrix below, what is the list of elements in spiral order? +1 3 5 4 +6 0 2 7 + +Answer: 1 3 5 4 7 2 0 6 + +Metadata: {'matrix': [[1, 3, 5, 4], [6, 0, 2, 7]], 'solution': [1, 3, 5, 4, 7, 2, 0, 6]} + +-------------------------------------------------- + +Sample 5: +Question: Given a matrix, your job is to generate a list of elements in spiral order. + +Example: + +Input: +1 2 3 4 +5 6 7 8 +9 10 11 12 + +Output: 1 2 3 4 8 12 11 10 9 5 6 7 + +For the matrix below, what is the list of elements in spiral order? +1 3 2 0 + +Answer: 1 3 2 0 + +Metadata: {'matrix': [[1, 3, 2, 0]], 'solution': [1, 3, 2, 0]} + +-------------------------------------------------- +``` + ### word_sequence_reversal diff --git a/README.md b/README.md index 9335a1d2..f177c0bf 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ See the [Dataset Gallery](GALLERY.md) for a complete list of available datasets - `WordSequenceReversalDataset`: Reverse word order in text spans - `WordLadderDataset`: Generate word ladder puzzles where one word is transformed into another by changing one letter at a time - `GroupAnagramsDataset`: Group anagrams together in a list of words +- `SprialMatrixDataset`: Print elements of a matrix in spiral order ### Code Tasks diff --git a/reasoning_gym/algorithmic/__init__.py b/reasoning_gym/algorithmic/__init__.py index 4e4688bf..b2f8709a 100644 --- a/reasoning_gym/algorithmic/__init__.py +++ b/reasoning_gym/algorithmic/__init__.py @@ -19,6 +19,7 @@ from .spell_backward import SpellBackwardConfig, SpellBackwardDataset from .word_ladder import WordLadderConfig, WordLadderDataset from .word_sequence_reversal import WordSequenceReversalConfig, WordSequenceReversalDataset from .word_sorting import TextTransformation, WordSortingConfig, WordSortingDataset +from .spiral_matrix import SpiralMatrixConfig, SpiralMatrixDataset __all__ = [ "SpellBackwardConfig", @@ -48,4 +49,6 @@ __all__ = [ "PalindromeDataset", "GroupAnagramsConfig", "GroupAnagramsDataset", + "SpiralMatrixConfig", + "SpiralMatrixDataset", ] diff --git a/reasoning_gym/algorithmic/spiral_matrix.py b/reasoning_gym/algorithmic/spiral_matrix.py new file mode 100644 index 00000000..a7d40d86 --- /dev/null +++ b/reasoning_gym/algorithmic/spiral_matrix.py @@ -0,0 +1,113 @@ +"""Print elements of a matrix in spiral order. + +A popular Leetcode problem: +https://leetcode.com/problems/spiral-matrix/description/ +""" + +from dataclasses import dataclass +from random import Random +from typing import Optional + +from ..factory import ProceduralDataset, register_dataset + +QUESTION_TEMPLATE = """Given a matrix, your job is to generate a list of elements in spiral order. + +Example: + +Input: +1 2 3 4 +5 6 7 8 +9 10 11 12 + +Output: 1 2 3 4 8 12 11 10 9 5 6 7 + +For the matrix below, what is the list of elements in spiral order? +{matrix} +""" + + +@dataclass +class SpiralMatrixConfig: + """Configuration for Spiral Matrix dataset generation""" + + max_rows: int = 10 # Maximum number of rows in the matrix + max_cols: int = 10 # Maximum number of columns in the matrix + + size: int = 500 # Virtual dataset size + seed: Optional[int] = None + + def validate(self): + """Validate configuration parameters""" + assert 1 <= self.max_rows, "max_rows must be at least 1" + assert 1 <= self.max_cols, "max_cols must be at least 1" + + +class SpiralMatrixDataset(ProceduralDataset): + """Generates Spiral Matrix exercises with configurable difficulty""" + + def __init__(self, config: SpiralMatrixConfig): + super().__init__(config=config, seed=config.seed, size=config.size) + + def _get_matrix(self, rng: Random) -> list[list[int]]: + """Generate a random matrix""" + rows, cols = rng.randint(1, self.config.max_rows), rng.randint(1, self.config.max_cols) + numbers = list(range(rows * cols)) + rng.shuffle(numbers) + matrix = [numbers[i * cols : (i + 1) * cols] for i in range(rows)] + return matrix + + def _get_spiral(self, matrix: list[list[int]]) -> list[int]: + """Return the elements of the matrix in spiral order""" + t, b = 0, len(matrix) + l, r = 0, len(matrix[0]) + + out = [] + + while True: + for i in range(l, r): + out.append(matrix[t][i]) + t += 1 + if t == b: break + + for i in range(t, b): + out.append(matrix[i][r-1]) + r -= 1 + if l == r: break + + for i in range(r-1, l-1, -1): + out.append(matrix[b-1][i]) + b -= 1 + if t == b: break + + for i in range(b-1, t-1, -1): + out.append(matrix[i][l]) + l += 1 + if l == r: break + + return out + + def _matrix_to_str(self, matrix: list[list[int]]) -> str: + """Get a string representation of the matrix""" + return "\n".join(" ".join(str(x) for x in row) for row in matrix) + + def _list_to_str(self, array: list[int]) -> str: + """Get a string representation of the array""" + return " ".join(str(x) for x in array) + + def __getitem__(self, idx: int) -> dict: + """Generate a single Spiral Matrix question""" + rng = Random(self.seed + idx) + + matrix = self._get_matrix(rng) + matrix_str = self._matrix_to_str(matrix) + answer = self._get_spiral(matrix) + answer_str = self._list_to_str(answer) + + return { + "question": QUESTION_TEMPLATE.format(matrix=matrix_str), + "answer": answer_str, + "metadata": {"matrix": matrix, "solution": answer}, + } + + +register_dataset("spiral_matrix", SpiralMatrixDataset, SpiralMatrixConfig) diff --git a/tests/test_spiral_matrix.py b/tests/test_spiral_matrix.py new file mode 100644 index 00000000..93578534 --- /dev/null +++ b/tests/test_spiral_matrix.py @@ -0,0 +1,95 @@ +"""Tests for Spiral Matrix questions generation""" + +import pytest + +from reasoning_gym.algorithmic.spiral_matrix import SpiralMatrixConfig, SpiralMatrixDataset + + +def test_spiral_matrix_config_validation(): + """Test that invalid configs raise appropriate errors""" + with pytest.raises(AssertionError): + config = SpiralMatrixConfig(max_rows=-1) # Negative not allowed + config.validate() + + with pytest.raises(AssertionError): + config = SpiralMatrixConfig(max_rows=0) # Zero not allowed + config.validate() + + with pytest.raises(AssertionError): + config = SpiralMatrixConfig(max_cols=-1) # Negative not allowed + config.validate() + + with pytest.raises(AssertionError): + config = SpiralMatrixConfig(max_cols=0) # Zero not allowed + config.validate() + + +def test_spiral_matrix_dataset_deterministic(): + """Test that dataset generates same items with same seed""" + config = SpiralMatrixConfig(seed=42, size=10) + dataset1 = SpiralMatrixDataset(config) + dataset2 = SpiralMatrixDataset(config) + + for i in range(len(dataset1)): + assert dataset1[i] == dataset2[i] + + +def test_spiral_matrix_dataset_items(): + """Test basic properties of generated items""" + config = SpiralMatrixConfig(max_rows=5, max_cols=5, size=10, seed=42) + dataset = SpiralMatrixDataset(config) + + for i in range(len(dataset)): + item = dataset[i] + # Check item structure + assert isinstance(item, dict) + assert "question" in item + assert "answer" in item + assert "metadata" in item + + # Check metadata + assert "matrix" in item["metadata"] + assert "solution" in item["metadata"] + + matrix = item["metadata"]["matrix"] + solution = item["metadata"]["solution"] + + # Verify list dimensions + assert len(matrix) <= config.max_rows + assert all(len(row) <= config.max_cols for row in matrix) + assert sum(len(row) for row in matrix) == len(solution) + assert len(list(set(solution))) == len(solution) + + +def test_spiral_matrix_dataset_iteration(): + """Test that iteration respects dataset size""" + config = SpiralMatrixConfig(size=5, seed=42) + dataset = SpiralMatrixDataset(config) + + items = list(dataset) + assert len(items) == config.size + + # Test multiple iterations yield same items + assert items == list(dataset) + + +def test_spiral_matrix_answer(): + """Test the _get_spiral method""" + config = SpiralMatrixConfig(seed=42) + dataset = SpiralMatrixDataset(config) + + # One element + matrix = [[0]] + assert dataset._get_spiral(matrix) == [0] + + # One row + matrix = [[0, 1, 2]] + assert dataset._get_spiral(matrix) == [0, 1, 2] + + # One column + matrix = [[0], [1], [2]] + assert dataset._get_spiral(matrix) == [0, 1, 2] + + # 2D grid + matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + assert dataset._get_spiral(matrix) == [1, 2, 3, 6, 9, 8, 7, 4, 5]