Skip to content

Added range query. #158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6cdca07
Added range query.
AlfredChester Dec 14, 2024
9c78fe4
Import range query from __init__.py
AlfredChester Dec 14, 2024
6d972fc
Formatted code according to yapf_style.cfg
AlfredChester Dec 15, 2024
6441f51
Fixed query io.
AlfredChester Dec 15, 2024
29f0034
Remove the last endl in to_str
AlfredChester Dec 15, 2024
d89d6e7
Fixed range_query_test.py
AlfredChester Dec 15, 2024
c468945
write some doc for query.py
Mr-Python-in-China Dec 17, 2024
85f535a
Fix range_query_test.py
AlfredChester Dec 19, 2024
2225b56
Merge branch 'master' into master
AlfredChester Dec 21, 2024
e4371a2
Initialize position_range in cyaron/query.py:get_one_query when nothi…
AlfredChester Jan 25, 2025
2d49a12
Set up all supported versions of pythons.
AlfredChester Jan 25, 2025
6d5e457
Use older version of poerty-core
AlfredChester Jan 25, 2025
b9517a9
Fixed expected time complexity.
AlfredChester Jan 25, 2025
99d039a
Use random.sample to enhance performance. Removed unused parameter in…
AlfredChester Jan 26, 2025
c69aa2e
Rollback to while loop to enhance performance.
AlfredChester Jan 26, 2025
0005775
Rollback test.yml
AlfredChester Jan 26, 2025
cbd215b
Set TEST_LEN to 20000 to save time.
AlfredChester Mar 9, 2025
fb31a5d
Support weight generator in range query.
AlfredChester Mar 9, 2025
81b5909
Fixed test for weighted range query
AlfredChester Mar 9, 2025
104c346
Again fixed tests for weighted range query
AlfredChester Mar 9, 2025
8ce5893
Merge branch 'luogu-dev:master' into master
AlfredChester Mar 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cyaron/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from random import choice, randint, random, randrange, uniform

#from .visual import visualize
# from .visual import visualize
from . import log
from .compare import Compare
from .consts import *
Expand All @@ -21,3 +21,4 @@
from .string import String
from .utils import *
from .vector import Vector
from .query import RangeQuery
20 changes: 12 additions & 8 deletions cyaron/graders/noipstyle.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@ def noipstyle(content, std):
i + 1, j + 1, content_lines[i][j:j + 5],
std_lines[i][j:j + 5]))
if len(std_lines[i]) > len(content_lines[i]):
return False, TextMismatch(content, std,
'Too short on line {}.', i + 1,
j + 1, content_lines[i][j:j + 5],
std_lines[i][j:j + 5])
return False, TextMismatch(
content,
std,
'Too short on line {}.',
i + 1,
)
if len(std_lines[i]) < len(content_lines[i]):
return False, TextMismatch(content, std,
'Too long on line {}.', i + 1,
j + 1, content_lines[i][j:j + 5],
std_lines[i][j:j + 5])
return False, TextMismatch(
content,
std,
'Too long on line {}.',
i + 1,
)

return True, None
162 changes: 162 additions & 0 deletions cyaron/query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
"""
This module provides a `RangeQuery` class for generating queries
based on limits of each dimension.

Classes:
RangeQuery: A class for generating random queries.

Usage:
n = randint(1, 10)
q = randint(1, 10)
Q = Query.random(q, [(1, n)])
"""

import random
from enum import IntEnum
from typing import Optional, Union, Tuple, List

from .utils import list_like


class RangeQueryRandomMode(IntEnum):
less = 0 # disallow l = r
allow_equal = 1 # allow l = r


class RangeQuery:
"""A class for generating random queries."""
result: List[Tuple[List[int], List[int], List]] # Vector L, R, weights.

def __init__(self):
self.result = []

def __len__(self):
return len(self.result)

def __getitem__(self, item):
return self.result[item]

def __str__(self):
"""__str__(self) -> str
Return a string to output the queries.
The string contains all the queries with l and r in a row, splits with "\\n".
"""
return self.to_str()

def to_str(self):
"""
Return a string to output the queries.
The string contains all the queries with l and r (and w if generated) in a row, splits with "\\n".
"""
res = ''
for l, r, w in self.result:
l_to_str = [str(x) for x in l]
r_to_str = [str(x) for x in r]
w_to_str = [str(x) for x in w]
res += ' '.join(l_to_str) + ' ' + ' '.join(r_to_str)
if len(w_to_str) > 0:
res += ' ' + ' '.join(w_to_str)
res += '\n'
return res[:-1] # remove the last '\n'

