mirror of
https://github.com/open-thought/reasoning-gym.git
synced 2026-04-19 12:58:07 +00:00
ransom note
This commit is contained in:
parent
426fa22fcc
commit
b24da41e69
5 changed files with 300 additions and 0 deletions
86
GALLERY.md
86
GALLERY.md
|
|
@ -20,6 +20,7 @@ This gallery shows examples from all available datasets using their default conf
|
|||
- [game_of_life](#game_of_life)
|
||||
- [gcd](#gcd)
|
||||
- [group_anagrams](#group_anagrams)
|
||||
- [ransom_note](#ransom_note)
|
||||
- [gsm_symbolic](#gsm_symbolic)
|
||||
- [intermediate_integration](#intermediate_integration)
|
||||
- [largest_island](#largest_island)
|
||||
|
|
@ -967,6 +968,91 @@ Metadata: {'words': ['eagerest', 'granitite', 'helium', 'nizam', 'nazim', 'strip
|
|||
|
||||
````
|
||||
|
||||
|
||||
### ransom_note
|
||||
|
||||
Check if you can construct a ransom note from letters in a magazine
|
||||
|
||||
Default configuration
|
||||
```python
|
||||
max_note_length: int = 10 # Maximum length of the ransom note
|
||||
max_magazine_length = 30 # Maximum length of the magazine
|
||||
p_solvable: float = 0.5 # Probability that the ransom note can be constructed
|
||||
```
|
||||
|
||||
Example tasks:
|
||||
````
|
||||
Sample 1:
|
||||
Question: Given two strings representing a ransom note and a magazine, return True if you can construct the ransom note using the letters in the magazine, and False otherwise.
|
||||
|
||||
Each letter in the magazine string can only be used once in your ransom note.
|
||||
|
||||
Ransom note: w
|
||||
Magazine: addyplkvt
|
||||
|
||||
Answer: False
|
||||
|
||||
Metadata: {'ransom_note': 'w', 'magazine': 'addyplkvt', 'solution': False, 'solvable': False}
|
||||
|
||||
--------------------------------------------------
|
||||
|
||||
Sample 2:
|
||||
Question: Given two strings representing a ransom note and a magazine, return True if you can construct the ransom note using the letters in the magazine, and False otherwise.
|
||||
|
||||
Each letter in the magazine string can only be used once in your ransom note.
|
||||
|
||||
Ransom note: ztx
|
||||
Magazine: zizohkpdrhvdoaxtrxosmerfb
|
||||
|
||||
Answer: True
|
||||
|
||||
Metadata: {'ransom_note': 'ztx', 'magazine': 'zizohkpdrhvdoaxtrxosmerfb', 'solution': True, 'solvable': True}
|
||||
|
||||
--------------------------------------------------
|
||||
|
||||
Sample 3:
|
||||
Question: Given two strings representing a ransom note and a magazine, return True if you can construct the ransom note using the letters in the magazine, and False otherwise.
|
||||
|
||||
Each letter in the magazine string can only be used once in your ransom note.
|
||||
|
||||
Ransom note: rdmalrnnc
|
||||
Magazine: nzbepjclllltnmdaucybmraddrr
|
||||
|
||||
Answer: True
|
||||
|
||||
Metadata: {'ransom_note': 'rdmalrnnc', 'magazine': 'nzbepjclllltnmdaucybmraddrr', 'solution': True, 'solvable': True}
|
||||
|
||||
--------------------------------------------------
|
||||
|
||||
Sample 4:
|
||||
Question: Given two strings representing a ransom note and a magazine, return True if you can construct the ransom note using the letters in the magazine, and False otherwise.
|
||||
|
||||
Each letter in the magazine string can only be used once in your ransom note.
|
||||
|
||||
Ransom note: cjyaqjka
|
||||
Magazine: crjqaiajkyc
|
||||
|
||||
Answer: True
|
||||
|
||||
Metadata: {'ransom_note': 'cjyaqjka', 'magazine': 'crjqaiajkyc', 'solution': True, 'solvable': True}
|
||||
|
||||
--------------------------------------------------
|
||||
|
||||
Sample 5:
|
||||
Question: Given two strings representing a ransom note and a magazine, return True if you can construct the ransom note using the letters in the magazine, and False otherwise.
|
||||
|
||||
Each letter in the magazine string can only be used once in your ransom note.
|
||||
|
||||
Ransom note: eusvpne
|
||||
Magazine: irzhvekszobxnsrexnpkltchp
|
||||
|
||||
Answer: False
|
||||
|
||||
Metadata: {'ransom_note': 'eusvpne', 'magazine': 'irzhvekszobxnsrexnpkltchp', 'solution': False, 'solvable': False}
|
||||
|
||||
--------------------------------------------------
|
||||
````
|
||||
|
||||
### gsm_symbolic
|
||||
Default configuration:
|
||||
```python
|
||||
|
|
|
|||
|
|
@ -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
|
||||
- `RansomNoteDataset`: Check if a ransom note can be created from a given set of letters in a magazine
|
||||
|
||||
### <small>Code Tasks</small>
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from .letter_jumble import LetterJumbleConfig, LetterJumbleDataset
|
|||
from .number_filtering import NumberFilteringConfig, NumberFilteringDataset
|
||||
from .number_sorting import NumberSortingConfig, NumberSortingDataset
|
||||
from .palindrome_generation import PalindromeConfig, PalindromeDataset
|
||||
from .ransom_note import RansomNoteConfig, RansomNoteDataset
|
||||
from .sentence_reordering import SentenceReorderingConfig, SentenceReorderingDataset
|
||||
from .spell_backward import SpellBackwardConfig, SpellBackwardDataset
|
||||
from .word_ladder import WordLadderConfig, WordLadderDataset
|
||||
|
|
@ -48,4 +49,6 @@ __all__ = [
|
|||
"PalindromeDataset",
|
||||
"GroupAnagramsConfig",
|
||||
"GroupAnagramsDataset",
|
||||
"RansomNoteConfig",
|
||||
"RansomNoteDataset",
|
||||
]
|
||||
|
|
|
|||
99
reasoning_gym/algorithmic/ransom_note.py
Normal file
99
reasoning_gym/algorithmic/ransom_note.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
"""Check if you can construct a ransom note from letters in a magazine.
|
||||
|
||||
A popular Leetcode problem:
|
||||
https://leetcode.com/problems/ransom-note/description/
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from random import Random
|
||||
from typing import Optional
|
||||
|
||||
from ..factory import ProceduralDataset, register_dataset
|
||||
|
||||
MAX_NOTE_LENGTH = 100_000
|
||||
MAX_MAGAZINE_LENGTH = 100_001
|
||||
|
||||
QUESTION_TEMPLATE = """Given two strings representing a ransom note and a magazine, return True if you can construct the ransom note using the letters in the magazine, and False otherwise.
|
||||
|
||||
Each letter in the magazine string can only be used once in your ransom note.
|
||||
|
||||
Ransom note: {ransom_note}
|
||||
Magazine: {magazine}
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class RansomNoteConfig:
|
||||
"""Configuration for Ransom Note dataset generation"""
|
||||
|
||||
max_note_length: int = 10 # Maximum length of the ransom note
|
||||
max_magazine_length: int = 30 # Maximum length of the magazine
|
||||
p_solvable: float = 0.5 # Probability that the ransom note can be constructed
|
||||
|
||||
size: int = 500 # Virtual dataset size
|
||||
seed: Optional[int] = None
|
||||
|
||||
def validate(self):
|
||||
"""Validate configuration parameters"""
|
||||
assert 1 <= self.max_note_length <= MAX_NOTE_LENGTH, "max_note_length must be between 1 and MAX_NOTE_LENGTH"
|
||||
assert (
|
||||
2 <= self.max_magazine_length <= MAX_MAGAZINE_LENGTH
|
||||
), "max_magazine_length must be between 2 and MAX_MAGAZINE_LENGTH"
|
||||
assert self.max_note_length < self.max_magazine_length, "max_note_length must be less than max_magazine_length"
|
||||
assert 0 <= self.p_solvable <= 1, "p_solvable must be between 0 and 1"
|
||||
|
||||
|
||||
class RansomNoteDataset(ProceduralDataset):
|
||||
"""Generates Ransom Note exercises with configurable difficulty"""
|
||||
|
||||
def __init__(self, config: RansomNoteConfig):
|
||||
super().__init__(config=config, seed=config.seed, size=config.size)
|
||||
self.letters = {chr(i) for i in range(ord("a"), ord("z") + 1)}
|
||||
|
||||
def _get_inputs(self, rng: Random, solvable: bool) -> tuple[str, str]:
|
||||
"""Generate random ransom note and magazine"""
|
||||
ransom_note_len = rng.randint(1, self.config.max_note_length)
|
||||
ransom_note = [rng.choice(list(self.letters)) for _ in range(ransom_note_len)]
|
||||
|
||||
magazine_len = rng.randint(ransom_note_len, self.config.max_magazine_length)
|
||||
magazine = ransom_note.copy()
|
||||
if solvable:
|
||||
magazine.extend([rng.choice(list(self.letters)) for _ in range(magazine_len - ransom_note_len)])
|
||||
else:
|
||||
remove_letter = rng.choice(magazine)
|
||||
magazine.remove(remove_letter)
|
||||
magazine.extend(
|
||||
[rng.choice(list(self.letters - {remove_letter})) for _ in range(magazine_len - ransom_note_len + 1)]
|
||||
)
|
||||
|
||||
rng.shuffle(ransom_note)
|
||||
rng.shuffle(magazine)
|
||||
return "".join(ransom_note), "".join(magazine)
|
||||
|
||||
def _can_construct(self, ransom_note: str, magazine: str) -> bool:
|
||||
"""Check if ransom note can be constructed from magazine"""
|
||||
count = defaultdict(int)
|
||||
for c in magazine:
|
||||
count[c] += 1
|
||||
for c in ransom_note:
|
||||
if count[c] <= 0:
|
||||
return False
|
||||
count[c] -= 1
|
||||
return True
|
||||
|
||||
def __getitem__(self, idx: int) -> dict:
|
||||
"""Generate a single Group Anagrams question"""
|
||||
rng = Random(self.seed + idx)
|
||||
solvable = rng.random() < self.config.p_solvable
|
||||
ransom_note, magazine = self._get_inputs(rng, solvable)
|
||||
answer = self._can_construct(ransom_note, magazine)
|
||||
|
||||
return {
|
||||
"question": QUESTION_TEMPLATE.format(ransom_note=ransom_note, magazine=magazine),
|
||||
"answer": str(answer),
|
||||
"metadata": {"ransom_note": ransom_note, "magazine": magazine, "solution": answer, "solvable": solvable},
|
||||
}
|
||||
|
||||
|
||||
register_dataset("ransom_note", RansomNoteDataset, RansomNoteConfig)
|
||||
111
tests/test_ransom_note.py
Normal file
111
tests/test_ransom_note.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
"""Tests for Ransom Note questions generation"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from reasoning_gym.algorithmic.ransom_note import RansomNoteConfig, RansomNoteDataset
|
||||
|
||||
|
||||
def test_ransom_note_config_validation():
|
||||
"""Test that invalid configs raise appropriate errors"""
|
||||
with pytest.raises(AssertionError):
|
||||
config = RansomNoteConfig(max_note_length=-1) # Negative not allowed
|
||||
config.validate()
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
config = RansomNoteConfig(max_note_length=0) # Zero not allowed
|
||||
config.validate()
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
config = RansomNoteConfig(max_magazine_length=-1) # Negative not allowed
|
||||
config.validate()
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
config = RansomNoteConfig(max_magazine_length=0) # Zero not allowed
|
||||
config.validate()
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
config = RansomNoteConfig(max_magazine_length=1) # One not allowed
|
||||
config.validate()
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
config = RansomNoteConfig(
|
||||
max_note_length=3, max_magazine_length=2
|
||||
) # max_note_length must be less than max_magazine_length
|
||||
config.validate()
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
config = RansomNoteConfig(p_solvable=-0.01) # p_solvable must be between 0 and 1
|
||||
config.validate()
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
config = RansomNoteConfig(p_solvable=1.01) # p_solvable must be between 0 and 1
|
||||
config.validate()
|
||||
|
||||
|
||||
def test_ransom_note_dataset_deterministic():
|
||||
"""Test that dataset generates same items with same seed"""
|
||||
config = RansomNoteConfig(seed=42, size=10)
|
||||
dataset1 = RansomNoteDataset(config)
|
||||
dataset2 = RansomNoteDataset(config)
|
||||
|
||||
for i in range(len(dataset1)):
|
||||
assert dataset1[i] == dataset2[i]
|
||||
|
||||
|
||||
def test_group_anagrams_dataset_items():
|
||||
"""Test basic properties of generated items"""
|
||||
config = RansomNoteConfig(max_note_length=10, max_magazine_length=30, size=10, seed=42)
|
||||
dataset = RansomNoteDataset(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 "ransom_note" in item["metadata"]
|
||||
assert "magazine" in item["metadata"]
|
||||
assert "solution" in item["metadata"]
|
||||
assert "solvable" in item["metadata"]
|
||||
|
||||
ransom_note = item["metadata"]["ransom_note"]
|
||||
magazine = item["metadata"]["magazine"]
|
||||
solution = item["metadata"]["solution"]
|
||||
solvable = item["metadata"]["solvable"]
|
||||
|
||||
# Verify dimensions
|
||||
assert len(ransom_note) <= config.max_note_length
|
||||
assert len(ransom_note) <= len(magazine)
|
||||
assert len(magazine) <= config.max_magazine_length
|
||||
assert solution == solvable
|
||||
|
||||
|
||||
def test_ransom_note_dataset_iteration():
|
||||
"""Test that iteration respects dataset size"""
|
||||
config = RansomNoteConfig(size=5, seed=42)
|
||||
dataset = RansomNoteDataset(config)
|
||||
|
||||
items = list(dataset)
|
||||
assert len(items) == config.size
|
||||
|
||||
# Test multiple iterations yield same items
|
||||
assert items == list(dataset)
|
||||
|
||||
|
||||
def test_ransom_note_answer():
|
||||
"""Test the _can_construct method"""
|
||||
config = RansomNoteConfig(seed=42)
|
||||
dataset = RansomNoteDataset(config)
|
||||
|
||||
# Correct solution
|
||||
ransom_note, magazine = "ab", "badhergh"
|
||||
assert dataset._can_construct(ransom_note, magazine) == True
|
||||
|
||||
# Inorrect solution
|
||||
ransom_note, magazine = "az", "badhergh"
|
||||
assert dataset._can_construct(ransom_note, magazine) == False
|
||||
Loading…
Add table
Add a link
Reference in a new issue