# -*- coding: utf-8 -*-
# UNFINISHED - do not use
from __future__ import print_function, division, absolute_import, unicode_literals
import collections # NOQA
import networkx as nx
import utool as ut
print, rrr, profile = ut.inject2(__name__)
# import bintrees
# import rbtree
[docs]def euler_tour_dfs(G, source=None):
"""adaptation of networkx dfs"""
if source is None:
# produce edges for all components
nodes = G
else:
# produce edges for components with source
nodes = [source]
yielder = []
visited = set()
for start in nodes:
if start in visited:
continue
visited.add(start)
stack = [(start, iter(G[start]))]
while stack:
parent, children = stack[-1]
try:
child = next(children)
if child not in visited:
# yielder += [[parent, child]]
yielder += [parent]
visited.add(child)
stack.append((child, iter(G[child])))
except StopIteration:
if stack:
last = stack[-1]
yielder += [last[0]]
stack.pop()
return yielder
[docs]@profile
def comparison():
r"""
CommandLine:
python -m utool.experimental.dynamic_connectivity comparison --profile
python -m utool.experimental.dynamic_connectivity comparison
"""
n = 12
a, b = 9, 20
num = 3
import utool
for timer in utool.Timerit(num, 'old bst version (PY)'):
g = nx.balanced_tree(2, n)
self = TestETT.from_tree(g, version='bst', fast=False)
with timer:
self.delete_edge_bst_version(a, b, bstjoin=False)
import utool
for timer in utool.Timerit(num, 'new bst version (PY) (with join)'):
g = nx.balanced_tree(2, n)
self = TestETT.from_tree(g, version='bst', fast=False)
with timer:
self.delete_edge_bst_version(a, b, bstjoin=True)
import utool
for timer in utool.Timerit(num, 'old bst version (C)'):
g = nx.balanced_tree(2, n)
self = TestETT.from_tree(g, version='bst', fast=True)
with timer:
self.delete_edge_bst_version(a, b, bstjoin=False)
import utool
for timer in utool.Timerit(num, 'new bst version (C) (with join)'):
g = nx.balanced_tree(2, n)
self = TestETT.from_tree(g, version='bst', fast=True)
with timer:
self.delete_edge_bst_version(a, b, bstjoin=True)
import utool
for timer in utool.Timerit(num, 'list version'):
g = nx.balanced_tree(2, n)
self = TestETT.from_tree(g, version='list')
with timer:
self.delete_edge_list_version(a, b)
pass
[docs]class TestETT(object):
"""
raise NotImplementedError()
hg clone https://bitbucket.org/mozman/bintrees
git clone git@github.com:Erotemic/bintrees.git
References:
https://courses.csail.mit.edu/6.851/spring07/scribe/lec05.pdf
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.192.8615&rep=rep1&type=pdf
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.208.2351&rep=rep1&type=pdf
https://en.wikipedia.org/wiki/Euler_tour_technique
Example:
>>> # DISABLE_DOCTEST
>>> from utool.experimental.dynamic_connectivity import * # NOQA
>>> edges = [(1, 2), (1, 6), (1, 5), (2, 3), (2, 4)]
>>> edges = [
>>> ('R', 'A'), ('R', 'B'),
>>> ('B', 'C'), ('C', 'D'), ('C', 'E'),
>>> ('B', 'F'), ('B', 'G'),
>>> ]
>>> mst = nx.Graph(edges)
>>> #mst = nx.balanced_tree(2, 11)
>>> self = TestETT.from_tree(mst)
>>> import wbia.plottool as pt
>>> pt.qt4ensure()
>>> pt.show_nx(mst)
>>> mst = nx.balanced_tree(2, 4)
"""
def __init__(self):
pass
[docs] @classmethod
@profile
def from_tree(cls, mst, version='bst', fast=True):
"""
>>> # DISABLE_DOCTEST
>>> from utool.experimental.dynamic_connectivity import * # NOQA
>>> mst = nx.balanced_tree(2, 4)
>>> self = TestETT.from_tree(mst)
>>> import wbia.plottool as pt
>>> pt.qt4ensure()
>>> pt.show_nx(self.to_networkx(), pnum=(2, 1, 1), fnum=1)
>>> a, b = 2, 5
>>> other = self.delete_edge_bst_version(a, b)
>>> pt.show_nx(other.to_networkx(), pnum=(2, 1, 1), fnum=2)
"""
tour = euler_tour_dfs(mst)
self = cls.from_tour(tour, version=version, fast=fast)
return self
[docs] @classmethod
@profile
def from_tour(cls, tour, version='bst', fast=True):
import bintrees
self = cls()
self.fast = fast
self.version = version
if version == 'bst':
# self.tour = tour
if fast:
tree = bintrees.FastAVLTree(enumerate(tour))
else:
tree = bintrees.AVLTree(enumerate(tour))
self.first_lookup = first_lookup = {}
self.last_lookup = last_lookup = {}
for key, node in tree.iter_items():
# node = avl_node.value
if node not in first_lookup:
# first_lookup[node] = avl_node.key
first_lookup[node] = key
# last_lookup[node] = avl_node.key
last_lookup[node] = key
self.tour_tree = tree
elif version == 'sortedcontainers':
"""
>>> from utool.experimental.dynamic_connectivity import * # NOQA
>>> mst = nx.balanced_tree(2, 4)
>>> tour = euler_tour_dfs(mst)
>>> self = TestETT()
"""
import sortedcontainers
tour_order = sortedcontainers.SortedList(enumerate(tour))
self.first_lookup = dict(i[::-1] for i in tour_order[::-1])
self.last_lookup = dict(i[::-1] for i in tour_order)
self.tour_order = tour_order
else:
self.tour = tour
tour_order = list(enumerate(tour))
self.first_lookup = dict(i[::-1] for i in tour_order[::-1])
self.last_lookup = dict(i[::-1] for i in tour_order)
tour_order.bisect_left((7, 0))
return self
[docs] @profile
def delete_edge_bst_version(self, a, b, bstjoin=False):
"""
a, b = (2, 5)
print(self.first_lookup[a] > self.first_lookup[b])
tree = self.tour_tree
list(tree.item_slice(k1, k2))
"""
if self.first_lookup[a] > self.last_lookup[b]:
a, b = b, a
o_a1 = self.first_lookup[a]
o_a2 = self.last_lookup[a]
o_b1 = self.first_lookup[b]
o_b2 = self.last_lookup[b]
assert o_a1 < o_b1
# assert o_b1 < o_b2
assert o_b2 < o_a2
if bstjoin:
# splice out the inside contiguous range inplace
inside, outside = self.tour_tree.splice_inplace(o_b1, o_b2 + 1)
# Remove unneeded values
outside = outside.splice_inplace(o_b1, o_a2 + 1)[1]
other = TestETT()
other.tour_tree = inside
# We can reuse these pointers without any modification
other.first_lookup = self.first_lookup
other.first_lookup = self.last_lookup
# Should make an O(n) cleanup step at some point
else:
# ET(T2) inner - is given by the interval of ET (o_b1, o_b2)
# Smaller compoment is reconstructed
# in amortized O(log(n)) time
t2_slice = self.tour_tree[o_b1 : o_b2 + 1]
t2_tour = list(t2_slice.values())
other = TestETT.from_tour(t2_tour, version=self.version, fast=self.fast)
# ET(T1) outer - is given by splicing out of ET the sequence
# (o_b1, o_a2)
t1_splice = self.tour_tree[o_b1 : o_a2 + 1]
self.tour_tree.remove_items(t1_splice)
return other
[docs] @profile
def delete_edge_list_version(self, a, b):
if self.first_lookup[a] > self.last_lookup[b]:
a, b = b, a
o_a1 = self.first_lookup[a]
o_a2 = self.last_lookup[a]
o_b1 = self.first_lookup[b]
o_b2 = self.last_lookup[b]
assert o_a1 < o_b1
# assert o_b1 < o_b2
assert o_b2 < o_a2
t2_list = self.tour[o_b1 : o_b2 + 1]
other = TestETT.from_tour(t2_list, version=self.version, fast=self.fast)
# ET(T1) outer - is given by splicing out of ET the sequence
# (o_b1, o_a2)
self.tour = self.tour[:o_b1] + self.tour[o_a2 + 1 :]
# need to recompute lookups O(n) style
# maybe we can do better?
# Keep old keys
if False:
tour_order = list(enumerate(self.tour))
self.first_lookup = dict(i[::-1] for i in tour_order[::-1])
self.last_lookup = dict(i[::-1] for i in tour_order)
return other
[docs] def reroot(self, s):
"""
s = 3
s = 'B'
Let os denote any occurrence of s.
Splice out the first part of the sequence ending with the occurrence before os,
remove its first occurrence (or),
and tack this on to the end of the sequence which now begins with os.
Add a new occurrence os to the end.
"""
# Splice out the first part of the sequence ending with the occurrence before os
# remove its first occurrence (or),
o_s1 = self.first_lookup[s]
splice1 = self.tour[1:o_s1]
rest = self.tour[o_s1 + 1 :]
new_tour = [s] + rest + splice1 + [s]
new_tree = TestETT.from_tour(new_tour, version=self.version, fast=self.fast)
return new_tree
[docs] def to_networkx(self):
import utool as ut
return nx.Graph(ut.itertwo(self.tour))
[docs] def join_trees(self, t1, t2, e):
pass
# class AVLMaster(object):
# def __init__(self, master=None):
# self.master = None
# class AVLKey(object):
# def __init__(self, master=None):
# self.master = None
[docs]class EulerTourList(object):
"""
load-list representation of an Euler tour inspired by sortedcontainers
this doesnt work for the same reason keyed bintrees dont work
the indexing needs to be implicit, but this has explicit indexes
"""
def __init__(self, iterable, load=1000):
self.first = {}
self.last = {}
self._len = 0
self._cumlen = []
self._lists = []
self._load = load
self._twice = load * 2
self._half = load >> 1
self._offset = 0
if iterable is not None:
self.update(iterable)
def __iter__(self):
import itertools as it
return it.chain.from_iterable(self._lists)
def __repr__(self):
return 'EulerTourList(' + str(list(self)) + ')'
[docs] def join(self, other):
r"""
Args:
other (?):
CommandLine:
python -m sortedcontainers.sortedlist join2
Example:
>>> from utool.experimental.dynamic_connectivity import * # NOQA
>>> self = EulerTourList([1, 2, 3, 2, 4, 2, 1], load=3)
>>> other = EulerTourList([0, 5, 9, 5, 0], load=3)
>>> result = self.join(other)
>>> print(result)
"""
assert self._load == other._load, 'loads must be the same'
self._lists.extend(other._lists)
self._cumlen.extend([c + self._len for c in other._cumlen])
self._len += other._len
[docs] def split(self, pos, idx):
(pos, idx) = self._pos(idx)
left_part = self._lists[0 : pos + 1]
right_part = self._lists[pos + 1 : 0]
left_last = left_part[-1][:idx]
right_first = left_part[-1][idx:]
if left_last:
left_part[-1] = left_last
else:
del left_part[-1]
if right_first:
right_part.insert(0, right_first)
other = EulerTourList()
other._list = right_part
[docs] def append(self, value):
pass
[docs] def update(self, iterable):
_lists = self._lists
values = list(iterable)
_load = self._load
_lists.extend(values[pos : (pos + _load)] for pos in range(0, len(values), _load))
self._cumlen = ut.cumsum(map(len, _lists))
self._len = len(values)
[docs]class EulerTourTree(object):
"""
CommandLine:
python -m utool.experimental.dynamic_connectivity EulerTourTree --show
Example:
>>> # DISABLE_DOCTEST
>>> from utool.experimental.dynamic_connectivity import * # NOQA
>>> #mst = nx.balanced_tree(2, 2)
>>> edges = [
>>> ('R', 'A'), ('R', 'B'),
>>> ('B', 'C'), ('C', 'D'), ('C', 'E'),
>>> ('B', 'F'), ('B', 'G'),
>>> ]
>>> mst = nx.Graph(edges)
>>> self = EulerTourTree.from_tree(mst)
>>> import wbia.plottool as pt
>>> pt.qt4ensure()
>>> fnum = 1
>>> pnum_ = pt.make_pnum_nextgen(1, 3)
>>> pt.show_nx(mst, pnum=pnum_(), fnum=fnum)
>>> pt.show_nx(self.to_networkx(), pnum=pnum_(), fnum=fnum)
>>> pt.show_nx(self.tour_tree.to_networkx(labels=['key', 'value']), pnum=pnum_(), fnum=fnum)
>>> print(self.tour)
>>> print(self.first_lookup)
>>> print(self.last_lookup)
>>> ut.show_if_requested()
"""
def __init__(self):
# node attributes in reprsented graph
self.first_lookup = {}
self.last_lookup = {}
# structure stored in auxillary graph
self.tour_tree = None
# self.tour = None
[docs] def to_networkx(self):
import utool as ut
# n = list(self.tour_tree._traverse_nodes())[0]
# return nx.Graph(ut.itertwo(self.tour))
# In order traversal of the tree is the tour order
# return nx.Graph(ut.itertwo(self.tour_tree.values()))
tour = (n.value for n in self.tour_tree._traverse_nodes())
graph = nx.Graph(ut.itertwo(tour))
return graph
[docs] def find_root(self, node):
pass
[docs] @classmethod
@profile
def from_tree(cls, mst, fast=True, start=0):
tour = euler_tour_dfs(mst)
self = cls.from_tour(tour, fast=fast, start=0)
return self
[docs] @classmethod
@profile
def from_tour(cls, tour, fast=False, start=0):
import bintrees
self = cls()
self.fast = fast
if fast:
tour_tree = bintrees.FastAVLTree(enumerate(tour, start=start))
else:
tour_tree = bintrees.AVLTree(enumerate(tour, start=start))
self.first_lookup = first_lookup = {}
self.last_lookup = last_lookup = {}
for key, node in tour_tree.iter_items():
# node = avl_node.value
if node not in first_lookup:
# first_lookup[node] = avl_node.key
first_lookup[node] = key
# last_lookup[node] = avl_node.key
last_lookup[node] = key
self.tour_tree = tour_tree
# self.tour = tour
tour_order = list(enumerate(tour))
self.first_lookup = dict(i[::-1] for i in tour_order[::-1])
self.last_lookup = dict(i[::-1] for i in tour_order)
# tour_order.bisect_left((7, 0))
return self
[docs] def join(self, other):
pass
[docs] @profile
def cut(self, a, b, bstjoin=False):
"""
cuts edge (a, b) into two parts because this is a tree
a, b = (2, 5)
print(self.first_lookup[a] > self.first_lookup[b])
tree = self.tour_tree
list(tree.item_slice(k1, k2))
"""
if self.first_lookup[a] > self.last_lookup[b]:
a, b = b, a
o_a1 = self.first_lookup[a]
o_a2 = self.last_lookup[a]
o_b1 = self.first_lookup[b]
o_b2 = self.last_lookup[b]
assert o_a1 < o_b1
# assert o_b1 < o_b2
assert o_b2 < o_a2
# splice out the inside contiguous range inplace
inside, outside = self.tour_tree.splice_inplace(o_b1, o_b2 + 1)
# Remove unneeded values
outside = outside.splice_inplace(o_b1, o_a2 + 1)[1]
other = EulerTourTree()
other.tour_tree = inside
# We can reuse these pointers without any modification
other.first_lookup = self.first_lookup
other.first_lookup = self.last_lookup
# Should make an O(n) cleanup step at some point
return other
[docs] def reroot(self, s):
"""
s = 3
s = 'B'
Let os denote any occurrence of s.
Splice out the first part of the sequence ending with the occurrence before os,
remove its first occurrence (or),
and tack this on to the end of the sequence which now begins with os.
Add a new occurrence os to the end.
"""
# Splice out the first part of the sequence ending with the occurrence before os
# remove its first occurrence (or),
o_s1 = self.first_lookup[s]
splice1 = self.tour[1:o_s1]
rest = self.tour[o_s1 + 1 :]
new_tour = [s] + rest + splice1 + [s]
new_tree = TestETT.from_tour(new_tour, fast=self.fast)
return new_tree
[docs]class BinaryNode(object):
def __init__(self, value, parent=None, left=None, right=None):
self.value = value
self.parent = parent
self.children = [left, right]
self._dir = 0
@property
def left(self):
return self.children[0]
@left.setter
def left(self, other):
self.children[0] = other
@property
def right(self):
return self.children[1]
@right.setter
def right(self, other):
self.children[1] = other
[docs] def set_child(self, other, dir_):
other.parent = self
self.children[dir_] = other
[docs]class EulerTourForest(object):
def __init__(self):
self.aux_trees = {}
self.first = {}
self.last = {}
self.n_nodes = 0
# import bintrees
# self._cls = bintrees.AVLTree
[docs] def has_node(self, node):
return node in self.first
[docs] def add_node(self, node):
if not self.has_node(node):
binnode = BinaryNode(node)
self.aux_trees[node] = binnode
self.first[node] = binnode
self.last[node] = binnode
[docs] def find_root(self, node):
return self.first[node]
[docs] def reroot(self, old, new):
assert old == new
return new
[docs] def add_edge(self, u, v):
"""
self = EulerTourForest()
self.add_node(1)
self.add_node(2)
u, v = 1, 2
"""
# ubin = self.find_root(u)
ru = self.find_root(u)
rv = self.find_root(v)
ru = self.reroot(ru, u)
rv = self.reroot(rv, v)
u.set_child(v)
pass
[docs]class DummyEulerTourForest(object):
"""
maintain a forests of euler tour trees
This is a bad implementation, but will let us use the DynConnGraph api
"""
def __init__(self, nodes=None):
# mapping from root node to tree
self.trees = {}
if nodes is not None:
for node in nodes:
self.add_node(node)
def _check_node_type(self, node):
if not isinstance(node, (int, str)):
raise ValueError('only primative int/str objects can be nodes')
[docs] def add_node(self, node):
self._check_node_type(node)
root = self.find_root(node)
if root is None:
# self.trees[node] = EulerTourTree(node)
new_root = nx.Graph()
new_root.add_node(node)
self.trees[node] = new_root
[docs] def has_edge(self, u, v):
return any(tree.has_edge(u, v) for tree in self.components())
[docs] def find_root(self, node):
for root, subgraph in self.trees.items():
if subgraph.has_node(node):
return root
[docs] def subtree(self, node):
root = self.find_root(node)
subtree = self.trees[root]
return subtree
[docs] def remove_edge(self, u, v):
ru = self.find_root(u)
rv = self.find_root(v)
assert ru == rv
subtree = self.trees[ru]
del self.trees[ru]
subtree.remove_edge(u, v)
for new_tree in nx.connected_component_subgraphs(subtree):
if new_tree.has_node(u):
self.trees[u] = new_tree
elif new_tree.has_node(v):
self.trees[v] = new_tree
# raise NotImplementedError('remove edge')
[docs] def add_edge(self, u, v):
print('[euler_tour_forest] add_edge(%r, %r)' % (u, v))
if self.has_edge(u, v):
return
ru = self.find_root(u)
rv = self.find_root(v)
if ru is None:
self.add_node(u)
ru = u
if rv is None:
self.add_node(v)
rv = v
assert ru is not rv, 'u=%r, v=%r not disjoint, can only join disjoint edges' % (
u,
v,
)
assert ru in self.trees, 'ru must be a root node'
assert rv in self.trees, 'rv must be a root node'
subtree1 = self.trees[ru]
subtree2 = self.trees[rv]
del self.trees[rv]
new_tree = nx.compose(subtree1, subtree2)
new_tree.add_edge(u, v)
self.trees[ru] = new_tree
print(list(new_tree.nodes()))
assert nx.is_connected(new_tree)
assert nx.is_tree(new_tree)
[docs] def components(self):
return self.trees.values()
[docs] def to_networkx(self):
graph = nx.compose_all(self.components())
return graph
[docs]class DynConnGraph(object):
"""
Stores a spanning forest with Euler Tour Trees
References:
https://courses.csail.mit.edu/6.851/spring14/lectures/L20.pdf
https://courses.csail.mit.edu/6.851/spring14/lectures/L20.html
http://cs.stackexchange.com/questions/33595/what-is-the-most-efficient-algorithm-and-data-structure-for-maintaining-connecte
https://www.cs.princeton.edu/courses/archive/fall03/cs528/handouts/Poly%20logarithmic.pdf
DEFINES ET-Trees
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.192.8615&rep=rep1&type=pdf
https://www.cs.princeton.edu/courses/archive/fall03/cs528/handouts/Poly%20logarithmic.pdf
http://courses.csail.mit.edu/6.851/spring12/scribe/L20.pdf
http://courses.csail.mit.edu/6.854/16/Projects/B/dynamic-graphs-survey.pdf
http://dl.acm.org/citation.cfm?id=502095
http://delivery.acm.org/10.1145/510000/502095/p723-holm.pdf?ip=128.213.17.14&id=502095&acc=ACTIVE%20SERVICE&key=7777116298C9657D%2EAF047EA360787914%2E4D4702B0C3E38B35%2E4D4702B0C3E38B35&CFID=905563744&CFTOKEN=37809688&__acm__=1488222284_4ae91dd7a761430ee714f0c69c17b772
Notes:
Paper uses level 0 at top, but video lecture uses floor(log(n)) as top.
All edges start at level floor(log(n)).
The level of each edge will change over time, but cannot decrease below zero.
Let G[i] = subgraph graph at level i. (contains only edges of level i or greater)
Let F[i] be Euler tour forest to correspond with G[i].
G[log(n)] = full graph
Invariant 1 Every connected component of G_i has at most 2^i vertices.
Invariant 2 F[0] ⊆ F[1] ⊆ F[2] ⊆ ... ⊆ F[log(n)].
In other words:
F[i] = F[log(n)] ∩ G_i, and
F[log(n)] is the minimum spanning forest of G_{log(n)},
where the weight of an edge is its level.
F[0] is a maximum spanning forest if using 0 as top level
CommandLine:
python -m utool.experimental.dynamic_connectivity DynConnGraph --show
Example:
>>> # DISABLE_DOCTEST
>>> from utool.experimental.dynamic_connectivity import * # NOQA
>>> import networkx as nx
>>> import utool as ut
>>> import wbia.plottool as pt
>>> graph = nx.Graph([
>>> (0, 1), (0, 2), (0, 3), (1, 3), (2, 4), (3, 4), (2, 3),
>>> (5, 6), (5, 7), (5, 8), (6, 8), (7, 9), (8, 9), (7, 8),
>>> ])
>>> graph = nx.generators.cycle_graph(5)
>>> self = DynConnGraph(graph)
>>> pt.qtensure()
>>> pt.show_nx(self.graph, fnum=1)
>>> self.show_internals(fnum=2)
>>> self.remove_edge(1, 2)
>>> self.show_internals(fnum=3)
>>> self.remove_edge(3, 4)
>>> self.show_internals(fnum=4)
>>> ut.show_if_requested()
"""
[docs] def show_internals(self, fnum=None):
import wbia.plottool as pt
pt.qtensure()
pnum_ = pt.make_pnum_nextgen(nRows=1, nCols=len(self.forests))
for level, forest in enumerate(self.forests):
pt.show_nx(
forest.to_networkx(), title='level=%r' % (level,), fnum=fnum, pnum=pnum_()
)
def _init_forests():
"""
F[i] is a spanning forest of G[i].
F[i] is stored as an EulerTourTree
F[floor(log(n))] - used to support connectivity queries
F[0] has fewest edges
F[log(n)] has most edges
Invariant 2 F[0] ⊆ F[1] ⊆ F[2] ⊆ ... ⊆ F[log(n)].
In other words:
F[i] = F[log(n)] ∩ G_i, and
F[log(n)] is the minimum spanning forest of G_{log(n)},
where the weight of an edge is its level.
F[i] is a min. spanning forest w.r.t level, otherwise invariant 2 is not satisfied
"""
pass
def __init__(self, graph):
# List of forests at each level
# self.graph = graph
self.graph = nx.Graph()
self.level = {}
self.forests = [DummyEulerTourForest()]
for u, v in graph.edges():
self.add_edge(u, v)
# self.n = graph.number_of_nodes()
# stores the level of each edges
# First add all tree edges at level 0
# Then add non-tree edges at higher levels
# Store each forest as a nx.Graph?
# forests = []
# current_level = list(nx.connected_component_subgraphs(graph))
# while current_level:
# next_level = []
# for subgraph in current_level:
# mst = nx.minimum_spanning_tree(subgraph)
# # mst_tour = find_euler_tour(mst)
# forests.append(mst)
# residual = nx.difference(subgraph, mst)
# if residual.number_of_edges():
# next_level.append(residual)
# current_level = next_level
# print('current_level = %r' % (current_level,))
[docs] def add_edge(self, u, v):
"""
O(log(n))
"""
# print('add_edge u, v = %r, %r' % (u, v,))
if self.graph.has_edge(u, v):
return
for node in (u, v):
if not self.graph.has_node(node):
self.graph.add_node(node)
for Fi in self.forests:
Fi.add_node(node)
# First set the level of (u, v) to 0
self.level[(u, v)] = 0
# update the adjacency lists of u and v
self.graph.add_edge(u, v)
# If u and v are in separate trees in F_0, add e to F_0
ru = self.forests[0].find_root(u)
rv = self.forests[0].find_root(v)
if ru is not rv:
# If they are in different connected compoments merge compoments
self.forests[0].add_edge(u, v)
[docs] def remove_edge(self, u, v):
"""
Using notation where 0 is top level
Intuitively speaking, when the level of a nontree edge is increased, it
is because we have discovered that its end points are close enough in F
to fit in a smaller tree on a higher level.
"""
# Remove (u, v) from represented graph
print('Dynamically removing uv=(%r, %r)' % (u, v))
self.graph.remove_edge(u, v)
e = (u, v)
# Remove edge e = (u, v) from all graphs.
if not self.forests[0].has_edge(u, v):
# If (u, v) is a non-tree edge, simply delete it.
# Nothing else to do.
return
# If (u, v) is a tree edge we delete it and search for a replacement.
# Delete from all higher levels
for i in reversed(range(0, self.level[e] + 1)):
self.forests[i].remove_edge(u, v)
# Determine if another edge that connects u and v exists.
# (This must be an edge r, level[r] <= level[e])
# (Find max possible level[r] <= level[e])
for i in reversed(range(0, self.level[e] + 1)):
# Tu != Tw b/c (u, v) was just deleted from all forests
Tu = self.forests[i].subtree(u)
print('Tu = %r' % (list(Tu.nodes()),))
Tv = self.forests[i].subtree(v)
print('Tv = %r' % (list(Tv.nodes()),))
# Relabel so len(Tu) <= len(Tv)
# This ensures len(Tu) < 2 ** (floor(log(n)) - i)
if len(Tu) > len(Tv):
Tu, Tv = Tv, Tu
# Note len(Tu) <= 2 * (len(Tu) + len(Tv) + 1)
# We can afford to push all of Tu's edges to the next level and
# still preserve invariant 1.
seen_ = set([])
for x in Tu.nodes():
# Visit all edges INCIDENT (in real graph) to nodes in Tu.
# This lets us find non-tree edges to make a tree edge
seen_.add(x)
for y in self.graph.neighbors(x):
if y in seen_:
continue
# print('Check replacement edge xy=(%r, %r)' % (x, y))
if y in Tv:
print('* Found replacement xy=(%r, %r)' % (x, y))
# edge (x, y) is a replacement edge.
# add (x, y) to prev forests F[0:i+1]
# This is the only place edges are added to forets of
# higher levels.
if len(self.forests) == i + 1:
self.forests.append(DummyEulerTourForest(self.graph.nodes()))
for j in range(0, i + 2):
print('* Add replacment to F[j=%r]' % (j,))
# Need euler tree augmentation for outgoing level edges
self.forests[j].add_edge(x, y)
return
else:
print('* Charging xy=(%r, %r)' % (x, y))
# charge --- add (x, y) to next level
# this pays for our search in an amortized sense
# (ie, the next search at this level wont consider this)
if len(self.forests) == i + 1:
self.forests.append(DummyEulerTourForest(self.graph.nodes()))
if self.forests[i].has_edge(x, y):
self.forests[i + 1].add_edge(x, y)
# # assert False, 'we got it, should add it?'
self.level[(x, y)] = i + 1
[docs] def is_connected(self, u, v):
"""
Check if vertices u and v are connected.
Query top level forest F[0] to see if u and v are in the same tree.
This can be done by checking F_{log(n)} if Findroot(u) = Findroot(v).
This costs O(log(n) / log(log(n))) using B-tree based Euler-Tour trees.
but this trades off with a O(log^2(n)/log(log(n))) update
This is O(log(n)) otherwise
"""
ru = self.forests[0].find_root(u)
rv = self.forests[0].find_root(v)
return ru == rv
if __name__ == '__main__':
r"""
CommandLine:
python -m wbia.algo.hots.dynamic_connectivity
python -m wbia.algo.hots.dynamic_connectivity --allexamples
"""
import multiprocessing
multiprocessing.freeze_support() # for win32
import utool as ut # NOQA
ut.doctest_funcs()