@staticmethod
def random(
num: int = 1,
position_range: Optional[List[Union[int, Tuple[int, int]]]] = None,
mode: RangeQueryRandomMode = RangeQueryRandomMode.allow_equal,
weight_generator=None,
):
"""
Generate `num` random queries with dimension limit.
Args:
num: the number of queries
position_range: a list of limits for each dimension
single number x represents range [1, x]
list [x, y] or tuple (x, y) represents range [x, y]
mode: the mode queries generate, see Enum Class RangeQueryRandomMode
weight_generator: A function that generates the weights for the queries. It should:
- Take the index of query (starting from 1), starting and ending positions as input.
- Return a list of weights of any length.
"""
if position_range is None:
position_range = [10]

if weight_generator is None:
weight_generator = lambda i, l, r: []

ret = RangeQuery()

if not list_like(position_range):
raise TypeError("the 2nd param must be a list or tuple")

for i in range(num):
ret.result.append(
RangeQuery.get_one_query(position_range, mode,
weight_generator, i + 1))
return ret

@staticmethod
def get_one_query(
position_range: Optional[List[Union[int, Tuple[int, int]]]] = None,
mode: RangeQueryRandomMode = RangeQueryRandomMode.allow_equal,
weight_generator=None,
index: int = 1) -> Tuple[List[int], List[int], List]:
"""
Generate a pair of query lists (query_l, query_r, w) based on the given position ranges and mode.
Args:
position_range (Optional[List[Union[int, Tuple[int, int]]]]): A list of position ranges. Each element can be:
- An integer, which will be treated as a range from 1 to that integer.
- A tuple of two integers, representing the lower and upper bounds of the range.
mode (RangeQueryRandomMode): The mode for generating the queries. It can be:
- RangeQueryRandomMode.allow_equal: Allow the generated l and r to be equal.
- RangeQueryRandomMode.less: Ensure that l and r are not equal.
weight_generator: A function that generates the weights for the queries. It should:
- Take the index of query (starting from 1), starting and ending positions as input.
- Return a list of weights of any length.
Returns:
Tuple[List[int], List[int]]: A tuple containing two lists:
- query_l: A list of starting positions.
- query_r: A list of ending positions.
Raises:
ValueError: If the upper-bound is smaller than the lower-bound.
ValueError: If the mode is set to less but the upper-bound is equal to the lower-bound.
"""
if position_range is None:
position_range = [10]

if weight_generator is None:
weight_generator = lambda i, l, r: []

dimension = len(position_range)
query_l: List[int] = []
query_r: List[int] = []
for i in range(dimension):
cur_range: Tuple[int, int]
if isinstance(position_range[i], int):
cur_range = (1, position_range[i])
elif len(position_range[i]) == 1:
cur_range = (1, position_range[i][0])
else:
cur_range = position_range[i]

if cur_range[0] > cur_range[1]:
raise ValueError(
"upper-bound should be larger than lower-bound")
if mode == RangeQueryRandomMode.less and cur_range[0] == cur_range[
1]:
raise ValueError(
"mode is set to less but upper-bound is equal to lower-bound"
)

l = random.randint(cur_range[0], cur_range[1])
r = random.randint(cur_range[0], cur_range[1])
# Expected complexity is O(1)
# We can use random.sample, But it's actually slower according to benchmarks.
while mode == RangeQueryRandomMode.less and l == r:
l = random.randint(cur_range[0], cur_range[1])
r = random.randint(cur_range[0], cur_range[1])
if l > r:
l, r = r, l
query_l.append(l)
query_r.append(r)
return (query_l, query_r, weight_generator(index, query_l, query_r))
1 change: 1 addition & 0 deletions cyaron/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
from .compare_test import TestCompare
from .graph_test import TestGraph
from .vector_test import TestVector
from .range_query_test import TestRangeQuery
from .general_test import TestGeneral
140 changes: 140 additions & 0 deletions cyaron/tests/range_query_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import unittest
import random
from cyaron.query import *
from cyaron.vector import *


def valid_query(l, r, mode: RangeQueryRandomMode, limits) -> bool:
if len(l) != len(r) or len(l) != len(limits):
return False
dimension = len(l)
for i in range(dimension):
cur_limit = limits[i]
if isinstance(cur_limit, int):
cur_limit = (1, cur_limit)
elif len(limits[i]) == 1:
cur_limit = (1, cur_limit[0])
if l[i] > r[i] or (l[i] == r[i] and mode == RangeQueryRandomMode.less):
print("bound", l[i], r[i])
return False
if not (cur_limit[0] <= l[i] <= r[i] <= cur_limit[1]):
print("limit", cur_limit[0], cur_limit[1], l[i], r[i])
return False
return True


TEST_LEN = 20000


class TestRangeQuery(unittest.TestCase):

