Demetrescu-Italiano dynamic APSP algorithm in Python
Dynamic all-pairs shortest paths for directed graphs with amortized update bounds.
- File
demetrescu_italiano_apsp.py, 169 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/demetrescu-italiano-apsp/. The authors are listed in prov/5HVW8R.jsonl.
"""Fully dynamic all-pairs shortest paths with locally shortest paths.
This is the core of the Demetrescu-Italiano method: vertex updates with a
cleanup phase and a fixup phase over a store of paths. Old paths stay in the
store until a cleanup removes them. The smoothing step of the paper, which
gives the amortized bound, is not implemented.
"""
import heapq
import math
class _Path:
"""A path x..y. l is the path without its last edge, r without its first."""
__slots__ = ("pid", "x", "y", "w", "mask", "l", "r", "last", "lext", "rext")
def __init__(self, pid, x, y, w, mask, l, r, last):
self.pid = pid
self.x, self.y = x, y
self.w, self.mask = w, mask # weight, and a tie-break bit per edge
self.l, self.r = l, r
self.last = last if last is not None else self # the last edge as a path
self.lext = {} # paths P with P.l is self
self.rext = {} # paths P with P.r is self
class DynamicAPSP:
"""Directed graph on vertices 0..n-1 with nonnegative edge weights."""
def __init__(self, n):
self.n = n
self._next = 0
self._triv = [self._new(v, v, 0, 0, None, None) for v in range(n)]
self.weights = {} # (a, b) -> weight of the edge from a to b
self.edges = {} # (a, b) -> edge path
self.store = {} # (l.pid, r.pid) -> path, for paths with at least two edges
self.cand = {} # (x, y) -> heap of (w, mask, pid, path) of stored paths
self.alive = set() # pids of stored paths
self.sp = {} # (x, y) -> the shortest path from x to y
self.lstar = {} # pid of s -> {pid: P} for registered P with P.r is s
self.rstar = {} # pid of s -> {pid: P} for registered P with P.l is s
def _new(self, x, y, w, mask, l, r, last=None):
p = _Path(self._next, x, y, w, mask, l, r, last)
self._next += 1
return p
def _add(self, p):
self.alive.add(p.pid)
heapq.heappush(self.cand.setdefault((p.x, p.y), []), (p.w, p.mask, p.pid, p))
def _register(self, p):
self.sp[(p.x, p.y)] = p
self.lstar.setdefault(p.r.pid, {})[p.pid] = p
self.rstar.setdefault(p.l.pid, {})[p.pid] = p
def _unregister(self, p):
del self.sp[(p.x, p.y)]
self.lstar.get(p.r.pid, {}).pop(p.pid, None)
self.rstar.get(p.l.pid, {}).pop(p.pid, None)
def update(self, v, changes):
"""Vertex update. changes maps (a, b) with v in (a, b) to a weight, or None to delete."""
for (a, b), w in changes.items():
if v not in (a, b) or a == b:
raise ValueError("each changed edge must touch v and not be a loop")
if w is not None and (w < 0 or math.isnan(w)):
raise ValueError("weights must be nonnegative")
self._cleanup(v) # @line L1
for (a, b), w in changes.items(): # @line L2
if w is None:
self.weights.pop((a, b), None)
else:
self.weights[(a, b)] = w
self._fixup(v) # @line L3
def _cleanup(self, v):
queue = [p for (a, b), p in self.edges.items() if v in (a, b)] # @line L4
for p in queue:
del self.edges[(p.x, p.y)]
while queue: # @line L5
p = queue.pop()
if p.pid not in self.alive:
continue
self.alive.discard(p.pid) # @line L6
if p.l is not None and p.l.l is not None:
self.store.pop((p.l.pid, p.r.pid), None)
p.l.lext.pop(p.pid, None)
p.r.rext.pop(p.pid, None)
if self.sp.get((p.x, p.y)) is p: # @line L7
self._unregister(p)
queue.extend(p.lext.values()) # @line L8
queue.extend(p.rext.values())
def _fixup(self, v):
for (a, b), w in self.weights.items(): # @line L9
if v in (a, b) and (a, b) not in self.edges:
p = self._new(a, b, w, 1 << self._next, self._triv[a], self._triv[b])
self.edges[(a, b)] = p
self._add(p)
heap = [] # @line L10
for h in self.cand.values():
while h and h[0][2] not in self.alive:
heapq.heappop(h)
if h:
heap.append(h[0])
heapq.heapify(heap)
settled = set()
while heap: # @line L11
_, _, _, p = heapq.heappop(heap) # @line L12
if p.pid not in self.alive or (p.x, p.y) in settled: # @line L13
continue
settled.add((p.x, p.y)) # @line L14
old = self.sp.get((p.x, p.y))
if old is p: # @line L15
continue
if old is not None: # @line L16
self._unregister(old)
self._register(p) # @line L17
for q in list(self.lstar.get(p.l.pid, {}).values()): # @line L18
if q.x != p.y:
self._combine(q, p, heap)
for q in list(self.rstar.get(p.r.pid, {}).values()): # @line L19
if q.y != p.x:
self._combine(p, q, heap)
def _combine(self, a, b, heap):
"""Store the path a.x .. b.y with l = a and r = b, if it is new."""
k = (a.pid, b.pid)
if k in self.store: # @line L20
return
e = b.last
p = self._new(a.x, b.y, a.w + e.w, a.mask + e.mask, a, b, e) # @line L21
self.store[k] = p # @line L22
a.lext[p.pid] = p
b.rext[p.pid] = p
self._add(p)
heapq.heappush(heap, (p.w, p.mask, p.pid, p)) # @line L23
def insert_edge(self, a, b, w):
self.update(a, {(a, b): w})
def delete_edge(self, a, b):
self.update(a, {(a, b): None})
def dist(self, x, y):
"""The weight of a shortest path from x to y, or math.inf."""
if x == y:
return 0
p = self.sp.get((x, y))
return math.inf if p is None else p.w
def path(self, x, y):
"""The vertices of a shortest path from x to y, or None."""
if x == y:
return [x]
p = self.sp.get((x, y))
if p is None:
return None
out = []
while p.l is not None:
out.append(p.y)
p = p.l
out.append(p.x)
return out[::-1]
test_demetrescu_italiano_apsp.py 119 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/demetrescu-italiano-apsp/. The authors are listed in prov/5HVW8R.jsonl.
import heapq
import math
import random
from demetrescu_italiano_apsp import DynamicAPSP
def static_dist(n, weights):
"""All distances by one Dijkstra run per source, from zero."""
out = [[math.inf] * n for _ in range(n)]
adj = [[] for _ in range(n)]
for (a, b), w in weights.items():
adj[a].append((b, w))
for s in range(n):
d = out[s]
d[s] = 0
h = [(0, s)]
while h:
du, u = heapq.heappop(h)
if du > d[u]:
continue
for v, w in adj[u]:
if du + w < d[v]:
d[v] = du + w
heapq.heappush(h, (d[v], v))
return out
def check(g, weights):
n = g.n
want = static_dist(n, weights)
for x in range(n):
for y in range(n):
assert g.dist(x, y) == want[x][y], (x, y, g.dist(x, y), want[x][y])
p = g.path(x, y)
if want[x][y] == math.inf:
assert p is None
else:
assert p[0] == x and p[-1] == y and len(set(p)) == len(p)
assert sum(weights[(a, b)] for a, b in zip(p, p[1:])) == want[x][y]
def test_example():
g = DynamicAPSP(4)
g.update(1, {(0, 1): 3, (1, 2): 1, (1, 3): 4})
g.update(2, {(0, 2): 5, (2, 3): 1})
assert [g.dist(0, y) for y in range(4)] == [0, 3, 4, 5]
assert g.path(0, 3) == [0, 1, 2, 3]
g.update(1, {(0, 1): None, (1, 3): 1})
assert [g.dist(0, y) for y in range(4)] == [0, math.inf, 5, 6]
assert g.path(0, 3) == [0, 2, 3]
g.update(1, {(0, 1): 1})
assert [g.dist(0, y) for y in range(4)] == [0, 1, 2, 2]
assert g.path(0, 3) == [0, 1, 3]
def test_edges():
g = DynamicAPSP(3)
assert g.dist(0, 0) == 0 and g.dist(0, 1) == math.inf and g.path(0, 1) is None
g.insert_edge(0, 1, 0)
g.insert_edge(1, 0, 0)
assert g.dist(0, 1) == 0 and g.dist(1, 0) == 0
g.delete_edge(0, 1)
assert g.dist(0, 1) == math.inf
for bad in ({(1, 2): 1}, {(0, 0): 1}, {(0, 1): -1}):
try:
g.update(0, bad)
except ValueError:
pass
else:
raise AssertionError("no error")
def test_random_against_static():
rng = random.Random(5)
for trial in range(60):
n = rng.randint(2, 7)
g = DynamicAPSP(n)
weights = {}
for _ in range(40):
v = rng.randrange(n)
changes = {}
for _ in range(rng.randint(1, 4)):
u = rng.randrange(n)
if u == v:
continue
e = (v, u) if rng.random() < 0.5 else (u, v)
changes[e] = None if rng.random() < 0.3 else rng.randint(0, 6)
g.update(v, changes)
for e, w in changes.items():
if w is None:
weights.pop(e, None)
else:
weights[e] = w
check(g, weights)
def test_dense_ties():
rng = random.Random(11)
n = 6
g = DynamicAPSP(n)
weights = {}
for _ in range(150):
v = rng.randrange(n)
changes = {}
for u in range(n):
if u != v and rng.random() < 0.6:
changes[(v, u)] = rng.choice([None, 1, 1, 2])
changes[(u, v)] = rng.choice([None, 1, 1, 2])
g.update(v, changes)
for e, w in changes.items():
if w is None:
weights.pop(e, None)
else:
weights[e] = w
check(g, weights)
What the next level needs
- the tests did not run in this build
Read the full page about Demetrescu-Italiano dynamic APSP algorithm