dtree-encoding 1.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dtree/__init__.py
ADDED
dtree/dtree.py
ADDED
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
"""DTree: a single-rooted directed tree encoded by two linear projections.
|
|
2
|
+
|
|
3
|
+
Python implementation of the DTree design and API by Enis Olgac (original C#
|
|
4
|
+
prototype in releases up to 1.1.0). This port was written with Claude (Anthropic) and checked against a
|
|
5
|
+
brute-force reference in ``tests/``.
|
|
6
|
+
|
|
7
|
+
Projection model
|
|
8
|
+
----------------
|
|
9
|
+
Every tree is encoded by two orderings of its nodes, both bracketed by the
|
|
10
|
+
symbolic markers SOURCE ("[") and SINK ("]"):
|
|
11
|
+
|
|
12
|
+
* Q1: pre-order, children left to right (in insertion order).
|
|
13
|
+
* Q2: pre-order, children right to left (sibling-reversed).
|
|
14
|
+
|
|
15
|
+
Node u reaches node v if and only if u strictly precedes v in both Q1 and Q2.
|
|
16
|
+
Reachability is irreflexive: no node reaches itself.
|
|
17
|
+
|
|
18
|
+
Update rules (taken from the C# original)
|
|
19
|
+
----------------------------------------
|
|
20
|
+
* insert(u, v): v is placed immediately after u in Q2, and in Q1 immediately
|
|
21
|
+
before the first node after u that u cannot reach (the end of u's subtree).
|
|
22
|
+
Insertion costs O(depth of the subtree below u); positions of other nodes
|
|
23
|
+
normally stay unchanged.
|
|
24
|
+
* delete(v): v is removed from both projections; its children move up to v's
|
|
25
|
+
parent and keep their relative order (a cut-and-paste of v's children).
|
|
26
|
+
* delete_subtree(v): the contiguous block of v's subtree is removed from both
|
|
27
|
+
projections.
|
|
28
|
+
* cut_n_paste(u, p): the contiguous block of u's subtree is lifted out of both
|
|
29
|
+
projections and re-inserted under p using the same rule as insert.
|
|
30
|
+
|
|
31
|
+
Positions are fractions. A new node gets the mediant (a+c)/(b+d) of its two
|
|
32
|
+
neighbors a/b and c/d (the Stern-Brocot construction), which always lies
|
|
33
|
+
strictly between them, so normally no other node moves. Rational positions
|
|
34
|
+
were Enis Olgac's idea; repeated insertion into the same gap (a node with many
|
|
35
|
+
children, appending at either end) then keeps positions at O(log n) bits. Only
|
|
36
|
+
when a denominator would exceed 64 bits, which needs alternating insertions,
|
|
37
|
+
is a small surrounding region renumbered with simple fractions k/2**32.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
from fractions import Fraction
|
|
43
|
+
from typing import Dict, Iterable, Iterator, List, Optional
|
|
44
|
+
|
|
45
|
+
SOURCE = "["
|
|
46
|
+
SINK = "]"
|
|
47
|
+
|
|
48
|
+
_Q = 1 << 32 # denominator used when a crowded region is renumbered
|
|
49
|
+
_LIMIT = 64 # renumber when a denominator would exceed this many bits
|
|
50
|
+
_SLACK = 16 # after renumbering, neighbors are at least this many 1/_Q apart
|
|
51
|
+
_GROWTH = 2 # each doubling of the region raises the required spacing by this factor
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class _Cell:
|
|
55
|
+
__slots__ = ("name", "prev", "next", "num", "den")
|
|
56
|
+
|
|
57
|
+
def __init__(self, name: str) -> None:
|
|
58
|
+
self.name = name
|
|
59
|
+
self.prev: Optional[_Cell] = None
|
|
60
|
+
self.next: Optional[_Cell] = None
|
|
61
|
+
self.num = 0
|
|
62
|
+
self.den = 1
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DList:
|
|
66
|
+
"""Doubly linked list with rational positions (O(1) precedence test).
|
|
67
|
+
|
|
68
|
+
Positions are fractions num/den. A new node gets the mediant
|
|
69
|
+
(a+c)/(b+d) of its neighbors a/b and c/d, which always lies strictly
|
|
70
|
+
between them; SOURCE and SINK act as -1/0 and +1/0. Nothing else moves.
|
|
71
|
+
Only when a denominator would exceed _LIMIT bits is a small surrounding
|
|
72
|
+
region renumbered with simple fractions k/_Q.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, name: str) -> None:
|
|
76
|
+
self.name = name
|
|
77
|
+
self._cells: Dict[str, _Cell] = {}
|
|
78
|
+
self.first = self._new(SOURCE)
|
|
79
|
+
self.last = self._new(SINK)
|
|
80
|
+
self.first.next = self.last
|
|
81
|
+
self.last.prev = self.first
|
|
82
|
+
self.first.num, self.first.den = -1, 0 # -infinity
|
|
83
|
+
self.last.num, self.last.den = 1, 0 # +infinity
|
|
84
|
+
# Statistics: how often positions were renumbered and how many moved.
|
|
85
|
+
self.renumber_count = 0
|
|
86
|
+
self.renumber_moved = 0
|
|
87
|
+
|
|
88
|
+
# -- internals --------------------------------------------------------
|
|
89
|
+
def _new(self, name: str) -> _Cell:
|
|
90
|
+
cell = _Cell(name)
|
|
91
|
+
self._cells[name] = cell
|
|
92
|
+
return cell
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def _lt(x: _Cell, y: _Cell) -> bool:
|
|
96
|
+
"""x lies strictly left of y, by comparing x.num/x.den with y.num/y.den."""
|
|
97
|
+
if x.den and y.den:
|
|
98
|
+
return x.num * y.den < y.num * x.den
|
|
99
|
+
if x.den == 0 and y.den == 0:
|
|
100
|
+
return x.num < y.num
|
|
101
|
+
return (x.num if x.den == 0 else 0) < (y.num if y.den == 0 else 0)
|
|
102
|
+
|
|
103
|
+
def _link_after(self, cell: _Cell, prev: _Cell) -> None:
|
|
104
|
+
nxt = prev.next
|
|
105
|
+
cell.prev, cell.next = prev, nxt
|
|
106
|
+
prev.next = cell
|
|
107
|
+
nxt.prev = cell
|
|
108
|
+
if prev is self.first and nxt is self.last:
|
|
109
|
+
cell.num, cell.den = 0, 1 # first node of an empty list
|
|
110
|
+
return
|
|
111
|
+
# Mediant of the neighbors. Next to SOURCE (-1/0) or SINK (+1/0) this
|
|
112
|
+
# steps by one whole unit, so a list can grow at either end forever
|
|
113
|
+
# without renumbering (positives to the right, negatives to the left).
|
|
114
|
+
cell.num = prev.num + nxt.num
|
|
115
|
+
cell.den = prev.den + nxt.den
|
|
116
|
+
if cell.den.bit_length() > _LIMIT:
|
|
117
|
+
self._rebalance(cell)
|
|
118
|
+
|
|
119
|
+
def _rebalance(self, cell: _Cell) -> None:
|
|
120
|
+
"""Renumber the smallest region around ``cell`` that has room, using fractions k/_Q."""
|
|
121
|
+
q = _Q
|
|
122
|
+
lo = hi = cell
|
|
123
|
+
m, k, need = 1, 1, _SLACK
|
|
124
|
+
while True:
|
|
125
|
+
for _ in range(k):
|
|
126
|
+
if lo.prev is not self.first:
|
|
127
|
+
lo = lo.prev
|
|
128
|
+
m += 1
|
|
129
|
+
if hi.next is not self.last:
|
|
130
|
+
hi = hi.next
|
|
131
|
+
m += 1
|
|
132
|
+
left, right = lo.prev, hi.next
|
|
133
|
+
if left is self.first and right is self.last:
|
|
134
|
+
ks = [(j - m // 2) * q for j in range(m)]
|
|
135
|
+
break
|
|
136
|
+
if left is self.first:
|
|
137
|
+
top = -((-right.num * q) // right.den) # ceil(right * q)
|
|
138
|
+
ks = [top - (m - j) * q for j in range(m)]
|
|
139
|
+
break
|
|
140
|
+
if right is self.last:
|
|
141
|
+
bottom = (left.num * q) // left.den # floor(left * q)
|
|
142
|
+
ks = [bottom + (j + 1) * q for j in range(m)]
|
|
143
|
+
break
|
|
144
|
+
bottom = (left.num * q) // left.den
|
|
145
|
+
top = -((-right.num * q) // right.den)
|
|
146
|
+
room = top - bottom - 1 # integers k with left < k/q < right
|
|
147
|
+
if room >= m * need:
|
|
148
|
+
ks = [bottom + ((j + 1) * (room + 1)) // (m + 1) for j in range(m)]
|
|
149
|
+
break
|
|
150
|
+
# Larger regions must be sparser before they are accepted (as in
|
|
151
|
+
# Bender et al.), so a renumbered region keeps spare room and the
|
|
152
|
+
# next overflow nearby can again be handled locally.
|
|
153
|
+
k *= 2
|
|
154
|
+
need *= _GROWTH
|
|
155
|
+
c = lo
|
|
156
|
+
for kv in ks:
|
|
157
|
+
c.num, c.den = kv, q
|
|
158
|
+
c = c.next
|
|
159
|
+
self.renumber_count += 1
|
|
160
|
+
self.renumber_moved += m
|
|
161
|
+
|
|
162
|
+
def _unlink(self, cell: _Cell) -> None:
|
|
163
|
+
cell.prev.next = cell.next
|
|
164
|
+
cell.next.prev = cell.prev
|
|
165
|
+
cell.prev = cell.next = None
|
|
166
|
+
|
|
167
|
+
# -- public -----------------------------------------------------------
|
|
168
|
+
def __contains__(self, name: str) -> bool:
|
|
169
|
+
return name in self._cells
|
|
170
|
+
|
|
171
|
+
# Names follow List.cs: Source/Sink, First/Last, Prev/Next, Index,
|
|
172
|
+
# IsAllocated, IsLeftOf ... IsSelfOrRightOf, InsAfter/InsBefore, IsSameAs.
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def source(self) -> str:
|
|
176
|
+
return self.first.name
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def sink(self) -> str:
|
|
180
|
+
return self.last.name
|
|
181
|
+
|
|
182
|
+
def index(self, name: str):
|
|
183
|
+
"""Position of ``name`` as a Fraction (SOURCE/SINK: -inf/+inf), or None if absent (List.cs: Index)."""
|
|
184
|
+
cell = self._cells.get(name)
|
|
185
|
+
if cell is None:
|
|
186
|
+
return None
|
|
187
|
+
if cell.den == 0:
|
|
188
|
+
return float("inf") if cell.num > 0 else float("-inf")
|
|
189
|
+
return Fraction(cell.num, cell.den)
|
|
190
|
+
|
|
191
|
+
label = index
|
|
192
|
+
|
|
193
|
+
def is_allocated(self, name: str) -> bool:
|
|
194
|
+
return name in self._cells
|
|
195
|
+
|
|
196
|
+
def is_not_allocated(self, name: str) -> bool:
|
|
197
|
+
return name not in self._cells
|
|
198
|
+
|
|
199
|
+
def next(self, name: str) -> Optional[str]:
|
|
200
|
+
nxt = self._cells[name].next
|
|
201
|
+
return nxt.name if nxt else None
|
|
202
|
+
|
|
203
|
+
def prev(self, name: str) -> Optional[str]:
|
|
204
|
+
prv = self._cells[name].prev
|
|
205
|
+
return prv.name if prv else None
|
|
206
|
+
|
|
207
|
+
def is_left_of(self, v: str, u: str) -> bool:
|
|
208
|
+
return self._lt(self._cells[v], self._cells[u])
|
|
209
|
+
|
|
210
|
+
def is_self_or_left_of(self, v: str, u: str) -> bool:
|
|
211
|
+
return v == u or self._lt(self._cells[v], self._cells[u])
|
|
212
|
+
|
|
213
|
+
def is_right_of(self, v: str, u: str) -> bool:
|
|
214
|
+
return self._lt(self._cells[u], self._cells[v])
|
|
215
|
+
|
|
216
|
+
def is_self_or_right_of(self, v: str, u: str) -> bool:
|
|
217
|
+
return v == u or self._lt(self._cells[u], self._cells[v])
|
|
218
|
+
|
|
219
|
+
def precedes(self, u: str, v: str) -> bool:
|
|
220
|
+
"""Strict order: u lies left of v (same as is_left_of)."""
|
|
221
|
+
x, y = self._cells[u], self._cells[v]
|
|
222
|
+
if x.den and y.den:
|
|
223
|
+
return x.num * y.den < y.num * x.den
|
|
224
|
+
return self._lt(x, y)
|
|
225
|
+
|
|
226
|
+
def _cell_for(self, name: str) -> _Cell:
|
|
227
|
+
"""Existing cell (unlinked first, so it is moved) or a new one."""
|
|
228
|
+
cell = self._cells.get(name)
|
|
229
|
+
if cell is None:
|
|
230
|
+
return self._new(name)
|
|
231
|
+
self._unlink(cell)
|
|
232
|
+
return cell
|
|
233
|
+
|
|
234
|
+
def insert_after(self, name: str, anchor: str) -> None:
|
|
235
|
+
"""Place ``name`` directly after ``anchor``; moves it if already present (List.cs: InsAfter)."""
|
|
236
|
+
if name == anchor or name == SINK:
|
|
237
|
+
return
|
|
238
|
+
if anchor == SINK:
|
|
239
|
+
return self.insert_before(name, SINK)
|
|
240
|
+
a = self._cells[anchor]
|
|
241
|
+
if a.next is not None and a.next.name == name:
|
|
242
|
+
return
|
|
243
|
+
cell = self._cell_for(name)
|
|
244
|
+
self._link_after(cell, self._cells[anchor])
|
|
245
|
+
|
|
246
|
+
def insert_before(self, name: str, anchor: str) -> None:
|
|
247
|
+
"""Place ``name`` directly before ``anchor``; moves it if already present (List.cs: InsBefore)."""
|
|
248
|
+
if name == anchor or name == SOURCE:
|
|
249
|
+
return
|
|
250
|
+
if anchor == SOURCE:
|
|
251
|
+
return self.insert_after(name, SOURCE)
|
|
252
|
+
a = self._cells[anchor]
|
|
253
|
+
if a.prev is not None and a.prev.name == name:
|
|
254
|
+
return
|
|
255
|
+
cell = self._cell_for(name)
|
|
256
|
+
self._link_after(cell, self._cells[anchor].prev)
|
|
257
|
+
|
|
258
|
+
def is_same_as(self, other: "DList") -> bool:
|
|
259
|
+
"""True if both lists hold the same nodes in the same order (List.cs: IsSameAs)."""
|
|
260
|
+
return list(self) == list(other)
|
|
261
|
+
|
|
262
|
+
def remove(self, name: str) -> None:
|
|
263
|
+
self._unlink(self._cells.pop(name))
|
|
264
|
+
|
|
265
|
+
def cut_block(self, first: str, last: str) -> List[str]:
|
|
266
|
+
"""Remove the contiguous block first..last and return its names in order."""
|
|
267
|
+
a, b = self._cells[first], self._cells[last]
|
|
268
|
+
names, cell = [], a
|
|
269
|
+
while True:
|
|
270
|
+
names.append(cell.name)
|
|
271
|
+
if cell is b:
|
|
272
|
+
break
|
|
273
|
+
cell = cell.next
|
|
274
|
+
a.prev.next = b.next
|
|
275
|
+
b.next.prev = a.prev
|
|
276
|
+
for n in names:
|
|
277
|
+
self._cells.pop(n)
|
|
278
|
+
return names
|
|
279
|
+
|
|
280
|
+
def paste_block_after(self, names: List[str], anchor: str) -> None:
|
|
281
|
+
for n in names:
|
|
282
|
+
self.insert_after(n, anchor)
|
|
283
|
+
anchor = n
|
|
284
|
+
|
|
285
|
+
def __iter__(self) -> Iterator[str]:
|
|
286
|
+
cell = self.first.next
|
|
287
|
+
while cell is not self.last:
|
|
288
|
+
yield cell.name
|
|
289
|
+
cell = cell.next
|
|
290
|
+
|
|
291
|
+
def to_list(self, with_markers: bool = False) -> List[str]:
|
|
292
|
+
body = list(self)
|
|
293
|
+
return [SOURCE, *body, SINK] if with_markers else body
|
|
294
|
+
|
|
295
|
+
def __repr__(self) -> str:
|
|
296
|
+
return f"{self.name}: [{', '.join(self)}]"
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class DTree:
|
|
300
|
+
"""Single-rooted directed tree with dual projections Q1 and Q2."""
|
|
301
|
+
|
|
302
|
+
def __init__(self, tree_id: str = "P1") -> None:
|
|
303
|
+
self.tree_id = tree_id
|
|
304
|
+
self._parent: Dict[str, str] = {} # child -> parent (root -> SOURCE)
|
|
305
|
+
self._children: Dict[str, List[str]] = {} # parent -> children, insertion order
|
|
306
|
+
self._q1 = DList(f"{tree_id}.Q1")
|
|
307
|
+
self._q2 = DList(f"{tree_id}.Q2")
|
|
308
|
+
self.edge_log: List[str] = []
|
|
309
|
+
self._reshapes = 0 # counts deletions and moves; insertions keep the order of existing nodes
|
|
310
|
+
|
|
311
|
+
# -- construction ------------------------------------------------------
|
|
312
|
+
@classmethod
|
|
313
|
+
def from_file(cls, path: str, tree_id: Optional[str] = None) -> "DTree":
|
|
314
|
+
"""Read the ``parent: child1; child2`` format used by the .csx examples."""
|
|
315
|
+
import os
|
|
316
|
+
|
|
317
|
+
tree = cls(tree_id or os.path.splitext(os.path.basename(path))[0])
|
|
318
|
+
with open(path, encoding="utf-8-sig") as fh:
|
|
319
|
+
for line in fh:
|
|
320
|
+
line = line.strip()
|
|
321
|
+
if not line or line.startswith(("#", "--", "**")):
|
|
322
|
+
continue
|
|
323
|
+
if line.startswith("@@"):
|
|
324
|
+
break
|
|
325
|
+
parent, _, rest = line.partition(":")
|
|
326
|
+
for child in (c.strip() for c in rest.split(";")):
|
|
327
|
+
if child:
|
|
328
|
+
tree.insert(parent.strip(), child)
|
|
329
|
+
return tree
|
|
330
|
+
|
|
331
|
+
def insert(self, parent: str, child: str) -> None:
|
|
332
|
+
"""Add the edge parent -> child.
|
|
333
|
+
|
|
334
|
+
The first call makes ``parent`` the root. Afterwards ``parent`` must
|
|
335
|
+
already exist and ``child`` must not.
|
|
336
|
+
"""
|
|
337
|
+
if parent in (SOURCE, SINK) or child in (SOURCE, SINK):
|
|
338
|
+
raise ValueError("SOURCE/SINK markers cannot be tree nodes")
|
|
339
|
+
if not self._parent:
|
|
340
|
+
self._attach(SOURCE, parent)
|
|
341
|
+
elif parent not in self._parent:
|
|
342
|
+
raise KeyError(f"parent {parent!r} is not in the tree")
|
|
343
|
+
if child in self._parent:
|
|
344
|
+
raise ValueError(f"child {child!r} is already in the tree")
|
|
345
|
+
self._attach(parent, child)
|
|
346
|
+
self.edge_log.append(f"({parent}, {child})")
|
|
347
|
+
|
|
348
|
+
def _attach(self, parent: str, child: str) -> None:
|
|
349
|
+
end = self._subtree_end_q1(parent)
|
|
350
|
+
self._q2.insert_after(child, parent)
|
|
351
|
+
self._q1.insert_before(child, end)
|
|
352
|
+
self._parent[child] = parent
|
|
353
|
+
self._children.setdefault(parent, []).append(child)
|
|
354
|
+
|
|
355
|
+
def _subtree_end_q1(self, u: str) -> str:
|
|
356
|
+
"""First node after u in Q1 that u cannot reach (may be SINK)."""
|
|
357
|
+
return self._q1.next(self._subtree_last(u, self._q1))
|
|
358
|
+
|
|
359
|
+
def _subtree_last(self, u: str, q: DList) -> str:
|
|
360
|
+
"""Last node of u's subtree in projection q, found in O(depth).
|
|
361
|
+
|
|
362
|
+
Q1 lists children in insertion order, so its last node is reached by
|
|
363
|
+
following last children; Q2 reverses siblings, so by following first
|
|
364
|
+
children.
|
|
365
|
+
"""
|
|
366
|
+
pick = -1 if q is self._q1 else 0
|
|
367
|
+
w = u
|
|
368
|
+
while w in self._children:
|
|
369
|
+
w = self._children[w][pick]
|
|
370
|
+
return w
|
|
371
|
+
|
|
372
|
+
# -- structural reconfiguration ---------------------------------------
|
|
373
|
+
def delete(self, node: str) -> None:
|
|
374
|
+
"""Remove ``node``; its children are re-attached to its parent.
|
|
375
|
+
|
|
376
|
+
Deleting the root is disallowed.
|
|
377
|
+
"""
|
|
378
|
+
self._require(node)
|
|
379
|
+
if self.is_root(node):
|
|
380
|
+
raise ValueError("the root cannot be deleted")
|
|
381
|
+
parent = self._parent.pop(node)
|
|
382
|
+
kids = self._children.pop(node, [])
|
|
383
|
+
siblings = self._children[parent]
|
|
384
|
+
i = siblings.index(node)
|
|
385
|
+
siblings[i : i + 1] = kids
|
|
386
|
+
for k in kids:
|
|
387
|
+
self._parent[k] = parent
|
|
388
|
+
if not siblings:
|
|
389
|
+
del self._children[parent]
|
|
390
|
+
self._q1.remove(node)
|
|
391
|
+
self._q2.remove(node)
|
|
392
|
+
self._reshapes += 1
|
|
393
|
+
|
|
394
|
+
def delete_subtree(self, node: str) -> List[str]:
|
|
395
|
+
"""Remove ``node`` together with all its descendants.
|
|
396
|
+
|
|
397
|
+
Returns the removed nodes in Q1 order. Deleting the root's subtree
|
|
398
|
+
(the whole tree) is disallowed, as for delete.
|
|
399
|
+
"""
|
|
400
|
+
self._require(node)
|
|
401
|
+
if self.is_root(node):
|
|
402
|
+
raise ValueError("the root cannot be deleted")
|
|
403
|
+
last1 = self._subtree_last(node, self._q1)
|
|
404
|
+
last2 = self._subtree_last(node, self._q2)
|
|
405
|
+
removed = self._q1.cut_block(node, last1)
|
|
406
|
+
self._q2.cut_block(node, last2)
|
|
407
|
+
parent = self._parent[node]
|
|
408
|
+
self._children[parent].remove(node)
|
|
409
|
+
if not self._children[parent]:
|
|
410
|
+
del self._children[parent]
|
|
411
|
+
for v in removed:
|
|
412
|
+
del self._parent[v]
|
|
413
|
+
self._children.pop(v, None)
|
|
414
|
+
self._reshapes += 1
|
|
415
|
+
return removed
|
|
416
|
+
|
|
417
|
+
def cut_n_paste(self, subtree_root: str, new_parent: str) -> None:
|
|
418
|
+
"""Detach the subtree rooted at ``subtree_root`` and attach it under ``new_parent``.
|
|
419
|
+
|
|
420
|
+
The subtree keeps its internal shape and becomes the last child of
|
|
421
|
+
``new_parent``. ``new_parent`` must not lie inside the moved subtree,
|
|
422
|
+
and the root cannot be moved.
|
|
423
|
+
"""
|
|
424
|
+
self._require(subtree_root)
|
|
425
|
+
self._require(new_parent)
|
|
426
|
+
if self.is_root(subtree_root):
|
|
427
|
+
raise ValueError("the root cannot be moved")
|
|
428
|
+
if new_parent == subtree_root or self.can_reach(subtree_root, new_parent):
|
|
429
|
+
raise ValueError("new_parent lies inside the subtree being moved")
|
|
430
|
+
u = subtree_root
|
|
431
|
+
last1 = self._subtree_last(u, self._q1)
|
|
432
|
+
last2 = self._subtree_last(u, self._q2)
|
|
433
|
+
block1 = self._q1.cut_block(u, last1)
|
|
434
|
+
block2 = self._q2.cut_block(u, last2)
|
|
435
|
+
old = self._parent[u]
|
|
436
|
+
self._children[old].remove(u)
|
|
437
|
+
if not self._children[old]:
|
|
438
|
+
del self._children[old]
|
|
439
|
+
# Same placement rule as insert: Q2 right after the parent,
|
|
440
|
+
# Q1 right after the current last node of the parent's subtree.
|
|
441
|
+
last = self._subtree_last(new_parent, self._q1)
|
|
442
|
+
self._q2.paste_block_after(block2, new_parent)
|
|
443
|
+
self._q1.paste_block_after(block1, last)
|
|
444
|
+
self._parent[u] = new_parent
|
|
445
|
+
self._reshapes += 1
|
|
446
|
+
self._children.setdefault(new_parent, []).append(u)
|
|
447
|
+
|
|
448
|
+
# -- accessors --------------------------------------------------------
|
|
449
|
+
@property
|
|
450
|
+
def root(self) -> Optional[str]:
|
|
451
|
+
kids = self._children.get(SOURCE)
|
|
452
|
+
return kids[0] if kids else None
|
|
453
|
+
|
|
454
|
+
@property
|
|
455
|
+
def source(self) -> str:
|
|
456
|
+
return SOURCE
|
|
457
|
+
|
|
458
|
+
@property
|
|
459
|
+
def sink(self) -> str:
|
|
460
|
+
return SINK
|
|
461
|
+
|
|
462
|
+
@property
|
|
463
|
+
def Q1(self) -> DList:
|
|
464
|
+
"""The Q1 projection itself, for list-level checks (is_left_of, index, ...).
|
|
465
|
+
|
|
466
|
+
Live view: read it, but change the tree only through DTree methods.
|
|
467
|
+
"""
|
|
468
|
+
return self._q1
|
|
469
|
+
|
|
470
|
+
@property
|
|
471
|
+
def Q2(self) -> DList:
|
|
472
|
+
"""The Q2 projection itself, for list-level checks. Live view, as Q1."""
|
|
473
|
+
return self._q2
|
|
474
|
+
|
|
475
|
+
@property
|
|
476
|
+
def q1(self) -> List[str]:
|
|
477
|
+
"""Q1 as a plain list of node names (a copy, without the markers)."""
|
|
478
|
+
return self._q1.to_list()
|
|
479
|
+
|
|
480
|
+
@property
|
|
481
|
+
def q2(self) -> List[str]:
|
|
482
|
+
return self._q2.to_list()
|
|
483
|
+
|
|
484
|
+
@property
|
|
485
|
+
def size(self) -> int:
|
|
486
|
+
return len(self._parent)
|
|
487
|
+
|
|
488
|
+
@property
|
|
489
|
+
def nodes(self) -> List[str]:
|
|
490
|
+
return self.q1
|
|
491
|
+
|
|
492
|
+
def __len__(self) -> int:
|
|
493
|
+
return self.size
|
|
494
|
+
|
|
495
|
+
def __contains__(self, node: str) -> bool:
|
|
496
|
+
return self.contains(node)
|
|
497
|
+
|
|
498
|
+
def __repr__(self) -> str:
|
|
499
|
+
return f"DTree({self.tree_id!r}, size={self.size})\n {self._q1!r}\n {self._q2!r}"
|
|
500
|
+
|
|
501
|
+
# -- queries ----------------------------------------------------------
|
|
502
|
+
def contains(self, node: str) -> bool:
|
|
503
|
+
return node in self._parent
|
|
504
|
+
|
|
505
|
+
def get_parent(self, node: str) -> Optional[str]:
|
|
506
|
+
"""Parent of ``node``; ``None`` for the root. Raises KeyError if absent."""
|
|
507
|
+
self._require(node)
|
|
508
|
+
p = self._parent[node]
|
|
509
|
+
return None if p == SOURCE else p
|
|
510
|
+
|
|
511
|
+
def children(self, node: str) -> List[str]:
|
|
512
|
+
self._require(node)
|
|
513
|
+
return list(self._children.get(node, []))
|
|
514
|
+
|
|
515
|
+
def can_reach(self, u: str, v: str) -> bool:
|
|
516
|
+
"""True if there is a directed path of length >= 1 from u to v.
|
|
517
|
+
|
|
518
|
+
Irreflexive: can_reach(u, u) is False.
|
|
519
|
+
"""
|
|
520
|
+
if u not in self._q1 or v not in self._q1:
|
|
521
|
+
return False
|
|
522
|
+
return self._q1.precedes(u, v) and self._q2.precedes(u, v)
|
|
523
|
+
|
|
524
|
+
def can_not_reach(self, u: str, v: str) -> bool:
|
|
525
|
+
return not self.can_reach(u, v)
|
|
526
|
+
|
|
527
|
+
def is_root(self, node: str) -> bool:
|
|
528
|
+
return node == self.root
|
|
529
|
+
|
|
530
|
+
def is_leaf(self, node: str) -> bool:
|
|
531
|
+
self._require(node)
|
|
532
|
+
return node not in self._children
|
|
533
|
+
|
|
534
|
+
def is_ancestor(self, u: str, v: str) -> bool:
|
|
535
|
+
"""True if u is a proper ancestor of v (u lies above v on v's root path)."""
|
|
536
|
+
return self.contains(u) and self.contains(v) and self.can_reach(u, v)
|
|
537
|
+
|
|
538
|
+
def is_descendant(self, u: str, v: str) -> bool:
|
|
539
|
+
"""True if u lies below v. Equivalent to is_ancestor(v, u)."""
|
|
540
|
+
return self.is_ancestor(v, u)
|
|
541
|
+
|
|
542
|
+
def same_subtree(self, u: str, v: str) -> bool:
|
|
543
|
+
"""True if both nodes are in this tree (and so share its root)."""
|
|
544
|
+
return self.contains(u) and self.contains(v)
|
|
545
|
+
|
|
546
|
+
def subtree(self, node: str) -> List[str]:
|
|
547
|
+
"""Nodes of the subtree rooted at ``node``, in Q1 order."""
|
|
548
|
+
self._require(node)
|
|
549
|
+
out, v = [node], self._q1.next(node)
|
|
550
|
+
while v != SINK and self.can_reach(node, v):
|
|
551
|
+
out.append(v)
|
|
552
|
+
v = self._q1.next(v)
|
|
553
|
+
return out
|
|
554
|
+
|
|
555
|
+
def subtree_size(self, node: str) -> int:
|
|
556
|
+
return len(self.subtree(node))
|
|
557
|
+
|
|
558
|
+
def path_to_root(self, node: str) -> List[str]:
|
|
559
|
+
"""[node, parent, ..., root]."""
|
|
560
|
+
self._require(node)
|
|
561
|
+
out, v = [], node
|
|
562
|
+
while v != SOURCE:
|
|
563
|
+
out.append(v)
|
|
564
|
+
v = self._parent[v]
|
|
565
|
+
return out
|
|
566
|
+
|
|
567
|
+
def root_to_path(self, node: str) -> List[str]:
|
|
568
|
+
"""[root, ..., parent, node]."""
|
|
569
|
+
return self.path_to_root(node)[::-1]
|
|
570
|
+
|
|
571
|
+
def path(self, u: str, v: str) -> List[str]:
|
|
572
|
+
"""Nodes from u down to v, both included; [u] when u == v.
|
|
573
|
+
|
|
574
|
+
Raises if u != v and u cannot reach v.
|
|
575
|
+
"""
|
|
576
|
+
if u != v and not self.can_reach(u, v):
|
|
577
|
+
raise ValueError(f"{u!r} cannot reach {v!r}")
|
|
578
|
+
out, w = [v], v
|
|
579
|
+
while w != u:
|
|
580
|
+
w = self._parent[w]
|
|
581
|
+
out.append(w)
|
|
582
|
+
return out[::-1]
|
|
583
|
+
|
|
584
|
+
def depth(self, node: str) -> int:
|
|
585
|
+
"""Distance from the root; the root has depth 0."""
|
|
586
|
+
return len(self.path_to_root(node)) - 1
|
|
587
|
+
|
|
588
|
+
# -- helpers ----------------------------------------------------------
|
|
589
|
+
# -- set queries ------------------------------------------------------
|
|
590
|
+
def vmin(self, nodes: Iterable[str] = ()) -> "VMin":
|
|
591
|
+
"""Constructed node VMin: the minimum of each index over the nodes (see VMin)."""
|
|
592
|
+
return VMin(self, nodes)
|
|
593
|
+
|
|
594
|
+
def vmax(self, nodes: Iterable[str] = ()) -> "VMax":
|
|
595
|
+
"""Constructed node VMax: the maximum of each index over the nodes (see VMax)."""
|
|
596
|
+
return VMax(self, nodes)
|
|
597
|
+
|
|
598
|
+
def umin(self, nodes: Iterable[str] = ()) -> "UMin":
|
|
599
|
+
"""Constructed node UMin: the minimum of each index over the nodes (see UMin)."""
|
|
600
|
+
return UMin(self, nodes)
|
|
601
|
+
|
|
602
|
+
def umax(self, nodes: Iterable[str] = ()) -> "UMax":
|
|
603
|
+
"""Constructed node UMax: the maximum of each index over the nodes (see UMax)."""
|
|
604
|
+
return UMax(self, nodes)
|
|
605
|
+
|
|
606
|
+
def _require(self, node: str) -> None:
|
|
607
|
+
if node not in self._parent:
|
|
608
|
+
raise KeyError(f"node {node!r} is not in the tree")
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
class _VNode:
|
|
612
|
+
"""A constructed node for a set S of tree nodes (E. Olgac, "Topology and Structure of
|
|
613
|
+
Directed Graphs - Inducing a Subgraph", 2021): the minimum (VMin, UMin) or maximum
|
|
614
|
+
(VMax, UMax) of each index over S. V means S takes the target role, can_reach(u, S),
|
|
615
|
+
and U the source role, can_reach(S, v); UMin and VMin hold the same numbers.
|
|
616
|
+
|
|
617
|
+
It stores no positions, only the members and, for each of Q1 and Q2, the member holding
|
|
618
|
+
the minimal (or maximal) index there; the two may be different nodes, so the constructed
|
|
619
|
+
node usually corresponds to no real node. Insertions and renumbering keep the order of
|
|
620
|
+
existing nodes, so it stays valid while the tree grows. After a deletion or a cut_n_paste it is
|
|
621
|
+
recomputed from the remaining members on its next use.
|
|
622
|
+
"""
|
|
623
|
+
|
|
624
|
+
_lowest = True # VMin keeps the leftmost member, VMax the rightmost
|
|
625
|
+
|
|
626
|
+
def __init__(self, tree: "DTree", nodes: Iterable[str] = ()) -> None:
|
|
627
|
+
self._t = tree
|
|
628
|
+
self._members: List[str] = [] # in Q1 order
|
|
629
|
+
self._seen: set = set()
|
|
630
|
+
self._c1: Optional[str] = None
|
|
631
|
+
self._c2: Optional[str] = None
|
|
632
|
+
self._reshapes = tree._reshapes
|
|
633
|
+
for v in nodes:
|
|
634
|
+
self.add(v)
|
|
635
|
+
|
|
636
|
+
def _better(self, q: DList, v: str, c: Optional[str]) -> bool:
|
|
637
|
+
if c is None:
|
|
638
|
+
return True
|
|
639
|
+
return q.precedes(v, c) if self._lowest else q.precedes(c, v)
|
|
640
|
+
|
|
641
|
+
def add(self, node: str) -> "_VNode":
|
|
642
|
+
"""Add a node of the tree to the set; returns the constructed node itself."""
|
|
643
|
+
self._t._require(node)
|
|
644
|
+
if node in self._seen:
|
|
645
|
+
return self
|
|
646
|
+
self._check()
|
|
647
|
+
self._seen.add(node)
|
|
648
|
+
q1 = self._t._q1
|
|
649
|
+
lo, hi = 0, len(self._members) # keep members sorted by Q1 order
|
|
650
|
+
while lo < hi:
|
|
651
|
+
mid = (lo + hi) // 2
|
|
652
|
+
if q1.precedes(node, self._members[mid]):
|
|
653
|
+
hi = mid
|
|
654
|
+
else:
|
|
655
|
+
lo = mid + 1
|
|
656
|
+
self._members.insert(lo, node)
|
|
657
|
+
if self._better(self._t._q1, node, self._c1):
|
|
658
|
+
self._c1 = node
|
|
659
|
+
if self._better(self._t._q2, node, self._c2):
|
|
660
|
+
self._c2 = node
|
|
661
|
+
self._added(node)
|
|
662
|
+
return self
|
|
663
|
+
|
|
664
|
+
def _added(self, node: str) -> None:
|
|
665
|
+
"""Hook for subclasses that keep more than the index nodes."""
|
|
666
|
+
|
|
667
|
+
def _rebuilt(self) -> None:
|
|
668
|
+
"""Hook for subclasses that keep more than the index nodes."""
|
|
669
|
+
|
|
670
|
+
def refresh(self) -> None:
|
|
671
|
+
"""Drop deleted members and recompute the index nodes."""
|
|
672
|
+
t = self._t
|
|
673
|
+
q1 = t._q1
|
|
674
|
+
self._members = sorted((v for v in self._members if t.contains(v)),
|
|
675
|
+
key=lambda v: q1.index(v))
|
|
676
|
+
self._seen = set(self._members)
|
|
677
|
+
self._c1 = self._c2 = None
|
|
678
|
+
for v in self._members:
|
|
679
|
+
if self._better(t._q1, v, self._c1):
|
|
680
|
+
self._c1 = v
|
|
681
|
+
if self._better(t._q2, v, self._c2):
|
|
682
|
+
self._c2 = v
|
|
683
|
+
self._reshapes = t._reshapes
|
|
684
|
+
self._rebuilt()
|
|
685
|
+
|
|
686
|
+
def _check(self) -> None:
|
|
687
|
+
t = self._t
|
|
688
|
+
if self._reshapes != t._reshapes:
|
|
689
|
+
self.refresh()
|
|
690
|
+
|
|
691
|
+
@property
|
|
692
|
+
def members(self) -> List[str]:
|
|
693
|
+
self._check()
|
|
694
|
+
return list(self._members)
|
|
695
|
+
|
|
696
|
+
@property
|
|
697
|
+
def index_nodes(self) -> tuple:
|
|
698
|
+
"""The members holding the minimal (VMin, UMin) or maximal (VMax, UMax) index in Q1 and in Q2 (possibly two different nodes)."""
|
|
699
|
+
self._check()
|
|
700
|
+
return self._c1, self._c2
|
|
701
|
+
|
|
702
|
+
def __len__(self) -> int:
|
|
703
|
+
self._check()
|
|
704
|
+
return len(self._members)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
class VMin(_VNode):
|
|
708
|
+
"""S in the target role, can_reach(u, S). Minimum of each index over a node set: u reaches all of the set exactly when u lies
|
|
709
|
+
strictly left of VMin in both Q1 and Q2."""
|
|
710
|
+
|
|
711
|
+
_lowest = True
|
|
712
|
+
|
|
713
|
+
def reaches_all(self, u: str) -> bool:
|
|
714
|
+
"""True if u reaches every node of the set (vacuously True for an empty set)."""
|
|
715
|
+
self._check()
|
|
716
|
+
t = self._t
|
|
717
|
+
if not t.contains(u):
|
|
718
|
+
return False
|
|
719
|
+
if self._c1 is None:
|
|
720
|
+
return True
|
|
721
|
+
return t._q1.precedes(u, self._c1) and t._q2.precedes(u, self._c2)
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
class VMax(_VNode):
|
|
725
|
+
"""S in the target role, can_reach(u, S). Maximum of each index over a node set: lying
|
|
726
|
+
strictly left of VMax in both Q1 and Q2 is necessary for u to reach some of the set, but
|
|
727
|
+
not sufficient, so may_reach_some is only a filter; reaches_some is exact."""
|
|
728
|
+
|
|
729
|
+
_lowest = False
|
|
730
|
+
|
|
731
|
+
def may_reach_some(self, u: str) -> bool:
|
|
732
|
+
"""Fast rejection: False means u reaches no node of the set. True is not a confirmation."""
|
|
733
|
+
self._check()
|
|
734
|
+
t = self._t
|
|
735
|
+
if self._c1 is None or not t.contains(u):
|
|
736
|
+
return False
|
|
737
|
+
return t._q1.precedes(u, self._c1) and t._q2.precedes(u, self._c2)
|
|
738
|
+
|
|
739
|
+
def reaches_some(self, u: str) -> bool:
|
|
740
|
+
"""True if u reaches at least one node of the set (exact).
|
|
741
|
+
|
|
742
|
+
The subtree of u is the block directly after u in Q1, so u reaches some member
|
|
743
|
+
exactly when the first member after u in Q1 lies in it: a binary search over the
|
|
744
|
+
members (kept in Q1 order) and one reachability test.
|
|
745
|
+
"""
|
|
746
|
+
if not self.may_reach_some(u):
|
|
747
|
+
return False
|
|
748
|
+
t, m = self._t, self._members
|
|
749
|
+
q1 = t._q1
|
|
750
|
+
lo, hi = 0, len(m)
|
|
751
|
+
while lo < hi:
|
|
752
|
+
mid = (lo + hi) // 2
|
|
753
|
+
if q1.precedes(u, m[mid]):
|
|
754
|
+
hi = mid
|
|
755
|
+
else:
|
|
756
|
+
lo = mid + 1
|
|
757
|
+
return lo < len(m) and t.can_reach(u, m[lo])
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
class UMax(_VNode):
|
|
761
|
+
"""S in the source role, can_reach(S, v). Maximum of each index over a node set: v is
|
|
762
|
+
reached by all of the set exactly when v lies strictly right of UMax in both Q1 and Q2."""
|
|
763
|
+
|
|
764
|
+
_lowest = False
|
|
765
|
+
|
|
766
|
+
def reached_by_all(self, v: str) -> bool:
|
|
767
|
+
"""True if every node of the set reaches v (vacuously True for an empty set)."""
|
|
768
|
+
self._check()
|
|
769
|
+
t = self._t
|
|
770
|
+
if not t.contains(v):
|
|
771
|
+
return False
|
|
772
|
+
if self._c1 is None:
|
|
773
|
+
return True
|
|
774
|
+
return t._q1.precedes(self._c1, v) and t._q2.precedes(self._c2, v)
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
class UMin(_VNode):
|
|
778
|
+
"""S in the source role, can_reach(S, v). Minimum of each index over a node set: lying
|
|
779
|
+
strictly right of UMin in both Q1 and Q2 is necessary for v to be reached by some of the
|
|
780
|
+
set, but not sufficient, so may_be_reached_by_some is only a filter; reached_by_some is
|
|
781
|
+
exact.
|
|
782
|
+
|
|
783
|
+
The exact test keeps the topmost members (those with no ancestor in the set) in Q1
|
|
784
|
+
order. Their subtrees are disjoint, so if one of them is an ancestor of v, no other
|
|
785
|
+
topmost member lies between it and v in Q1: the last topmost member before v in Q1 is
|
|
786
|
+
the only one to test.
|
|
787
|
+
"""
|
|
788
|
+
|
|
789
|
+
_lowest = True
|
|
790
|
+
|
|
791
|
+
def __init__(self, tree: "DTree", nodes: Iterable[str] = ()) -> None:
|
|
792
|
+
self._tops: List[str] = [] # topmost members, in Q1 order
|
|
793
|
+
super().__init__(tree, nodes)
|
|
794
|
+
|
|
795
|
+
def _last_top_before(self, v: str) -> int:
|
|
796
|
+
"""Number of topmost members strictly left of v in Q1."""
|
|
797
|
+
q1, m = self._t._q1, self._tops
|
|
798
|
+
lo, hi = 0, len(m)
|
|
799
|
+
while lo < hi:
|
|
800
|
+
mid = (lo + hi) // 2
|
|
801
|
+
if q1.precedes(m[mid], v):
|
|
802
|
+
lo = mid + 1
|
|
803
|
+
else:
|
|
804
|
+
hi = mid
|
|
805
|
+
return lo
|
|
806
|
+
|
|
807
|
+
def _covered(self, v: str) -> bool:
|
|
808
|
+
i = self._last_top_before(v)
|
|
809
|
+
return i > 0 and self._t.can_reach(self._tops[i - 1], v)
|
|
810
|
+
|
|
811
|
+
def _added(self, node: str) -> None:
|
|
812
|
+
if self._covered(node):
|
|
813
|
+
return # an ancestor of node is already in the set
|
|
814
|
+
i = self._last_top_before(node)
|
|
815
|
+
t, m = self._t, self._tops
|
|
816
|
+
while i < len(m) and t.can_reach(node, m[i]):
|
|
817
|
+
del m[i] # node is above them: they are no longer topmost
|
|
818
|
+
m.insert(i, node)
|
|
819
|
+
|
|
820
|
+
def _rebuilt(self) -> None:
|
|
821
|
+
self._tops = []
|
|
822
|
+
t = self._t
|
|
823
|
+
for v in self._members: # Q1 order: ancestors come first
|
|
824
|
+
if not (self._tops and t.can_reach(self._tops[-1], v)):
|
|
825
|
+
self._tops.append(v)
|
|
826
|
+
|
|
827
|
+
def may_be_reached_by_some(self, v: str) -> bool:
|
|
828
|
+
"""Fast rejection: False means no node of the set reaches v; True is not a confirmation."""
|
|
829
|
+
self._check()
|
|
830
|
+
t = self._t
|
|
831
|
+
if self._c1 is None or not t.contains(v):
|
|
832
|
+
return False
|
|
833
|
+
return t._q1.precedes(self._c1, v) and t._q2.precedes(self._c2, v)
|
|
834
|
+
|
|
835
|
+
def reached_by_some(self, v: str) -> bool:
|
|
836
|
+
"""True if at least one node of the set reaches v (exact, O(log k))."""
|
|
837
|
+
if not self.may_be_reached_by_some(v):
|
|
838
|
+
return False
|
|
839
|
+
return self._covered(v)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dtree-encoding
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: DTree: incremental encoding of directed trees by two projections (Q1, Q2) with O(1) reachability and set queries
|
|
5
|
+
Author-email: Enis Olgac <enis@digraphs.info>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/EnisOlgac/DTree
|
|
8
|
+
Project-URL: Documentation, https://github.com/EnisOlgac/DTree/blob/main/docs/API.md
|
|
9
|
+
Project-URL: Changelog, https://github.com/EnisOlgac/DTree/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Paper, https://doi.org/10.5772/intechopen.1013408
|
|
11
|
+
Project-URL: Archive, https://doi.org/10.5281/zenodo.23395710
|
|
12
|
+
Keywords: directed tree,reachability,incremental encoding,dynamic tree,graph theory,data structure
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# DTree for Python
|
|
25
|
+
|
|
26
|
+
Python implementation of `DTree`, the incremental encoding of directed trees by two projections Q1 and Q2 (see the [API reference](https://github.com/EnisOlgac/DTree/blob/main/docs/API.md) and the [paper](https://doi.org/10.5772/intechopen.1013408)). No dependencies; Python 3.8+.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
pip install dtree-encoding
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The package is imported as `dtree`.
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from dtree import DTree
|
|
36
|
+
|
|
37
|
+
t = DTree("P1")
|
|
38
|
+
t.insert("A", "B")
|
|
39
|
+
t.insert("B", "C")
|
|
40
|
+
|
|
41
|
+
t.can_reach("A", "C") # True
|
|
42
|
+
t.path_to_root("C") # ['C', 'B', 'A']
|
|
43
|
+
t.q1, t.q2 # (['A', 'B', 'C'], ['A', 'B', 'C'])
|
|
44
|
+
|
|
45
|
+
t.insert("A", "D")
|
|
46
|
+
t.cut_n_paste("C", "D") # move subtree C under D
|
|
47
|
+
t.delete("B") # B's children (none left) move up to A
|
|
48
|
+
t.delete_subtree("D") # removes D and C
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Load one of the example files (`parent: child1; child2` per line):
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
t = DTree.from_file("Test.csx") # e.g. examples/Test.csx from the repository
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API (C# name → Python name)
|
|
58
|
+
|
|
59
|
+
| C# (docs) | Python |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `DTree(id)` | `DTree(tree_id="P1")` |
|
|
62
|
+
| `Insert(parent, child)` | `insert(parent, child)` |
|
|
63
|
+
| `Contains(node)` | `contains(node)` / `node in t` |
|
|
64
|
+
| `GetParent(node)` | `get_parent(node)` (`None` for the root) |
|
|
65
|
+
| `CutNPaste(u, p)` | `cut_n_paste(subtree_root, new_parent)` |
|
|
66
|
+
| `Delete(node)` | `delete(node)`—children move up to the parent |
|
|
67
|
+
| — | `delete_subtree(node)`—removes the node and all descendants |
|
|
68
|
+
| `Q1`, `Q2` | `Q1`, `Q2` (the `DList` projections, for list-level checks); `q1`, `q2` (plain lists, without the `[` `]` markers) |
|
|
69
|
+
| `Source`, `Sink` | `source`, `sink` |
|
|
70
|
+
| `CanReach`, `CanNotReach` | `can_reach`, `can_not_reach` |
|
|
71
|
+
| `IsRoot`, `IsLeaf` | `is_root`, `is_leaf` |
|
|
72
|
+
| `IsAncestor`, `IsDescendant` | `is_ancestor`, `is_descendant` |
|
|
73
|
+
| `SameSubtree`, `SubtreeSize` | `same_subtree`, `subtree_size` (plus `subtree`) |
|
|
74
|
+
| `Depth` | `depth` (root = 0) |
|
|
75
|
+
| `PathToRoot`, `RootToPath` | `path_to_root`, `root_to_path` (plus `path(u, v)`) |
|
|
76
|
+
| `VMin`, `VMax`, `UMin`, `UMax` (of nodes) | `vmin`, `vmax`, `umin`, `umax` |
|
|
77
|
+
| `ReachesAll`, `MayReachSome`, `ReachesSome` | `reaches_all`, `may_reach_some`, `reaches_some` |
|
|
78
|
+
| `ReachedByAll`, `MayBeReachedBySome`, `ReachedBySome` | `reached_by_all`, `may_be_reached_by_some`, `reached_by_some` |
|
|
79
|
+
|
|
80
|
+
## Semantics
|
|
81
|
+
|
|
82
|
+
- `can_reach(u, v)` is irreflexive: no node reaches itself, so `can_reach(u, u)` is `False`. (The C# prototype is reflexive.)
|
|
83
|
+
- `is_ancestor(u, v)` equals `can_reach(u, v)` and is likewise proper.
|
|
84
|
+
- `delete(v)` re-attaches v's children to v's parent, in v's place among its siblings.
|
|
85
|
+
- `delete_subtree(v)` removes v with all its descendants and returns them in Q1 order.
|
|
86
|
+
- `cut_n_paste(u, p)` moves the subtree rooted at u under p as p's last child; p must not lie inside u's subtree.
|
|
87
|
+
- The constructed nodes `vmin(S)`, `vmax(S)` (S as target) and `umin(S)`, `umax(S)` (S as source) take the minimum or maximum of each index over S, separately in Q1 and Q2. `reaches_all` and `reached_by_all` are exact O(1) tests; `may_reach_some` and `may_be_reached_by_some` are only filters, and `reaches_some` and `reached_by_some` are exact in O(log k). A constructed node stays valid under insertions and is recomputed automatically after `delete`, `delete_subtree` or `cut_n_paste`. See [set queries](https://github.com/EnisOlgac/DTree/blob/main/docs/set-queries.md).
|
|
88
|
+
- Projection positions are fractions: a new node gets the mediant (a+c)/(b+d) of its neighbors, so normally no other node moves. Only rare alternating insertion patterns trigger a local renumbering. Tree depth and width are unbounded. `t.Q1.index(v)` returns a position as a `Fraction`.
|
|
89
|
+
|
|
90
|
+
## Tests
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
python -m unittest discover -s tests
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Every operation is checked against a brute-force parent/children model on random trees, including random sequences of deletes and moves.
|
|
97
|
+
|
|
98
|
+
## Benchmarks
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
python benchmarks/bench.py # trees of 10,000 and 100,000 nodes
|
|
102
|
+
python benchmarks/bench.py --quick # a few seconds
|
|
103
|
+
python benchmarks/bench.py --dynamic # only insertions and queries alternating
|
|
104
|
+
python benchmarks/bench.py 1000000 # custom sizes
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The script prints Markdown tables: build time and renumbering counts per tree shape (random, chain, star at the root, star at an inner node, alternating worst case), list growth at either end, and timings for `can_reach`, `depth`, `delete`, `cut_n_paste` and `delete_subtree`, and comparisons with a classical parent walk: reachability by tree depth, set queries with constructed nodes, and dynamic updates (insertions and queries alternating). Each `DList` keeps the counters `renumber_count` and `renumber_moved`, so the same figures are available in your own code via `t.Q1` and `t.Q2`.
|
|
108
|
+
|
|
109
|
+
## Provenance
|
|
110
|
+
|
|
111
|
+
Design and API: Enis Olgac. This Python port was written with Claude (Anthropic) from the C# original and the API reference.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
dtree/__init__.py,sha256=T_N2PGTrhcITiUjo3wTqqbvDBNBwUcGx1DL4otbse74,241
|
|
2
|
+
dtree/dtree.py,sha256=RD3R2DaA_aVQrOnXSpYtPl3ibpNJb9GVDJ2_KRquClc,31814
|
|
3
|
+
dtree_encoding-1.5.0.dist-info/METADATA,sha256=EQYpNNa7r8WIc1zAZI9S9VIQOdkLG10sgiTZT0-3eUc,6028
|
|
4
|
+
dtree_encoding-1.5.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
dtree_encoding-1.5.0.dist-info/top_level.txt,sha256=4QtyU5yebXuqrNE0olcPNOJmZ8woJFEiJczb-jmar2o,6
|
|
6
|
+
dtree_encoding-1.5.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dtree
|