def test_allow_equal_v1(self):
dimension = random.randint(1, 10)
limits = Vector.random(dimension, [(1, 1000)]) # n1, n2 ...
Q = RangeQuery.random(TEST_LEN, limits)
self.assertEqual(len(Q), TEST_LEN)
for i in range(TEST_LEN):
self.assertTrue(
valid_query(Q[i][0], Q[i][1], RangeQueryRandomMode.allow_equal,
limits))
self.assertTrue(Q[i][2] == [])

def test_allow_equal_v2_throw(self):
dimension = random.randint(1, 10)
limits = Vector.random(dimension, [(1, 1000), (1, 1000)]) # n1, n2 ...
conflict = False
for i in range(dimension):
conflict = conflict or limits[i][0] > limits[i][1]
throw = False
try:
Q = RangeQuery.random(TEST_LEN, limits)
self.assertEqual(len(Q), TEST_LEN)
for i in range(TEST_LEN):
self.assertTrue(
valid_query(Q[i][0], Q[i][1],
RangeQueryRandomMode.allow_equal, limits))
except:
throw = True

self.assertEqual(throw, conflict)

def test_allow_equal_v2_no_throw(self):
dimension = random.randint(1, 10)
limits = Vector.random(dimension, [(1, 1000), (1, 1000)]) # n1, n2 ...
for i in range(dimension):
if limits[i][0] > limits[i][1]:
limits[i][0], limits[i][1] = limits[i][1], limits[i][0]
Q = RangeQuery.random(TEST_LEN, limits)
self.assertEqual(len(Q), TEST_LEN)
for i in range(TEST_LEN):
self.assertTrue(
valid_query(Q[i][0], Q[i][1], RangeQueryRandomMode.allow_equal,
limits))
self.assertTrue(Q[i][2] == [])

def test_less_v1(self):
dimension = random.randint(1, 10)
limits = Vector.random(dimension, [(2, 1000)]) # n1, n2 ...
Q = RangeQuery.random(TEST_LEN, limits, RangeQueryRandomMode.less)
self.assertEqual(len(Q), TEST_LEN)
for i in range(TEST_LEN):
self.assertTrue(
valid_query(Q[i][0], Q[i][1], RangeQueryRandomMode.less,
limits))
self.assertTrue(Q[i][2] == [])

def test_less_v2_throw(self):
dimension = random.randint(1, 10)
limits = Vector.random(dimension, [(1, 1000), (1, 1000)]) # n1, n2 ...
conflict = False
for i in range(dimension):
conflict = conflict or limits[i][0] >= limits[i][1]
throw = False
try:
Q = RangeQuery.random(TEST_LEN, limits, RangeQueryRandomMode.less)
self.assertEqual(len(Q), TEST_LEN)
for i in range(TEST_LEN):
self.assertTrue(
valid_query(Q[i][0], Q[i][1], RangeQueryRandomMode.less,
limits))
except:
throw = True

self.assertEqual(throw, conflict)

def test_less_v2_no_throw(self):
dimension = random.randint(1, 10)
limits = Vector.random(dimension, [(1, 1000), (1, 1000)]) # n1, n2 ...
for i in range(dimension):
while limits[i][0] == limits[i][1]:
limits[i][0] = random.randint(1, 1000)
limits[i][1] = random.randint(1, 1000)
if limits[i][0] > limits[i][1]:
limits[i][0], limits[i][1] = limits[i][1], limits[i][0]
Q = RangeQuery.random(TEST_LEN, limits, RangeQueryRandomMode.less)
self.assertEqual(len(Q), TEST_LEN)
for i in range(TEST_LEN):
self.assertTrue(
valid_query(Q[i][0], Q[i][1], RangeQueryRandomMode.less,
limits))
self.assertTrue(Q[i][2] == [])

def test_weight(self):

def foo(i, l, r):
ret = pow(114514, i, 19260817)
self.assertEqual(len(l), len(r))
for j in range(len(l)):
ret = (ret + l[j] * r[j] * 3301) % 19260817
return [ret]

dimension = random.randint(1, 10)
limits = Vector.random(dimension, [(1, 1000), (1, 1000)]) # n1, n2 ...
for i in range(dimension):
if limits[i][0] > limits[i][1]:
limits[i][0], limits[i][1] = limits[i][1], limits[i][0]
Q = RangeQuery.random(TEST_LEN, limits, weight_generator=foo)
i = 1
for l, r, w in Q.result:
self.assertEqual(w, foo(i, l, r))
i += 1
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ colorful = "^0.5.6"


[build-system]
requires = ["poetry-core"]
requires = ["poetry-core<2.0.0"]
build-backend = "poetry.core.masonry.api"

[project.urls]
Expand Down