Even-Shiloach tree in Python
Maintains bounded-depth shortest-path distances under edge deletions.
- File
even_shiloach_tree.py, 100 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/even-shiloach-tree/. The authors are listed in prov/VS2KBW.jsonl.
"""Even-Shiloach tree: single-source distances up to a depth D under deletions.
Directed graph with positive integer edge weights (1 for the classic
unweighted case). Each vertex v keeps a level l(v), which equals the distance
from the source when that distance is at most D, and infinity otherwise. Each
vertex also keeps a scan pointer into its list of incoming edges; edges before
the pointer are known not to give l(v). Levels only grow, by one at a time,
so the total time over all deletions is O(m D). Deletions only (decremental);
deterministic; the bound is a total, so per deletion it is amortized.
"""
import heapq
INF = float("inf")
class EvenShiloachTree:
def __init__(self, n: int, edges: list[tuple[int, int, int]], source: int, depth: int) -> None:
self.n, self.s, self.depth = n, source, depth
self.tail: list[int] = []
self.head: list[int] = []
self.w: list[int] = []
self.alive: list[bool] = []
self.inc: list[list[int]] = [[] for _ in range(n)]
self.out: list[list[int]] = [[] for _ in range(n)]
for u, v, w in edges:
if w <= 0:
raise ValueError("weights must be positive integers")
e = len(self.tail)
self.tail.append(u)
self.head.append(v)
self.w.append(w)
self.alive.append(True)
self.inc[v].append(e)
self.out[u].append(e)
# L1: initial levels by Dijkstra, cut at the depth bound.
lev = [INF] * n
lev[source] = 0
heap = [(0, source)]
while heap:
d, x = heapq.heappop(heap)
if d > lev[x]:
continue
for e in self.out[x]:
y, nd = self.head[e], d + self.w[e]
if nd <= depth and nd < lev[y]:
lev[y] = nd
heapq.heappush(heap, (nd, y))
self.level = lev # @line L1
self.ptr = [0] * n
for v in range(n):
if v != source and lev[v] != INF:
self._settle_pointer(v)
def _attains(self, e: int, x: int) -> bool:
return self.alive[e] and self.level[self.tail[e]] + self.w[e] == self.level[x]
def _settle_pointer(self, x: int) -> bool:
"""Advance the scan pointer of x to an edge that gives l(x)."""
lst = self.inc[x]
p = self.ptr[x]
while p < len(lst) and not self._attains(lst[p], x):
p += 1 # @line L5
self.ptr[x] = p
return p < len(lst)
def delete(self, e: int) -> None:
"""Delete the edge with id e (ids follow the order of the input list)."""
if not self.alive[e]:
return
self.alive[e] = False # @line L2
queue = [self.head[e]] # @line L3
while queue:
x = queue.pop() # @line L4
if x == self.s or self.level[x] == INF:
continue
if self._settle_pointer(x):
continue
# No incoming edge gives l(x): raise the level by one.
self.level[x] += 1 # @line L6
self.ptr[x] = 0
if self.level[x] > self.depth:
self.level[x] = INF # @line L7
else:
queue.append(x)
for f in self.out[x]:
if self.alive[f]:
queue.append(self.head[f]) # @line L8
def distance(self, v: int) -> float:
return self.level[v] # @line L9
def parent(self, v: int) -> int:
"""The tail of the tree edge into v, or -1 at the source or if unreachable."""
if v == self.s or self.level[v] == INF:
return -1
return self.tail[self.inc[v][self.ptr[v]]]
test_even_shiloach_tree.py 74 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/even-shiloach-tree/. The authors are listed in prov/VS2KBW.jsonl.
import heapq
import random
from even_shiloach_tree import INF, EvenShiloachTree
def _dist(n: int, edges: list[tuple[int, int, int]], alive: list[bool], s: int, depth: int) -> list[float]:
adj: list[list[tuple[int, int]]] = [[] for _ in range(n)]
for i, (u, v, w) in enumerate(edges):
if alive[i]:
adj[u].append((v, w))
d = [INF] * n
d[s] = 0
pq = [(0, s)]
while pq:
du, u = heapq.heappop(pq)
if du > d[u]:
continue
for v, w in adj[u]:
if du + w < d[v]:
d[v] = du + w
heapq.heappush(pq, (d[v], v))
return [x if x <= depth else INF for x in d]
def test_example() -> None:
edges = [(0, 1, 1), (0, 2, 1), (1, 3, 1), (2, 3, 1), (3, 4, 1), (2, 4, 1), (4, 5, 1), (1, 5, 1)]
t = EvenShiloachTree(6, edges, 0, 4)
assert [t.distance(v) for v in range(6)] == [0, 1, 1, 2, 2, 2]
t.delete(5) # 2 -> 4
assert [t.distance(v) for v in range(6)] == [0, 1, 1, 2, 3, 2]
t.delete(0) # 0 -> 1
assert [t.distance(v) for v in range(6)] == [0, INF, 1, 2, 3, 4]
t.delete(3) # 2 -> 3
assert [t.distance(v) for v in range(6)] == [0, INF, 1, INF, INF, INF]
def test_random_against_dijkstra() -> None:
rng = random.Random(11)
for _ in range(500):
n = rng.randint(1, 8)
m = rng.randint(0, 20)
maxw = rng.choice([1, 1, 3])
edges = [(rng.randrange(n), rng.randrange(n), rng.randint(1, maxw)) for _ in range(m)]
s = rng.randrange(n)
depth = rng.randint(0, 3 * n)
t = EvenShiloachTree(n, edges, s, depth)
alive = [True] * m
order = list(range(m))
rng.shuffle(order)
assert [t.distance(v) for v in range(n)] == _dist(n, edges, alive, s, depth)
for e in order:
t.delete(e)
alive[e] = False
exp = _dist(n, edges, alive, s, depth)
assert [t.distance(v) for v in range(n)] == exp
for v in range(n):
p = t.parent(v)
if p != -1:
assert exp[p] < exp[v]
def test_long_path() -> None:
n = 600
edges = [(i, i + 1, 1) for i in range(n - 1)] + [(0, n - 1, 1)]
t = EvenShiloachTree(n, edges, 0, n)
assert t.distance(n - 1) == 1
t.delete(n - 1)
assert t.distance(n - 1) == n - 1
t.delete(0)
assert t.distance(5) == INF
What the next level needs
- the tests did not run in this build