Skip to content

Commit 2a7d6c7

Browse files
pratikglczgdp1807
andauthored
Added SparseTable and RangeQueryStatic APIs (#414)
Co-authored-by: Gagandeep Singh <gdp.1807@gmail.com>
1 parent c726bf2 commit 2a7d6c7

File tree

12 files changed

+399
-10
lines changed

12 files changed

+399
-10
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ You can install the library by running the following command,
1919
python -m pip install .
2020
```
2121

22-
For development purposes, you can use the option `develop` as shown below,
22+
For development purposes, you can use the option `e` as shown below,
2323

2424
```python
2525
python -m pip install -e .
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Algorithms
2+
==========
3+
4+
.. autoclass:: pydatastructs.RangeQueryStatic

docs/source/pydatastructs/miscellaneous_data_structures/miscellaneous_data_structures.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ Miscellaneous Data Structures
77
stack.rst
88
queue.rst
99
binomial_trees.rst
10-
disjoint_set.rst
10+
disjoint_set.rst
11+
sparse_table.rst
12+
algorithms.rst
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
SparseTable
2+
===========
3+
4+
.. autoclass:: pydatastructs.SparseTable

pydatastructs/miscellaneous_data_structures/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
stack,
55
binomial_trees,
66
queue,
7-
disjoint_set
7+
disjoint_set,
8+
sparse_table
89
)
910

