Algorithm Search⌘K

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
even_shiloach_tree.pyRawGitHub
# 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

Read the full page about Even-Shiloach tree