pkstruct 0.1.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.
- pkstruct/__init__.py +167 -0
- pkstruct/graphs/__init__.py +127 -0
- pkstruct/graphs/connectivity.py +157 -0
- pkstruct/graphs/directed.py +95 -0
- pkstruct/graphs/exceptions.py +63 -0
- pkstruct/graphs/graph.py +262 -0
- pkstruct/graphs/mst.py +118 -0
- pkstruct/graphs/scc.py +138 -0
- pkstruct/graphs/shortest_path.py +250 -0
- pkstruct/graphs/topo_sort.py +108 -0
- pkstruct/graphs/traversal.py +175 -0
- pkstruct/graphs/visualization.py +90 -0
- pkstruct/graphs/weighted.py +37 -0
- pkstruct/linear/__init__.py +95 -0
- pkstruct/linear/deques/__init__.py +33 -0
- pkstruct/linear/deques/deque.py +194 -0
- pkstruct/linear/deques/linked_deque.py +198 -0
- pkstruct/linear/exceptions.py +26 -0
- pkstruct/linear/linked_lists/__init__.py +5 -0
- pkstruct/linear/linked_lists/_base.py +608 -0
- pkstruct/linear/linked_lists/circular_linked_list.py +230 -0
- pkstruct/linear/linked_lists/doubly_linked_list.py +151 -0
- pkstruct/linear/linked_lists/nodes.py +68 -0
- pkstruct/linear/linked_lists/singly_linked_list.py +136 -0
- pkstruct/linear/queues/__init__.py +44 -0
- pkstruct/linear/queues/circular_queue.py +258 -0
- pkstruct/linear/queues/linked_queue.py +186 -0
- pkstruct/linear/queues/priority_queue.py +202 -0
- pkstruct/linear/queues/queue.py +174 -0
- pkstruct/linear/stacks/__init__.py +38 -0
- pkstruct/linear/stacks/array_stack.py +165 -0
- pkstruct/linear/stacks/linked_stack.py +168 -0
- pkstruct/linear/stacks/stack.py +158 -0
- pkstruct/linear/utils/__init__.py +18 -0
- pkstruct/linear/utils/benchmark.py +255 -0
- pkstruct/linear/utils/debug_tools.py +239 -0
- pkstruct/linear/utils/helpers.py +143 -0
- pkstruct/linear/utils/iterators.py +148 -0
- pkstruct/linear/visualization/__init__.py +0 -0
- pkstruct/linear/visualization/ascii_visualizer.py +114 -0
- pkstruct/linear/visualization/linked_list_visualizer.py +126 -0
- pkstruct/shared/__init__.py +67 -0
- pkstruct/shared/benchmarking/__init__.py +78 -0
- pkstruct/shared/debugging/__init__.py +69 -0
- pkstruct/shared/exceptions/__init__.py +59 -0
- pkstruct/shared/serializers/__init__.py +65 -0
- pkstruct/shared/threading/__init__.py +43 -0
- pkstruct/shared/validators/__init__.py +98 -0
- pkstruct/shared/visualization/__init__.py +21 -0
- pkstruct/trees/__init__.py +92 -0
- pkstruct/trees/avl.py +321 -0
- pkstruct/trees/balancing.py +253 -0
- pkstruct/trees/bplus.py +425 -0
- pkstruct/trees/bst.py +948 -0
- pkstruct/trees/btree.py +504 -0
- pkstruct/trees/exceptions.py +96 -0
- pkstruct/trees/fenwick_tree.py +312 -0
- pkstruct/trees/interval_tree.py +541 -0
- pkstruct/trees/node.py +356 -0
- pkstruct/trees/red_black.py +710 -0
- pkstruct/trees/segment_tree.py +398 -0
- pkstruct/trees/traversal.py +456 -0
- pkstruct/trees/tree_helpers.py +366 -0
- pkstruct/trees/utils/__init__.py +15 -0
- pkstruct/trees/utils/complexity_helpers.py +231 -0
- pkstruct/trees/visualization/__init__.py +0 -0
- pkstruct/trees/visualization/ascii_renderer.py +220 -0
- pkstruct/trees/visualization/tree_printer.py +129 -0
- pkstruct-0.1.0.dist-info/METADATA +482 -0
- pkstruct-0.1.0.dist-info/RECORD +72 -0
- pkstruct-0.1.0.dist-info/WHEEL +4 -0
- pkstruct-0.1.0.dist-info/licenses/LICENSE +21 -0
pkstruct/graphs/graph.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.graphs.graph
|
|
3
|
+
=====================
|
|
4
|
+
Core graph data structure using adjacency-list representation.
|
|
5
|
+
|
|
6
|
+
Supports both directed and undirected modes, with optional edge weights.
|
|
7
|
+
All public operations are thread-safe via ``StructureLock``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Iterator
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from pkstruct.graphs.exceptions import VertexNotFoundError, EdgeNotFoundError
|
|
16
|
+
from pkstruct.shared.threading import StructureLock
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Graph:
|
|
20
|
+
"""Adjacency-list based graph supporting directed/undirected and weighted modes.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
directed : bool, default=False
|
|
25
|
+
If *True*, edges are one-way; otherwise edges are bidirectional.
|
|
26
|
+
|
|
27
|
+
Example
|
|
28
|
+
-------
|
|
29
|
+
>>> g = Graph()
|
|
30
|
+
>>> g.add_vertex("A")
|
|
31
|
+
>>> g.add_edge("A", "B")
|
|
32
|
+
>>> g.add_edge("A", "C")
|
|
33
|
+
>>> len(g)
|
|
34
|
+
3
|
|
35
|
+
>>> list(g.get_neighbors("A"))
|
|
36
|
+
['B', 'C']
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
__slots__ = ("_adj", "_directed", "_lock", "_edge_count")
|
|
40
|
+
|
|
41
|
+
def __init__(self, directed: bool = False) -> None:
|
|
42
|
+
self._adj: dict[Any, dict[Any, float]] = {}
|
|
43
|
+
self._directed: bool = directed
|
|
44
|
+
self._lock: StructureLock = StructureLock()
|
|
45
|
+
self._edge_count: int = 0
|
|
46
|
+
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
# Mutation
|
|
49
|
+
# ------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
def add_vertex(self, v: Any) -> None:
|
|
52
|
+
"""Add a vertex to the graph. No-op if already present."""
|
|
53
|
+
with self._lock:
|
|
54
|
+
if v not in self._adj:
|
|
55
|
+
self._adj[v] = {}
|
|
56
|
+
|
|
57
|
+
def add_edge(self, u: Any, v: Any, weight: float = 1.0) -> None:
|
|
58
|
+
"""Add an edge between *u* and *v* with optional *weight*.
|
|
59
|
+
|
|
60
|
+
Vertices are created automatically if they do not exist.
|
|
61
|
+
If the edge already exists, its weight is updated.
|
|
62
|
+
"""
|
|
63
|
+
with self._lock:
|
|
64
|
+
self.add_vertex(u)
|
|
65
|
+
self.add_vertex(v)
|
|
66
|
+
if v not in self._adj[u]:
|
|
67
|
+
self._edge_count += 1
|
|
68
|
+
self._adj[u][v] = weight
|
|
69
|
+
if not self._directed and u != v:
|
|
70
|
+
self._adj[v][u] = weight
|
|
71
|
+
|
|
72
|
+
def remove_vertex(self, v: Any) -> None:
|
|
73
|
+
"""Remove *v* and all incident edges.
|
|
74
|
+
|
|
75
|
+
Raises
|
|
76
|
+
------
|
|
77
|
+
VertexNotFoundError
|
|
78
|
+
If *v* is not in the graph.
|
|
79
|
+
"""
|
|
80
|
+
with self._lock:
|
|
81
|
+
if v not in self._adj:
|
|
82
|
+
raise VertexNotFoundError(v)
|
|
83
|
+
for neighbor in list(self._adj[v]):
|
|
84
|
+
self._adj[neighbor].pop(v, None)
|
|
85
|
+
del self._adj[v]
|
|
86
|
+
self._edge_count = sum(len(nbrs) for nbrs in self._adj.values())
|
|
87
|
+
if not self._directed:
|
|
88
|
+
self._edge_count //= 2
|
|
89
|
+
|
|
90
|
+
def remove_edge(self, u: Any, v: Any) -> None:
|
|
91
|
+
"""Remove the edge from *u* to *v*.
|
|
92
|
+
|
|
93
|
+
Raises
|
|
94
|
+
------
|
|
95
|
+
VertexNotFoundError
|
|
96
|
+
If either vertex does not exist.
|
|
97
|
+
EdgeNotFoundError
|
|
98
|
+
If the edge does not exist.
|
|
99
|
+
"""
|
|
100
|
+
with self._lock:
|
|
101
|
+
if u not in self._adj:
|
|
102
|
+
raise VertexNotFoundError(u)
|
|
103
|
+
if v not in self._adj:
|
|
104
|
+
raise VertexNotFoundError(v)
|
|
105
|
+
if v not in self._adj[u]:
|
|
106
|
+
raise EdgeNotFoundError(u, v)
|
|
107
|
+
del self._adj[u][v]
|
|
108
|
+
self._edge_count -= 1
|
|
109
|
+
if not self._directed and u != v:
|
|
110
|
+
self._adj[v].pop(u, None)
|
|
111
|
+
|
|
112
|
+
def clear(self) -> None:
|
|
113
|
+
"""Remove all vertices and edges."""
|
|
114
|
+
with self._lock:
|
|
115
|
+
self._adj.clear()
|
|
116
|
+
self._edge_count = 0
|
|
117
|
+
|
|
118
|
+
# ------------------------------------------------------------------
|
|
119
|
+
# Access
|
|
120
|
+
# ------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
def has_vertex(self, v: Any) -> bool:
|
|
123
|
+
"""Return *True* if *v* is in the graph."""
|
|
124
|
+
with self._lock:
|
|
125
|
+
return v in self._adj
|
|
126
|
+
|
|
127
|
+
def has_edge(self, u: Any, v: Any) -> bool:
|
|
128
|
+
"""Return *True* if the edge (u, v) exists."""
|
|
129
|
+
with self._lock:
|
|
130
|
+
return u in self._adj and v in self._adj[u]
|
|
131
|
+
|
|
132
|
+
def get_weight(self, u: Any, v: Any) -> float:
|
|
133
|
+
"""Return the weight of edge (u, v).
|
|
134
|
+
|
|
135
|
+
Raises
|
|
136
|
+
------
|
|
137
|
+
VertexNotFoundError
|
|
138
|
+
If either vertex does not exist.
|
|
139
|
+
EdgeNotFoundError
|
|
140
|
+
If the edge does not exist.
|
|
141
|
+
"""
|
|
142
|
+
with self._lock:
|
|
143
|
+
if u not in self._adj:
|
|
144
|
+
raise VertexNotFoundError(u)
|
|
145
|
+
if v not in self._adj[u]:
|
|
146
|
+
raise EdgeNotFoundError(u, v)
|
|
147
|
+
return self._adj[u][v]
|
|
148
|
+
|
|
149
|
+
def set_weight(self, u: Any, v: Any, weight: float) -> None:
|
|
150
|
+
"""Set the weight of edge (u, v).
|
|
151
|
+
|
|
152
|
+
Raises
|
|
153
|
+
------
|
|
154
|
+
VertexNotFoundError
|
|
155
|
+
If either vertex does not exist.
|
|
156
|
+
EdgeNotFoundError
|
|
157
|
+
If the edge does not exist.
|
|
158
|
+
"""
|
|
159
|
+
with self._lock:
|
|
160
|
+
if u not in self._adj:
|
|
161
|
+
raise VertexNotFoundError(u)
|
|
162
|
+
if v not in self._adj[u]:
|
|
163
|
+
raise EdgeNotFoundError(u, v)
|
|
164
|
+
self._adj[u][v] = weight
|
|
165
|
+
if not self._directed and u != v:
|
|
166
|
+
self._adj[v][u] = weight
|
|
167
|
+
|
|
168
|
+
def get_neighbors(self, v: Any) -> list[Any]:
|
|
169
|
+
"""Return a list of neighbors of *v* (outgoing in directed mode).
|
|
170
|
+
|
|
171
|
+
Raises
|
|
172
|
+
------
|
|
173
|
+
VertexNotFoundError
|
|
174
|
+
If *v* is not in the graph.
|
|
175
|
+
"""
|
|
176
|
+
with self._lock:
|
|
177
|
+
if v not in self._adj:
|
|
178
|
+
raise VertexNotFoundError(v)
|
|
179
|
+
return list(self._adj[v].keys())
|
|
180
|
+
|
|
181
|
+
def get_vertices(self) -> list[Any]:
|
|
182
|
+
"""Return a list of all vertices in the graph."""
|
|
183
|
+
with self._lock:
|
|
184
|
+
return list(self._adj.keys())
|
|
185
|
+
|
|
186
|
+
def get_edges(self) -> list[tuple[Any, Any, float]]:
|
|
187
|
+
"""Return a list of all edges as (u, v, weight) tuples."""
|
|
188
|
+
with self._lock:
|
|
189
|
+
edges: list[tuple[Any, Any, float]] = []
|
|
190
|
+
seen: set[frozenset] = set()
|
|
191
|
+
for u in self._adj:
|
|
192
|
+
for v, w in self._adj[u].items():
|
|
193
|
+
if not self._directed:
|
|
194
|
+
key = frozenset((u, v))
|
|
195
|
+
if key in seen:
|
|
196
|
+
continue
|
|
197
|
+
seen.add(key)
|
|
198
|
+
edges.append((u, v, w))
|
|
199
|
+
return edges
|
|
200
|
+
|
|
201
|
+
def degree(self, v: Any) -> int:
|
|
202
|
+
"""Return the degree of vertex *v*.
|
|
203
|
+
|
|
204
|
+
For directed graphs, returns the out-degree (number of outgoing edges).
|
|
205
|
+
|
|
206
|
+
Raises
|
|
207
|
+
------
|
|
208
|
+
VertexNotFoundError
|
|
209
|
+
If *v* is not in the graph.
|
|
210
|
+
"""
|
|
211
|
+
with self._lock:
|
|
212
|
+
if v not in self._adj:
|
|
213
|
+
raise VertexNotFoundError(v)
|
|
214
|
+
return len(self._adj[v])
|
|
215
|
+
|
|
216
|
+
def order(self) -> int:
|
|
217
|
+
"""Return the number of vertices in the graph."""
|
|
218
|
+
with self._lock:
|
|
219
|
+
return len(self._adj)
|
|
220
|
+
|
|
221
|
+
def edge_count(self) -> int:
|
|
222
|
+
"""Return the number of edges in the graph."""
|
|
223
|
+
with self._lock:
|
|
224
|
+
return self._edge_count
|
|
225
|
+
|
|
226
|
+
def is_directed(self) -> bool:
|
|
227
|
+
"""Return *True* if this is a directed graph."""
|
|
228
|
+
return self._directed
|
|
229
|
+
|
|
230
|
+
def is_empty(self) -> bool:
|
|
231
|
+
"""Return *True* if the graph has no vertices."""
|
|
232
|
+
with self._lock:
|
|
233
|
+
return len(self._adj) == 0
|
|
234
|
+
|
|
235
|
+
def copy(self) -> Graph:
|
|
236
|
+
"""Return a deep copy of the graph."""
|
|
237
|
+
with self._lock:
|
|
238
|
+
new_graph = Graph(directed=self._directed)
|
|
239
|
+
for v in self._adj:
|
|
240
|
+
new_graph._adj[v] = dict(self._adj[v])
|
|
241
|
+
new_graph._edge_count = self._edge_count
|
|
242
|
+
return new_graph
|
|
243
|
+
|
|
244
|
+
# ------------------------------------------------------------------
|
|
245
|
+
# Dunders
|
|
246
|
+
# ------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
def __len__(self) -> int:
|
|
249
|
+
return self.order()
|
|
250
|
+
|
|
251
|
+
def __contains__(self, v: Any) -> bool:
|
|
252
|
+
return self.has_vertex(v)
|
|
253
|
+
|
|
254
|
+
def __iter__(self) -> Iterator[Any]:
|
|
255
|
+
vertices = self.get_vertices()
|
|
256
|
+
return iter(vertices)
|
|
257
|
+
|
|
258
|
+
def __repr__(self) -> str:
|
|
259
|
+
with self._lock:
|
|
260
|
+
vertices = list(self._adj.keys())
|
|
261
|
+
edges = self.get_edges()
|
|
262
|
+
return f"Graph(directed={self._directed}, vertices={len(vertices)}, edges={len(edges)})"
|
pkstruct/graphs/mst.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.graphs.mst
|
|
3
|
+
===================
|
|
4
|
+
Minimum spanning tree algorithms: Kruskal and Prim.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import heapq
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pkstruct.graphs.graph import Graph
|
|
13
|
+
from pkstruct.graphs.exceptions import InvalidGraphOperationError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def kruskal(graph: Graph) -> list[tuple[Any, Any, float]]:
|
|
17
|
+
"""Compute the Minimum Spanning Tree using Kruskal's algorithm.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
graph : Graph
|
|
22
|
+
An undirected weighted graph.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
list[tuple[Any, Any, float]]
|
|
27
|
+
Edges of the MST as ``(u, v, weight)`` tuples.
|
|
28
|
+
|
|
29
|
+
Raises
|
|
30
|
+
------
|
|
31
|
+
InvalidGraphOperationError
|
|
32
|
+
If the graph is directed.
|
|
33
|
+
"""
|
|
34
|
+
if graph.is_directed():
|
|
35
|
+
raise InvalidGraphOperationError("Kruskal's algorithm requires an undirected graph.")
|
|
36
|
+
|
|
37
|
+
edges = sorted(graph.get_edges(), key=lambda e: e[2])
|
|
38
|
+
parent: dict[Any, Any] = {}
|
|
39
|
+
rank: dict[Any, int] = {}
|
|
40
|
+
|
|
41
|
+
def find(x: Any) -> Any:
|
|
42
|
+
while parent[x] != x:
|
|
43
|
+
parent[x] = parent[parent[x]]
|
|
44
|
+
x = parent[x]
|
|
45
|
+
return x
|
|
46
|
+
|
|
47
|
+
def union(x: Any, y: Any) -> None:
|
|
48
|
+
rx, ry = find(x), find(y)
|
|
49
|
+
if rx == ry:
|
|
50
|
+
return
|
|
51
|
+
if rank[rx] < rank[ry]:
|
|
52
|
+
parent[rx] = ry
|
|
53
|
+
elif rank[rx] > rank[ry]:
|
|
54
|
+
parent[ry] = rx
|
|
55
|
+
else:
|
|
56
|
+
parent[ry] = rx
|
|
57
|
+
rank[rx] += 1
|
|
58
|
+
|
|
59
|
+
for v in graph:
|
|
60
|
+
parent[v] = v
|
|
61
|
+
rank[v] = 0
|
|
62
|
+
|
|
63
|
+
mst: list[tuple[Any, Any, float]] = []
|
|
64
|
+
for u, v, w in edges:
|
|
65
|
+
if find(u) != find(v):
|
|
66
|
+
union(u, v)
|
|
67
|
+
mst.append((u, v, w))
|
|
68
|
+
|
|
69
|
+
return mst
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def prim(graph: Graph) -> list[tuple[Any, Any, float]]:
|
|
73
|
+
"""Compute the Minimum Spanning Tree using Prim's algorithm.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
graph : Graph
|
|
78
|
+
An undirected weighted graph.
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
list[tuple[Any, Any, float]]
|
|
83
|
+
Edges of the MST as ``(u, v, weight)`` tuples.
|
|
84
|
+
|
|
85
|
+
Raises
|
|
86
|
+
------
|
|
87
|
+
InvalidGraphOperationError
|
|
88
|
+
If the graph is directed or empty.
|
|
89
|
+
"""
|
|
90
|
+
if graph.is_directed():
|
|
91
|
+
raise InvalidGraphOperationError("Prim's algorithm requires an undirected graph.")
|
|
92
|
+
|
|
93
|
+
vertices = list(graph)
|
|
94
|
+
if not vertices:
|
|
95
|
+
raise InvalidGraphOperationError("Graph is empty.")
|
|
96
|
+
|
|
97
|
+
start = vertices[0]
|
|
98
|
+
visited: set[Any] = {start}
|
|
99
|
+
pq: list[tuple[float, Any, Any]] = []
|
|
100
|
+
|
|
101
|
+
for neighbor in graph.get_neighbors(start):
|
|
102
|
+
weight = graph.get_weight(start, neighbor)
|
|
103
|
+
heapq.heappush(pq, (weight, start, neighbor))
|
|
104
|
+
|
|
105
|
+
mst: list[tuple[Any, Any, float]] = []
|
|
106
|
+
|
|
107
|
+
while pq and len(visited) < len(vertices):
|
|
108
|
+
w, u, v = heapq.heappop(pq)
|
|
109
|
+
if v in visited:
|
|
110
|
+
continue
|
|
111
|
+
visited.add(v)
|
|
112
|
+
mst.append((u, v, w))
|
|
113
|
+
for neighbor in graph.get_neighbors(v):
|
|
114
|
+
if neighbor not in visited:
|
|
115
|
+
weight = graph.get_weight(v, neighbor)
|
|
116
|
+
heapq.heappush(pq, (weight, v, neighbor))
|
|
117
|
+
|
|
118
|
+
return mst
|
pkstruct/graphs/scc.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.graphs.scc
|
|
3
|
+
===================
|
|
4
|
+
Strongly connected components algorithms: Kosaraju and Tarjan.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pkstruct.graphs.graph import Graph
|
|
12
|
+
from pkstruct.graphs.directed import DirectedGraph
|
|
13
|
+
from pkstruct.graphs.exceptions import InvalidGraphOperationError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def kosaraju(graph: Graph) -> list[list[Any]]:
|
|
17
|
+
"""Find strongly connected components using Kosaraju's algorithm.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
graph : Graph
|
|
22
|
+
A directed graph.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
list[list[Any]]
|
|
27
|
+
List of SCCs, each being a list of vertices.
|
|
28
|
+
|
|
29
|
+
Raises
|
|
30
|
+
------
|
|
31
|
+
InvalidGraphOperationError
|
|
32
|
+
If the graph is undirected.
|
|
33
|
+
"""
|
|
34
|
+
if not graph.is_directed():
|
|
35
|
+
raise InvalidGraphOperationError("Kosaraju's algorithm requires a directed graph.")
|
|
36
|
+
|
|
37
|
+
visited: set[Any] = set()
|
|
38
|
+
finish_stack: list[Any] = []
|
|
39
|
+
|
|
40
|
+
def _dfs(v: Any) -> None:
|
|
41
|
+
visited.add(v)
|
|
42
|
+
for neighbor in graph.get_neighbors(v):
|
|
43
|
+
if neighbor not in visited:
|
|
44
|
+
_dfs(neighbor)
|
|
45
|
+
finish_stack.append(v)
|
|
46
|
+
|
|
47
|
+
for v in graph:
|
|
48
|
+
if v not in visited:
|
|
49
|
+
_dfs(v)
|
|
50
|
+
|
|
51
|
+
if isinstance(graph, DirectedGraph):
|
|
52
|
+
transposed = graph.reverse()
|
|
53
|
+
else:
|
|
54
|
+
transposed = DirectedGraph()
|
|
55
|
+
for v in graph:
|
|
56
|
+
transposed.add_vertex(v)
|
|
57
|
+
for u in graph:
|
|
58
|
+
for v, w in {n: graph.get_weight(u, n) for n in graph.get_neighbors(u)}.items():
|
|
59
|
+
transposed.add_edge(v, u, w)
|
|
60
|
+
|
|
61
|
+
visited.clear()
|
|
62
|
+
sccs: list[list[Any]] = []
|
|
63
|
+
|
|
64
|
+
def _dfs_reverse(v: Any, component: list[Any]) -> None:
|
|
65
|
+
visited.add(v)
|
|
66
|
+
component.append(v)
|
|
67
|
+
for neighbor in transposed.get_neighbors(v):
|
|
68
|
+
if neighbor not in visited:
|
|
69
|
+
_dfs_reverse(neighbor, component)
|
|
70
|
+
|
|
71
|
+
while finish_stack:
|
|
72
|
+
v = finish_stack.pop()
|
|
73
|
+
if v not in visited:
|
|
74
|
+
component: list[Any] = []
|
|
75
|
+
_dfs_reverse(v, component)
|
|
76
|
+
sccs.append(component)
|
|
77
|
+
|
|
78
|
+
return sccs
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def tarjan(graph: Graph) -> list[list[Any]]:
|
|
82
|
+
"""Find strongly connected components using Tarjan's algorithm.
|
|
83
|
+
|
|
84
|
+
Parameters
|
|
85
|
+
----------
|
|
86
|
+
graph : Graph
|
|
87
|
+
A directed graph.
|
|
88
|
+
|
|
89
|
+
Returns
|
|
90
|
+
-------
|
|
91
|
+
list[list[Any]]
|
|
92
|
+
List of SCCs, each being a list of vertices.
|
|
93
|
+
|
|
94
|
+
Raises
|
|
95
|
+
------
|
|
96
|
+
InvalidGraphOperationError
|
|
97
|
+
If the graph is undirected.
|
|
98
|
+
"""
|
|
99
|
+
if not graph.is_directed():
|
|
100
|
+
raise InvalidGraphOperationError("Tarjan's algorithm requires a directed graph.")
|
|
101
|
+
|
|
102
|
+
index_counter: int = 0
|
|
103
|
+
index: dict[Any, int] = {}
|
|
104
|
+
lowlink: dict[Any, int] = {}
|
|
105
|
+
stack: list[Any] = []
|
|
106
|
+
on_stack: set[Any] = set()
|
|
107
|
+
sccs: list[list[Any]] = []
|
|
108
|
+
|
|
109
|
+
def _strongconnect(v: Any) -> None:
|
|
110
|
+
nonlocal index_counter
|
|
111
|
+
index[v] = index_counter
|
|
112
|
+
lowlink[v] = index_counter
|
|
113
|
+
index_counter += 1
|
|
114
|
+
stack.append(v)
|
|
115
|
+
on_stack.add(v)
|
|
116
|
+
|
|
117
|
+
for neighbor in graph.get_neighbors(v):
|
|
118
|
+
if neighbor not in index:
|
|
119
|
+
_strongconnect(neighbor)
|
|
120
|
+
lowlink[v] = min(lowlink[v], lowlink[neighbor])
|
|
121
|
+
elif neighbor in on_stack:
|
|
122
|
+
lowlink[v] = min(lowlink[v], index[neighbor])
|
|
123
|
+
|
|
124
|
+
if lowlink[v] == index[v]:
|
|
125
|
+
component: list[Any] = []
|
|
126
|
+
while True:
|
|
127
|
+
w = stack.pop()
|
|
128
|
+
on_stack.discard(w)
|
|
129
|
+
component.append(w)
|
|
130
|
+
if w == v:
|
|
131
|
+
break
|
|
132
|
+
sccs.append(component)
|
|
133
|
+
|
|
134
|
+
for v in graph:
|
|
135
|
+
if v not in index:
|
|
136
|
+
_strongconnect(v)
|
|
137
|
+
|
|
138
|
+
return sccs
|