1011
from .binomial_trees import (
@@ -27,3 +28,13 @@
2728
DisjointSetForest,
2829
)
2930
__all__.extend(disjoint_set.__all__)
31+
32+
from .sparse_table import (
33+
SparseTable,
34+
)
35+
__all__.extend(sparse_table.__all__)
36+
37+
from .algorithms import (
38+
RangeQueryStatic
39+
)
40+
__all__.extend(algorithms.__all__)
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
from pydatastructs.miscellaneous_data_structures.sparse_table import SparseTable
2+
from pydatastructs.utils.misc_util import _check_range_query_inputs
3+
4+
__all__ = ['RangeQueryStatic']
5+
6+
7+
class RangeQueryStatic:
8+
"""
9+
Produces results for range queries of different kinds
10+
by using specified data structure.
11+
12+
Parameters
13+
==========
14+
15+
array: OneDimensionalArray
16+
The array for which we need to answer queries.
17+
All the elements should be of type `int`.
18+
func: callable
19+
The function to be used for generating results
20+
of a query. It should accept only one tuple as an
21+
argument. The size of the tuple will be either 1 or 2
22+
and any one of the elements can be `None`. You can treat
23+
`None` in whatever way you want according to the query
24+
you are performing. For example, in case of range minimum
25+
queries, `None` can be treated as infinity. We provide
26+
the following which can be used as an argument value for this
27+
parameter,
28+
29+
`minimum` - For range minimum queries.
30+
31+
`greatest_common_divisor` - For queries finding greatest
32+
common divisor of a range.
33+
34+
`summation` - For range sum queries.
35+
data_structure: str
36+
The data structure to be used for performing
37+
range queries.
38+
Currently the following data structures are supported,
39+
40+
'array' -> Array data structure.
41+
Each query takes O(end - start) time asymptotically.
42+
43+
'sparse_table' -> Sparse table data structure.
44+
Each query takes O(log(end - start)) time
45+
asymptotically.
46+
47+
By default, 'sparse_table'.
48+
49+
Examples
50+
========
51+
52+
>>> from pydatastructs import OneDimensionalArray, RangeQueryStatic
53+
>>> from pydatastructs import minimum
54+
>>> arr = OneDimensionalArray(int, [4, 6, 1, 5, 7, 3])
55+
>>> RMQ = RangeQueryStatic(arr, minimum)
56+
>>> RMQ.query(3, 5)
57+
5
58+
>>> RMQ.query(0, 5)
59+
1
60+
>>> RMQ.query(0, 3)
61+
1
62+
63+
Note
64+
====
65+
66+
The array once passed as an input should not be modified
67+
once the `RangeQueryStatic` constructor is called. If you
68+
have updated the array, then you need to create a new
69+
`RangeQueryStatic` object with this updated array.
70+
"""
71+
72+
def __new__(cls, array, func, data_structure='sparse_table'):
73+
if len(array) == 0:
74+
raise ValueError("Input %s array is empty."%(array))
75+
76+
if data_structure == 'array':
77+
return RangeQueryStaticArray(array, func)
78+
elif data_structure == 'sparse_table':
79+
return RangeQueryStaticSparseTable(array, func)
80+
else:
81+
raise NotImplementedError(
82+
"Currently %s data structure for range "
83+
"query without updates isn't implemented yet."
84+
% (data_structure))
85+
86+
@classmethod
87+
def methods(cls):
88+
return ['query']
89+
90+
def query(start, end):
91+
"""
92+
Method to perform a query in [start, end) range.
93+
94+
Parameters
95+
==========
96+
97+
start: int
98+
The starting index of the range.
99+
end: int
100+
The index just before which the range ends.
101+
This means that this index will be excluded
102+
from the range for generating results.
103+
"""
104+
raise NotImplementedError(
105+
"This is an abstract method.")
106+
107+
108+
class RangeQueryStaticSparseTable(RangeQueryStatic):
109+
110+
__slots__ = ["sparse_table", "bounds"]
111+
112+
def __new__(cls, array, func):
113+
obj = object.__new__(cls)
114+
sparse_table = SparseTable(array, func)
115+
obj.bounds = (0, len(array))
116+
obj.sparse_table = sparse_table
117+
return obj
118+
119+
@classmethod
120+
def methods(cls):
121+
return ['query']
122+
123+
def query(self, start, end):
124+
_check_range_query_inputs((start, end), self.bounds)
125+
return self.sparse_table.query(start, end)
126+
127+
128+
class RangeQueryStaticArray(RangeQueryStatic):
129+
130+
__slots__ = ["array", "func"]
131+
132+
def __new__(cls, array, func):
133+
obj = object.__new__(cls)
134+
obj.array = array
135+
obj.func = func
136+
return obj
137+
138+
@classmethod
139+
def methods(cls):
140+
return ['query']
141+
142+
def query(self, start, end):
143+
_check_range_query_inputs((start, end), (0, len(self.array)))
144+
145+
rsize = end - start
146+
147+
if rsize == 1:
148+
return self.func((self.array[start],))
149+
150+
query_ans = self.func((self.array[start], self.array[start + 1]))
151+
for i in range(start + 2, end):
152+
query_ans = self.func((query_ans, self.array[i]))
153+
return query_ans
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from pydatastructs.linear_data_structures.arrays import (
2+
MultiDimensionalArray, OneDimensionalArray)
3+
from pydatastructs.utils.misc_util import NoneType
4+
import math
5+
6+
__all__ = ['SparseTable']
7+
8+
9+
class SparseTable(object):
10+
"""
11+
Represents the sparse table data structure.
12+
13+
Parameters
14+
==========
15+
16+
array: OneDimensionalArray
17+
The array to be used for filling the sparse table.
18+
func: callable
19+
The function to be used for filling the sparse table.
20+
It should accept only one tuple as an argument. The
21+
size of the tuple will be either 1 or 2 and any one
22+
of the elements can be `None`. You can treat `None` in
23+
whatever way you want. For example, in case of minimum
24+
values, `None` can be treated as infinity. We provide
25+
the following which can be used as an argument value for this
26+
parameter,
27+
28+
`minimum` - For range minimum queries.
29+
30+
`greatest_common_divisor` - For queries finding greatest
31+
common divisor of a range.
32+
33+
`summation` - For range sum queries.
34+
35+
Examples
36+
========
37+
38+
>>> from pydatastructs import SparseTable, minimum
39+
>>> from pydatastructs import OneDimensionalArray
40+
>>> arr = OneDimensionalArray(int, [1, 2, 3, 4, 5])
41+
>>> s_t = SparseTable(arr, minimum)
42+
>>> str(s_t)
43+
"['[1, 1, 1]', '[2, 2, 2]', '[3, 3, None]', '[4, 4, None]', '[5, None, None]']"
44+
45+
References
46+
==========
47+
48+
.. [1] https://cp-algorithms.com/data_structures/sparse-table.html
49+
"""
50+
51+
__slots__ = ['_table', 'func']
52+
53+
def __new__(cls, array, func):
54+
55+
if len(array) == 0:
56+
raise ValueError("Input %s array is empty."%(array))
57+
58+
obj = object.__new__(cls)
59+
size = len(array)
60+
log_size = int(math.log2(size)) + 1
61+
obj._table = [OneDimensionalArray(int, log_size) for _ in range(size)]
62+
obj.func = func
63+
64+
for i in range(size):
65+
obj._table[i][0] = func((array[i],))
66+
67+
for j in range(1, log_size + 1):
68+
for i in range(size - (1 << j) + 1):
69+
obj._table[i][j] = func((obj._table[i][j - 1],
70+
obj._table[i + (1 << (j - 1))][j - 1]))
71+
72+
return obj
73+
74+
@classmethod
75+
def methods(cls):
76+
return ['query', '__str__']
77+
78+
def query(self, start, end):
79+
"""
80+
Method to perform a query on sparse table in [start, end)
81+
range.
82+
83+
Parameters
84+
==========
85+
86+
start: int
87+
The starting index of the range.
88+
end: int
89+
The index just before which the range ends.
90+
This means that this index will be excluded
91+
from the range for generating results.
92+
"""
93+
end -= 1
94+
j = int(math.log2(end - start + 1)) + 1
95+
answer = None
96+
while j >= 0:
97+
if start + (1 << j) - 1 <= end:
98+
answer = self.func((answer, self._table[start][j]))
99+
start += 1 << j
100+
j -= 1
101+
return answer
102+
103+
def __str__(self):
104+
return str([str(array) for array in self._table])
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from pydatastructs import (
2+
RangeQueryStatic, minimum,
3+
greatest_common_divisor, summation,
4+
OneDimensionalArray)
5+
from pydatastructs.utils.raises_util import raises
6+
import random, math
7+
8+
def _test_RangeQueryStatic_common(func, gen_expected):
9+
10+
array = OneDimensionalArray(int, [])
11+
raises(ValueError, lambda: RangeQueryStatic(array, func))
12+
13+
array = OneDimensionalArray(int, [1])
14+
rq = RangeQueryStatic(array, func)
15+
assert rq.query(0, 1) == 1
16+
raises(ValueError, lambda: rq.query(0, 0))
17+
raises(IndexError, lambda: rq.query(0, 2))
18+
19+
array_sizes = [3, 6, 12, 24, 48, 96]
20+
random.seed(0)
21+
for array_size in array_sizes:
22+
data = random.sample(range(-2*array_size, 2*array_size), array_size)
23+
array = OneDimensionalArray(int, data)
24+
25+
expected = []
26+
inputs = []
27+
for i in range(array_size):
28+
for j in range(i + 1, array_size):
29+
inputs.append((i, j))
30+
expected.append(gen_expected(data, i, j))
31+
32+
data_structures = ["array", "sparse_table"]
33+
for ds in data_structures:
34+
rmq = RangeQueryStatic(array, func, data_structure=ds)
35+
for input, correct in zip(inputs, expected):
36+
assert rmq.query(input[0], input[1]) == correct
37+
38+
def test_RangeQueryStatic_minimum():
39+
40+
def _gen_minimum_expected(data, i, j):
41+
return min(data[i:j])
42+
43+
_test_RangeQueryStatic_common(minimum, _gen_minimum_expected)
44+
45+
def test_RangeQueryStatic_greatest_common_divisor():
46+
47+
def _gen_gcd_expected(data, i, j):
48+
if j - i == 1:
49+
return data[i]
50+
else:
51+
expected_gcd = math.gcd(data[i], data[i + 1])
52+
for idx in range(i + 2, j):
53+
expected_gcd = math.gcd(expected_gcd, data[idx])
54+
return expected_gcd
55+
56+
_test_RangeQueryStatic_common(greatest_common_divisor, _gen_gcd_expected)
57+
58+
def test_RangeQueryStatic_summation():
59+
60+
def _gen_summation_expected(data, i, j):
61+
return sum(data[i:j])
62+
63+
return _test_RangeQueryStatic_common(summation, _gen_summation_expected)

pydatastructs/utils/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
CartesianTreeNode,
1414
RedBlackTreeNode,
1515
TrieNode,
16-
SkipNode
16+
SkipNode,
17+
summation,
18+
greatest_common_divisor,
19+
minimum
1720
)
1821
__all__.extend(misc_util.__all__)

0 commit comments

Comments
 (0)