From 67ea55bab4f9b2f4bc8db41963f3ed90cb618997 Mon Sep 17 00:00:00 2001 From: Sakshi Oza <125877202+sak-codes@users.noreply.github.com> Date: Thu, 29 Jun 2023 19:26:57 +0530 Subject: [PATCH 1/3] Add lazy segment tree --- .../segment_tree.py | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/pydatastructs/miscellaneous_data_structures/segment_tree.py b/pydatastructs/miscellaneous_data_structures/segment_tree.py index 0895ba6da..c1b73e340 100644 --- a/pydatastructs/miscellaneous_data_structures/segment_tree.py +++ b/pydatastructs/miscellaneous_data_structures/segment_tree.py @@ -1,4 +1,5 @@ from .stack import Stack +from pydatastructs import OneDimensionalArray from pydatastructs.utils.misc_util import (TreeNode, Backend, raise_if_backend_is_not_python) @@ -223,3 +224,138 @@ def query(self, start, end): return self._query(self._root, 0, len(self._array) - 1, start, end) + + +class OneDimensionalArraySegmentTreeLazy(OneDimensionalArraySegmentTree): + + __slots__ = ["_func", "_array", "_lazy_node", + "_neutral_element", "_root", "_backend"] + + def __new__(cls, array, neutral_element, func, **kwargs): + backend = kwargs.get('backend', Backend.PYTHON) + raise_if_backend_is_not_python(cls, backend) + + obj = object.__new__(cls) + obj._func = func + obj._array = array + obj._lazy_node = None + obj._neutral_element = neutral_element + obj._root = None + obj._backend = backend + return obj + + @classmethod + def methods(self): + return ['__new__', 'build', 'update_range', + 'update', 'query'] + + @property + def is_ready(self): + return self._root is not None + + def build(self): + if self.is_ready: + return + + recursion_stack = Stack(implementation='linked_list') + node = TreeNode((0, len(self._array) - 1), None, backend=self._backend) + lazy_node = TreeNode((0, len(self._array) - 1), None, backend=self._backend) + node.is_root = True + lazy_node.is_root = True + self._root = node + self._lazy_node = lazy_node + recursion_stack.push((node, lazy_node)) + + while not recursion_stack.is_empty: + node, lazy_node = recursion_stack.peek.key + start, end = node.key + if start == end: + node.data = self._array[start] + lazy_node.data = self._neutral_element + recursion_stack.pop() + continue + + if (node.left is not None and + node.right is not None): + recursion_stack.pop() + node.data = self._func((node.left.data, node.right.data)) + lazy_node.data = self._func((node.left.data, node.right.data)) + else: + mid = (start + end) // 2 + if node.left is None: + left_node = TreeNode((start, mid), None) + node.left = left_node + lazy_left_node = TreeNode((start, mid), None) + lazy_node.left = lazy_left_node + recursion_stack.push((left_node, lazy_left_node)) + if node.right is None: + right_node = TreeNode((mid + 1, end), None) + node.right = right_node + lazy_rig_node = TreeNode((start, mid), None) + lazy_node.right = lazy_rig_node + recursion_stack.push((right_node, lazy_rig_node)) + + def _update_range(self, node, lazy_node, start, end, l, r, value): + if not self.is_ready: + raise ValueError("{} tree is not built yet. ".format(self) + + "Call .build method to prepare the segment tree.") + if lazy_node.data != self._neutral_element: + node.data = self._func((node.data, lazy_node.data)) + if start != end: + lazy_node.left.data = self._func((lazy_node.left.data, lazy_node.data)) + lazy_node.right.data = self._func((lazy_node.right.data, lazy_node.data)) + lazy_node.data = self._neutral_element + if r < start or end < l: + return + + if l <= start and end <= r: + node.data = self._func((node.data, value)) + if start != end: + lazy_node.left.data = self._func((lazy_node.left.data, value)) + lazy_node.right.data = self._func((lazy_node.right.data, value)) + return + mid = (start + end) // 2 + self._update_range(node.left, lazy_node.left, start, mid, l, r, value) + self._update_range(node.right, lazy_node.right, mid + 1, end, l, r, value) + node.data = self._func((node.left.data, node.right.data)) + + def update_range(self, start, end, value): + if not self.is_ready: + raise ValueError("{} tree is not built yet. ".format(self) + + "Call .build method to prepare the segment tree.") + self._update_range(self._root, self._lazy_node, 0, len(self._array) - 1, + start, end, value) + + def update(self, index, value): + if not self.is_ready: + raise ValueError("{} tree is not built yet. ".format(self) + + "Call .build method to prepare the segment tree.") + self._update_range(self._root, self._lazy_node, 0, len(self._array) - 1, + index, index, value) + + def _query(self, node, lazy_node, start, end, l, r): + if r < start or end < l: + return None + + if lazy_node.data != self._neutral_element: + node.data = self._func((node.data, lazy_node.data)) + if start != end: + lazy_node.left.data = self._func((lazy_node.left.data, lazy_node.data)) + lazy_node.right.data = self._func((lazy_node.right.data, lazy_node.data)) + lazy_node.data = self._neutral_element + + if l <= start and end <= r: + return node.data + + mid = (start + end) // 2 + left_result = self._query(node.left, lazy_node.left, start, mid, l, r) + right_result = self._query(node.right, lazy_node.right, mid + 1, end, l, r) + return self._func((left_result, right_result)) + + def query(self, start, end): + if not self.is_ready: + raise ValueError("{} tree is not built yet. ".format(self) + + "Call .build method to prepare the segment tree.") + + return self._query(self._root, 0, len(self._array) - 1, + start, end) From e924c7232ab5ad8fe11259d4175ed16c4d9aef90 Mon Sep 17 00:00:00 2001 From: Sakshi Oza <125877202+sak-codes@users.noreply.github.com> Date: Sat, 12 Aug 2023 11:45:49 +0530 Subject: [PATCH 2/3] use setitem for update --- .../algorithms.py | 5 +- .../segment_tree.py | 53 ++++++++++++++----- .../tests/test_range_query_dynamic.py | 7 +-- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/pydatastructs/miscellaneous_data_structures/algorithms.py b/pydatastructs/miscellaneous_data_structures/algorithms.py index 3c2f86516..6810cdfd8 100644 --- a/pydatastructs/miscellaneous_data_structures/algorithms.py +++ b/pydatastructs/miscellaneous_data_structures/algorithms.py @@ -318,7 +318,8 @@ def __new__(cls, array, func, **kwargs): raise_if_backend_is_not_python( cls, kwargs.pop('backend', Backend.PYTHON)) obj = object.__new__(cls) - obj.segment_tree = ArraySegmentTree(array, func, dimensions=1) + is_lazy = kwargs.pop('lazy', False) + obj.segment_tree = ArraySegmentTree(array, func, dimensions=1, is_lazy=is_lazy, **kwargs) obj.segment_tree.build() obj.bounds = (0, len(array)) return obj @@ -332,4 +333,4 @@ def query(self, start, end): return self.segment_tree.query(start, end) def update(self, index, value): - self.segment_tree.update(index, value) + self.segment_tree[index] = value diff --git a/pydatastructs/miscellaneous_data_structures/segment_tree.py b/pydatastructs/miscellaneous_data_structures/segment_tree.py index c1b73e340..7657cfb57 100644 --- a/pydatastructs/miscellaneous_data_structures/segment_tree.py +++ b/pydatastructs/miscellaneous_data_structures/segment_tree.py @@ -52,7 +52,7 @@ class ArraySegmentTree(object): 1 >>> s_t.query(1, 3) 2 - >>> s_t.update(2, -1) + >>> s_t[2] = -1 >>> s_t.query(1, 3) -1 >>> arr = OneDimensionalArray(int, [1, 2]) @@ -66,10 +66,18 @@ class ArraySegmentTree(object): .. [1] https://cp-algorithms.com/data_structures/segment_tree.html """ - def __new__(cls, array, func, **kwargs): + def __new__(cls, array, func, is_lazy=False, **kwargs): dimensions = kwargs.pop("dimensions", 1) if dimensions == 1: + if is_lazy: + if kwargs.get('neutral_element') is not None: + neutral_element = kwargs.pop('neutral_element') + return OneDimensionalArraySegmentTreeLazy(array, neutral_element, + func, **kwargs) + else: + raise ValueError("ArraySegmentTree with lazy implementation should have a " + "neutral_element argument") return OneDimensionalArraySegmentTree(array, func, **kwargs) else: raise NotImplementedError("ArraySegmentTree do not support " @@ -83,7 +91,7 @@ def build(self): raise NotImplementedError( "This is an abstract method.") - def update(self, index, value): + def __setitem__(self, index, value): """ Updates the value at given index. """ @@ -132,7 +140,7 @@ def __new__(cls, array, func, **kwargs): @classmethod def methods(self): - return ['__new__', 'build', 'update', + return ['__new__', 'build', 'query'] @property @@ -172,7 +180,7 @@ def build(self): node.right = right_node recursion_stack.push(right_node) - def update(self, index, value): + def __setitem__(self, index, value): if not self.is_ready: raise ValueError("{} tree is not built yet. ".format(self) + "Call .build method to prepare the segment tree.") @@ -247,7 +255,7 @@ def __new__(cls, array, neutral_element, func, **kwargs): @classmethod def methods(self): return ['__new__', 'build', 'update_range', - 'update', 'query'] + 'query'] @property def is_ready(self): @@ -279,7 +287,7 @@ def build(self): node.right is not None): recursion_stack.pop() node.data = self._func((node.left.data, node.right.data)) - lazy_node.data = self._func((node.left.data, node.right.data)) + lazy_node.data = self._neutral_element else: mid = (start + end) // 2 if node.left is None: @@ -291,7 +299,7 @@ def build(self): if node.right is None: right_node = TreeNode((mid + 1, end), None) node.right = right_node - lazy_rig_node = TreeNode((start, mid), None) + lazy_rig_node = TreeNode((mid + 1, end), None) lazy_node.right = lazy_rig_node recursion_stack.push((right_node, lazy_rig_node)) @@ -326,12 +334,33 @@ def update_range(self, start, end, value): self._update_range(self._root, self._lazy_node, 0, len(self._array) - 1, start, end, value) - def update(self, index, value): + + def _set_single_element(self, node, lazy_node, start, end, index, value): + if lazy_node.data != self._neutral_element: + node.data = self._func((node.data, lazy_node.data)) + if start != end: + lazy_node.left.data = self._func((lazy_node.left.data, lazy_node.data)) + lazy_node.right.data = self._func((lazy_node.right.data, lazy_node.data)) + lazy_node.data = self._neutral_element + if index == start and end == index: + node.data = value + return + mid = (start + end) // 2 + if start <= index and index <= mid: + self._set_single_element(node.left, lazy_node.left, start, mid, index, value) + else: + self._set_single_element(node.right, lazy_node.right, mid + 1, end, index, value) + node.data = self._func((node.left.data, node.right.data)) + + + def __setitem__(self, index, value): if not self.is_ready: raise ValueError("{} tree is not built yet. ".format(self) + "Call .build method to prepare the segment tree.") - self._update_range(self._root, self._lazy_node, 0, len(self._array) - 1, - index, index, value) + assert self._root is not None + assert self._lazy_node is not None + self._set_single_element(self._root, self._lazy_node, 0, len(self._array) - 1, + index, value) def _query(self, node, lazy_node, start, end, l, r): if r < start or end < l: @@ -357,5 +386,5 @@ def query(self, start, end): raise ValueError("{} tree is not built yet. ".format(self) + "Call .build method to prepare the segment tree.") - return self._query(self._root, 0, len(self._array) - 1, + return self._query(self._root, self._lazy_node, 0, len(self._array) - 1, start, end) diff --git a/pydatastructs/miscellaneous_data_structures/tests/test_range_query_dynamic.py b/pydatastructs/miscellaneous_data_structures/tests/test_range_query_dynamic.py index f655c546d..b97827f63 100644 --- a/pydatastructs/miscellaneous_data_structures/tests/test_range_query_dynamic.py +++ b/pydatastructs/miscellaneous_data_structures/tests/test_range_query_dynamic.py @@ -25,11 +25,12 @@ def _test_RangeQueryDynamic_common(func, gen_expected): for j in range(i + 1, array_size): inputs.append((i, j)) - data_structures = ["array", "segment_tree"] - for ds in data_structures: + data_structures = {"array":{}, "segment_tree": {}, + "segment_tree": {'lazy': True, 'neutral_element': int(1e10)}} + for ds, kw in data_structures.items(): data = random.sample(range(-2*array_size, 2*array_size), array_size) array = OneDimensionalArray(int, data) - rmq = RangeQueryDynamic(array, func, data_structure=ds) + rmq = RangeQueryDynamic(array, func, data_structure=ds, **kw) for input in inputs: assert rmq.query(input[0], input[1]) == gen_expected(data, input[0], input[1]) From 069b5dfa1d089a39d19c89c4517857f92f6e0dd8 Mon Sep 17 00:00:00 2001 From: Sakshi Oza <125877202+sak-codes@users.noreply.github.com> Date: Sat, 12 Aug 2023 12:09:16 +0530 Subject: [PATCH 3/3] test range update --- .../algorithms.py | 24 ++++++++++++++--- .../segment_tree.py | 11 ++++++-- .../tests/test_range_query_dynamic.py | 26 +++++++++++++++---- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/pydatastructs/miscellaneous_data_structures/algorithms.py b/pydatastructs/miscellaneous_data_structures/algorithms.py index 6810cdfd8..8be9fc7d5 100644 --- a/pydatastructs/miscellaneous_data_structures/algorithms.py +++ b/pydatastructs/miscellaneous_data_structures/algorithms.py @@ -257,7 +257,7 @@ def __new__(cls, array, func, data_structure='segment_tree', **kwargs): @classmethod def methods(cls): - return ['query', 'update'] + return ['query', 'update', 'update_range'] def query(start, end): """ @@ -289,6 +289,21 @@ def update(self, index, value): raise NotImplementedError( "This is an abstract method.") + def update_range(self, start, end, value): + """ + Method to update [start, end] with a new value. + + Parameters + ========== + + index: int + The index to be update. + value: int + The new value. + """ + raise NotImplementedError( + "This is an abstract method.") + class RangeQueryDynamicArray(RangeQueryDynamic): __slots__ = ["range_query_static"] @@ -312,7 +327,7 @@ def update(self, index, value): class RangeQueryDynamicSegmentTree(RangeQueryDynamic): - __slots__ = ["segment_tree", "bounds"] + __slots__ = ["segment_tree", "bounds", "is_lazy"] def __new__(cls, array, func, **kwargs): raise_if_backend_is_not_python( @@ -326,7 +341,7 @@ def __new__(cls, array, func, **kwargs): @classmethod def methods(cls): - return ['query', 'update'] + return ['query', 'update', 'update_range'] def query(self, start, end): _check_range_query_inputs((start, end + 1), self.bounds) @@ -334,3 +349,6 @@ def query(self, start, end): def update(self, index, value): self.segment_tree[index] = value + + def update_range(self, start, end, value): + self.segment_tree.update_range(start, end, value) diff --git a/pydatastructs/miscellaneous_data_structures/segment_tree.py b/pydatastructs/miscellaneous_data_structures/segment_tree.py index 7657cfb57..b9af40e8e 100644 --- a/pydatastructs/miscellaneous_data_structures/segment_tree.py +++ b/pydatastructs/miscellaneous_data_structures/segment_tree.py @@ -107,6 +107,15 @@ def query(self, start, end): raise NotImplementedError( "This is an abstract method.") + def update_range(self, start, end, value): + """ + Updates [start, end] range according + to the function provided while constructing + `ArraySegmentTree` object. + """ + raise NotImplementedError( + "This is an abstract method.") + def __str__(self): recursion_stack = Stack(implementation='linked_list') recursion_stack.push(self._root) @@ -357,8 +366,6 @@ def __setitem__(self, index, value): if not self.is_ready: raise ValueError("{} tree is not built yet. ".format(self) + "Call .build method to prepare the segment tree.") - assert self._root is not None - assert self._lazy_node is not None self._set_single_element(self._root, self._lazy_node, 0, len(self._array) - 1, index, value) diff --git a/pydatastructs/miscellaneous_data_structures/tests/test_range_query_dynamic.py b/pydatastructs/miscellaneous_data_structures/tests/test_range_query_dynamic.py index b97827f63..35de0a4b1 100644 --- a/pydatastructs/miscellaneous_data_structures/tests/test_range_query_dynamic.py +++ b/pydatastructs/miscellaneous_data_structures/tests/test_range_query_dynamic.py @@ -6,7 +6,8 @@ import random, math from copy import deepcopy -def _test_RangeQueryDynamic_common(func, gen_expected): +def _test_RangeQueryDynamic_common(func, neutral_element, gen_expected, + range_update_possible=True): array = OneDimensionalArray(int, []) raises(ValueError, lambda: RangeQueryDynamic(array, func)) @@ -26,8 +27,9 @@ def _test_RangeQueryDynamic_common(func, gen_expected): inputs.append((i, j)) data_structures = {"array":{}, "segment_tree": {}, - "segment_tree": {'lazy': True, 'neutral_element': int(1e10)}} + "segment_tree": {'lazy': True, 'neutral_element': neutral_element}} for ds, kw in data_structures.items(): + range_update_possible = range_update_possible and kw.get('lazy', False) data = random.sample(range(-2*array_size, 2*array_size), array_size) array = OneDimensionalArray(int, data) rmq = RangeQueryDynamic(array, func, data_structure=ds, **kw) @@ -44,12 +46,26 @@ def _test_RangeQueryDynamic_common(func, gen_expected): for input in inputs: assert rmq.query(input[0], input[1]) == gen_expected(data_copy, input[0], input[1]) + if range_update_possible: + for _ in range(min(array_size//2, 20)): + start = random.randint(0, array_size - 1) + end = random.randint(0, array_size - 1) + value = random.randint(0, 4 * array_size) + start, end = min(start, end), max(start, end) + for j in range(start, end+1): + data_copy[j] = func((data_copy[j], value)) + rmq.update_range(start, end, value) + + for input in inputs: + assert rmq.query(input[0], input[1]) == gen_expected(data_copy, input[0], input[1]) + + def test_RangeQueryDynamic_minimum(): def _gen_minimum_expected(data, i, j): return min(data[i:j + 1]) - _test_RangeQueryDynamic_common(minimum, _gen_minimum_expected) + _test_RangeQueryDynamic_common(minimum, int(1e10), _gen_minimum_expected) def test_RangeQueryDynamic_greatest_common_divisor(): @@ -62,11 +78,11 @@ def _gen_gcd_expected(data, i, j): expected_gcd = math.gcd(expected_gcd, data[idx]) return expected_gcd - _test_RangeQueryDynamic_common(greatest_common_divisor, _gen_gcd_expected) + _test_RangeQueryDynamic_common(greatest_common_divisor, 0, _gen_gcd_expected) def test_RangeQueryDynamic_summation(): def _gen_summation_expected(data, i, j): return sum(data[i:j + 1]) - return _test_RangeQueryDynamic_common(summation, _gen_summation_expected) + return _test_RangeQueryDynamic_common(summation, 0, _gen_summation_expected, False)