diff --git a/wizard-work/beam/dsu_rollback.py b/wizard-work/beam/dsu_rollback.py index 83e5814..ee80708 100644 --- a/wizard-work/beam/dsu_rollback.py +++ b/wizard-work/beam/dsu_rollback.py @@ -6,13 +6,16 @@ cheaply reversed. This implementation uses union by size and records enough state to undo each union in O(1). API: - DSU(n) - find(x) -> root (no path compression) - union(a, b) -> bool (True if merged) + DSU(n) n < 0 -> ValueError + find(x) -> root no path compression; TypeError if x is not an int or + is a bool, ValueError if x is outside [0, n) + union(a, b) -> bool True if merged; both arguments are checked as above + and a refused call mutates nothing snapshot() -> int (marker) - rollback(marker) -> None (revert to marker) - size(x) -> component size - connected(a, b) -> bool + rollback(marker) -> None revert to marker; TypeError if marker is not an int + or is a bool, ValueError if marker outside [0, len(history)] + size(x) -> component size index checked as find + connected(a, b) -> bool both indexes checked as find """ from typing import List, Tuple diff --git a/wizard-work/beam/test_dsu_rollback.py b/wizard-work/beam/test_dsu_rollback.py index e786484..3b9423c 100644 --- a/wizard-work/beam/test_dsu_rollback.py +++ b/wizard-work/beam/test_dsu_rollback.py @@ -61,12 +61,17 @@ class TestDSURollback(unittest.TestCase): d.rollback(-1) def test_no_path_compression_find_stable(self): + # Q94 bullet 2: the node asked about must sit TWO levels down, else a + # compressing find rewrites nothing and the cell is vacuous. + # union-by-size: union(0,1)==(1<-0), union(2,3)==(3<-2), union(1,2)==(2<-0) d = DSU(4) - d.union(0, 1); d.union(1, 2) + d.union(0, 1); d.union(2, 3); d.union(1, 2) + self.assertEqual(d.parent, [0, 0, 0, 2]) + self.assertEqual(d.parent[3], 2) # precondition: depth 2 before = list(d.parent) - r = d.find(2) - self.assertEqual(r, d.find(0)) - self.assertEqual(d.parent[2], before[2]) + self.assertEqual(d.find(3), 0) # root via parent[3] -> 2 -> 0 + self.assertEqual(d.parent, before) # a compressing find writes parent[3]=0 + self.assertEqual(d.size(3), 4) def test_size_after_rollback(self): d = DSU(3) @@ -112,6 +117,48 @@ class TestDSURollback(unittest.TestCase): with self.assertRaises((TypeError, ValueError)): d.rollback(bad) + def _fingerprint(self, d): + return (list(d.parent), list(d.sz), list(d.history), d.components()) + + def test_refuses_out_of_range_index(self): + # Q94 bullet 3: every entry point that takes an index refuses a range + # violation, and a refused call mutates nothing. + d = DSU(3) + d.union(0, 1) + before = self._fingerprint(d) + for bad in (-1, 3, 10): + for name in ("find", "size"): + with self.assertRaises(ValueError, msg="%s(%r)" % (name, bad)): + getattr(d, name)(bad) + self.assertEqual(self._fingerprint(d), before) + for args in ((bad, 0), (0, bad)): + with self.assertRaises(ValueError, msg="union%r" % (args,)): + d.union(*args) + self.assertEqual(self._fingerprint(d), before) + with self.assertRaises(ValueError, msg="connected%r" % (args,)): + d.connected(*args) + self.assertEqual(self._fingerprint(d), before) + + def test_refuses_non_int_index(self): + # Q94 bullet 4: a bool is an int subclass in Python; True must not alias + # index 1 and False must not merge index 0. + d = DSU(3) + before = self._fingerprint(d) + for bad in (True, False): + with self.assertRaises(TypeError, msg="find(%r)" % (bad,)): + d.find(bad) + with self.assertRaises(TypeError, msg="size(%r)" % (bad,)): + d.size(bad) + with self.assertRaises(TypeError, msg="union(%r, 1)" % (bad,)): + d.union(bad, 1) + with self.assertRaises(TypeError, msg="connected(%r, 0)" % (bad,)): + d.connected(bad, 0) + self.assertEqual(self._fingerprint(d), before) + with self.assertRaises(TypeError): + d.find(1.5) + with self.assertRaises(TypeError): + d.find(None) + if __name__ == "__main__": unittest.main()