197 lines
7.1 KiB
Python
197 lines
7.1 KiB
Python
"""Cells for segment_tree.SegmentTree (Q77).
|
|
|
|
Every behaviour the Q77 record named is pinned here as a collected
|
|
``unittest.TestCase`` method, so ``unittest discover -p 'test_*.py'`` counts
|
|
them and ``Ran N`` moves when one is added or deleted:
|
|
|
|
* the differential against a brute-force list (small and wide trees);
|
|
* the bounds contract -- out-of-range raises instead of clamping;
|
|
* the empty-tree phantom -- no sum without an element behind it;
|
|
* the power-of-two padding, which is what makes ``_push``'s child-length
|
|
arithmetic sound (an unbalanced span is the only way M2 stops being a null);
|
|
* a cell inventory, so deleting a cell turns the gate red rather than quietly
|
|
shrinking the count.
|
|
"""
|
|
|
|
import random
|
|
import sys
|
|
import unittest
|
|
|
|
from segment_tree import SegmentTree
|
|
|
|
|
|
class TestRandomDifferential(unittest.TestCase):
|
|
"""The primitive against a list: every add and every sum, plus the whole array."""
|
|
|
|
def test_random_ops_match_brute_list(self):
|
|
rng = random.Random(42)
|
|
for trial in range(400):
|
|
n = rng.randint(1, 12)
|
|
data = [rng.randint(-5, 5) for _ in range(n)]
|
|
st = SegmentTree(data)
|
|
arr = list(data)
|
|
for _ in range(40):
|
|
if rng.random() < 0.5:
|
|
l = rng.randint(0, n - 1)
|
|
r = rng.randint(l, n - 1)
|
|
v = rng.randint(-5, 5)
|
|
st.range_add(l, r, v)
|
|
for i in range(l, r + 1):
|
|
arr[i] += v
|
|
else:
|
|
l = rng.randint(0, n - 1)
|
|
r = rng.randint(l, n - 1)
|
|
self.assertEqual(
|
|
st.range_sum(l, r), sum(arr[l:r + 1]),
|
|
msg=f"trial {trial} sum[{l},{r}]",
|
|
)
|
|
self.assertEqual(st.to_list(), arr, msg=f"trial {trial} list")
|
|
|
|
def test_wide_tree_matches_brute_list(self):
|
|
rng = random.Random(7)
|
|
n = 1000
|
|
data = [rng.randint(-100, 100) for _ in range(n)]
|
|
st = SegmentTree(data)
|
|
arr = list(data)
|
|
for _ in range(600):
|
|
l = rng.randint(0, n - 1)
|
|
r = rng.randint(l, n - 1)
|
|
v = rng.randint(-50, 50)
|
|
st.range_add(l, r, v)
|
|
for i in range(l, r + 1):
|
|
arr[i] += v
|
|
self.assertEqual(st.to_list(), arr)
|
|
self.assertEqual(st.range_sum(0, n - 1), sum(arr))
|
|
|
|
|
|
class TestBoundsContract(unittest.TestCase):
|
|
"""Out-of-range raises IndexError; nothing is silently clamped."""
|
|
|
|
def test_right_edge_out_of_range_raises(self):
|
|
st = SegmentTree([1, 2, 3])
|
|
with self.assertRaises(IndexError):
|
|
st.range_sum(0, 3)
|
|
with self.assertRaises(IndexError):
|
|
st.range_sum(3, 3)
|
|
with self.assertRaises(IndexError):
|
|
st.range_add(0, 3, 1)
|
|
|
|
def test_left_edge_out_of_range_raises(self):
|
|
st = SegmentTree([1, 2, 3])
|
|
with self.assertRaises(IndexError):
|
|
st.range_sum(-1, 1)
|
|
with self.assertRaises(IndexError):
|
|
st.range_add(-5, 10, 1)
|
|
with self.assertRaises(IndexError):
|
|
st.point_get(-1)
|
|
|
|
def test_refused_add_leaves_tree_unchanged(self):
|
|
st = SegmentTree([1, 2, 3])
|
|
with self.assertRaises(IndexError):
|
|
st.range_add(-5, 10, 1)
|
|
self.assertEqual(st.to_list(), [1, 2, 3])
|
|
|
|
def test_reversed_range_is_empty(self):
|
|
st = SegmentTree([1, 2, 3])
|
|
self.assertEqual(st.range_sum(2, 1), 0)
|
|
st.range_add(2, 1, 5)
|
|
self.assertEqual(st.to_list(), [1, 2, 3])
|
|
|
|
|
|
class TestEmptyTree(unittest.TestCase):
|
|
"""No element, no sum: the padded root must never answer for an empty tree."""
|
|
|
|
def test_empty_tree_rejects_range_sum(self):
|
|
st = SegmentTree([])
|
|
with self.assertRaises(IndexError):
|
|
st.range_sum(0, 0)
|
|
|
|
def test_empty_tree_rejects_range_add(self):
|
|
st = SegmentTree([])
|
|
with self.assertRaises(IndexError):
|
|
st.range_add(0, 0, 5)
|
|
self.assertEqual(st.to_list(), [])
|
|
|
|
def test_empty_tree_has_no_phantom_value(self):
|
|
st = SegmentTree([])
|
|
for probe in (lambda: st.range_sum(0, 0), lambda: st.point_get(0)):
|
|
try:
|
|
got = probe()
|
|
except IndexError:
|
|
continue
|
|
self.fail(f"empty tree answered {got!r} where there is no element")
|
|
|
|
|
|
class TestPaddingInvariant(unittest.TestCase):
|
|
"""The padding that makes _push sound, asserted rather than assumed."""
|
|
|
|
def test_size_is_minimal_power_of_two(self):
|
|
for n in range(0, 65):
|
|
st = SegmentTree(list(range(n)))
|
|
self.assertGreaterEqual(st.size, max(1, n), msg=f"n={n}")
|
|
self.assertEqual(st.size & (st.size - 1), 0, msg=f"n={n} not a power of two")
|
|
if st.size > 1:
|
|
self.assertLess(st.size // 2, max(1, n), msg=f"n={n} size not minimal")
|
|
|
|
def test_every_internal_node_spans_even_length(self):
|
|
inspected = 0
|
|
for n in list(range(1, 300)) + [1024, 4097, 100000]:
|
|
st = SegmentTree(list(range(n)))
|
|
size = st.size
|
|
self.assertEqual(size & (size - 1), 0, msg=f"n={n}")
|
|
for node in range(1, size):
|
|
span = size >> (node.bit_length() - 1)
|
|
inspected += 1
|
|
self.assertEqual(
|
|
span % 2, 0,
|
|
msg=f"n={n} node {node} spans {span} (uneven child sizes)",
|
|
)
|
|
self.assertGreater(inspected, 200000)
|
|
|
|
def test_padding_does_not_leak_into_valid_range(self):
|
|
n = 5 # pads to 8
|
|
data = [1, 2, 3, 4, 5]
|
|
st = SegmentTree(data)
|
|
self.assertEqual(st.size, 8)
|
|
st.range_add(0, n - 1, 7)
|
|
self.assertEqual(st.range_sum(0, n - 1), sum(data) + 7 * n)
|
|
self.assertEqual(st.to_list(), [x + 7 for x in data])
|
|
|
|
|
|
class TestLazyCorrectness(unittest.TestCase):
|
|
"""Hand-checked cases for the push path."""
|
|
|
|
def test_overlapping_range_adds(self):
|
|
st = SegmentTree([0, 0, 0, 0, 0])
|
|
st.range_add(0, 2, 1)
|
|
st.range_add(1, 3, 10)
|
|
st.range_add(2, 4, 100)
|
|
self.assertEqual(st.to_list(), [1, 11, 111, 110, 100])
|
|
self.assertEqual(st.range_sum(1, 3), 232)
|
|
self.assertEqual(st.range_sum(2, 2), 111)
|
|
|
|
def test_single_element_tree(self):
|
|
st = SegmentTree([5])
|
|
self.assertEqual(st.range_sum(0, 0), 5)
|
|
st.range_add(0, 0, -2)
|
|
self.assertEqual(st.point_get(0), 3)
|
|
with self.assertRaises(IndexError):
|
|
st.range_sum(0, 1)
|
|
|
|
|
|
class TestCellInventory(unittest.TestCase):
|
|
"""A deleted cell must turn the gate red, not just shrink Ran N."""
|
|
|
|
MIN_CELLS = 15
|
|
|
|
def test_no_cell_was_deleted(self):
|
|
module = sys.modules[__name__]
|
|
names = set()
|
|
for obj in vars(module).values():
|
|
if isinstance(obj, type) and issubclass(obj, unittest.TestCase):
|
|
names.update(n for n in dir(obj) if n.startswith("test_"))
|
|
self.assertGreaterEqual(
|
|
len(names), self.MIN_CELLS,
|
|
msg=f"only {len(names)} cells present; {self.MIN_CELLS} expected",
|
|
)
|