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
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.graphs.shortest_path
|
|
3
|
+
==============================
|
|
4
|
+
Shortest-path algorithms: Dijkstra, Bellman-Ford, Floyd-Warshall.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import heapq
|
|
10
|
+
import math
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pkstruct.graphs.graph import Graph
|
|
14
|
+
from pkstruct.graphs.exceptions import (
|
|
15
|
+
NegativeCycleError,
|
|
16
|
+
NoPathError,
|
|
17
|
+
VertexNotFoundError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def dijkstra(
|
|
22
|
+
graph: Graph, source: Any, target: Any | None = None
|
|
23
|
+
) -> tuple[dict[Any, float], dict[Any, Any | None]]:
|
|
24
|
+
"""Compute shortest paths from *source* using Dijkstra's algorithm.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
graph : Graph
|
|
29
|
+
Weighted graph (edge weights must be non-negative).
|
|
30
|
+
source : Any
|
|
31
|
+
Starting vertex.
|
|
32
|
+
target : Any, optional
|
|
33
|
+
If provided, early-exit when *target* is reached.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
tuple[dict[Any, float], dict[Any, Any | None]]
|
|
38
|
+
``(distances, predecessors)`` where *distances* maps each vertex
|
|
39
|
+
to its shortest distance from *source*, and *predecessors* maps
|
|
40
|
+
each vertex to its predecessor on the shortest path.
|
|
41
|
+
|
|
42
|
+
Raises
|
|
43
|
+
------
|
|
44
|
+
VertexNotFoundError
|
|
45
|
+
If *source* is not in the graph.
|
|
46
|
+
"""
|
|
47
|
+
if not graph.has_vertex(source):
|
|
48
|
+
raise VertexNotFoundError(source)
|
|
49
|
+
|
|
50
|
+
distances: dict[Any, float] = {v: math.inf for v in graph}
|
|
51
|
+
predecessors: dict[Any, Any | None] = {v: None for v in graph}
|
|
52
|
+
distances[source] = 0.0
|
|
53
|
+
|
|
54
|
+
pq: list[tuple[float, Any]] = [(0.0, source)]
|
|
55
|
+
visited: set[Any] = set()
|
|
56
|
+
|
|
57
|
+
while pq:
|
|
58
|
+
d, v = heapq.heappop(pq)
|
|
59
|
+
if v in visited:
|
|
60
|
+
continue
|
|
61
|
+
visited.add(v)
|
|
62
|
+
if target is not None and v == target:
|
|
63
|
+
break
|
|
64
|
+
for neighbor in graph.get_neighbors(v):
|
|
65
|
+
if neighbor in visited:
|
|
66
|
+
continue
|
|
67
|
+
weight = graph.get_weight(v, neighbor)
|
|
68
|
+
new_dist = d + weight
|
|
69
|
+
if new_dist < distances[neighbor]:
|
|
70
|
+
distances[neighbor] = new_dist
|
|
71
|
+
predecessors[neighbor] = v
|
|
72
|
+
heapq.heappush(pq, (new_dist, neighbor))
|
|
73
|
+
|
|
74
|
+
return distances, predecessors
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def bellman_ford(graph: Graph, source: Any) -> tuple[dict[Any, float], dict[Any, Any | None]]:
|
|
78
|
+
"""Compute shortest paths from *source* using Bellman-Ford.
|
|
79
|
+
|
|
80
|
+
Supports negative edge weights. Raises an error if a negative cycle
|
|
81
|
+
is reachable from *source*.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
graph : Graph
|
|
86
|
+
Weighted graph (may contain negative edge weights).
|
|
87
|
+
source : Any
|
|
88
|
+
Starting vertex.
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
tuple[dict[Any, float], dict[Any, Any | None]]
|
|
93
|
+
``(distances, predecessors)``.
|
|
94
|
+
|
|
95
|
+
Raises
|
|
96
|
+
------
|
|
97
|
+
VertexNotFoundError
|
|
98
|
+
If *source* is not in the graph.
|
|
99
|
+
NegativeCycleError
|
|
100
|
+
If a negative-weight cycle is reachable from *source*.
|
|
101
|
+
"""
|
|
102
|
+
if not graph.has_vertex(source):
|
|
103
|
+
raise VertexNotFoundError(source)
|
|
104
|
+
|
|
105
|
+
distances: dict[Any, float] = {v: math.inf for v in graph}
|
|
106
|
+
predecessors: dict[Any, Any | None] = {v: None for v in graph}
|
|
107
|
+
distances[source] = 0.0
|
|
108
|
+
|
|
109
|
+
vertices = list(graph)
|
|
110
|
+
n = len(vertices)
|
|
111
|
+
edges: list[tuple[Any, Any, float]] = []
|
|
112
|
+
for u in vertices:
|
|
113
|
+
for v in graph.get_neighbors(u):
|
|
114
|
+
w = graph.get_weight(u, v)
|
|
115
|
+
edges.append((u, v, w))
|
|
116
|
+
|
|
117
|
+
for _ in range(n - 1):
|
|
118
|
+
updated = False
|
|
119
|
+
for u, v, w in edges:
|
|
120
|
+
if distances[u] != math.inf and distances[u] + w < distances[v]:
|
|
121
|
+
distances[v] = distances[u] + w
|
|
122
|
+
predecessors[v] = u
|
|
123
|
+
updated = True
|
|
124
|
+
if not updated:
|
|
125
|
+
break
|
|
126
|
+
|
|
127
|
+
for u, v, w in edges:
|
|
128
|
+
if distances[u] != math.inf and distances[u] + w < distances[v]:
|
|
129
|
+
raise NegativeCycleError()
|
|
130
|
+
|
|
131
|
+
return distances, predecessors
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def floyd_warshall(
|
|
135
|
+
graph: Graph,
|
|
136
|
+
) -> tuple[dict[Any, dict[Any, float]], dict[Any, dict[Any, Any | None]]]:
|
|
137
|
+
"""Compute all-pairs shortest paths using Floyd-Warshall.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
graph : Graph
|
|
142
|
+
Weighted graph.
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
tuple[dict[Any, dict[Any, float]], dict[Any, dict[Any, Any | None]]]
|
|
147
|
+
``(distances, next_nodes)`` where *distances[u][v]* is the
|
|
148
|
+
shortest distance from *u* to *v*, and *next_nodes[u][v]* is
|
|
149
|
+
the next vertex on the shortest path from *u* to *v* (for path
|
|
150
|
+
reconstruction).
|
|
151
|
+
|
|
152
|
+
Raises
|
|
153
|
+
------
|
|
154
|
+
NegativeCycleError
|
|
155
|
+
If the graph contains a negative-weight cycle.
|
|
156
|
+
"""
|
|
157
|
+
vertices = list(graph)
|
|
158
|
+
dist: dict[Any, dict[Any, float]] = {u: {v: math.inf for v in vertices} for u in vertices}
|
|
159
|
+
nxt: dict[Any, dict[Any, Any | None]] = {u: {v: None for v in vertices} for u in vertices}
|
|
160
|
+
|
|
161
|
+
for v in vertices:
|
|
162
|
+
dist[v][v] = 0.0
|
|
163
|
+
|
|
164
|
+
for u in vertices:
|
|
165
|
+
for v in graph.get_neighbors(u):
|
|
166
|
+
w = graph.get_weight(u, v)
|
|
167
|
+
if w < dist[u][v]:
|
|
168
|
+
dist[u][v] = w
|
|
169
|
+
nxt[u][v] = v
|
|
170
|
+
|
|
171
|
+
for k in vertices:
|
|
172
|
+
for i in vertices:
|
|
173
|
+
for j in vertices:
|
|
174
|
+
if dist[i][k] != math.inf and dist[k][j] != math.inf:
|
|
175
|
+
new_dist = dist[i][k] + dist[k][j]
|
|
176
|
+
if new_dist < dist[i][j]:
|
|
177
|
+
dist[i][j] = new_dist
|
|
178
|
+
nxt[i][j] = nxt[i][k]
|
|
179
|
+
|
|
180
|
+
for v in vertices:
|
|
181
|
+
if dist[v][v] < 0:
|
|
182
|
+
raise NegativeCycleError()
|
|
183
|
+
|
|
184
|
+
return dist, nxt
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def reconstruct_path(predecessors: dict[Any, Any | None], source: Any, target: Any) -> list[Any]:
|
|
188
|
+
"""Reconstruct a shortest path from a predecessors dictionary.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
predecessors : dict[Any, Any | None]
|
|
193
|
+
Predecessor map from Dijkstra or Bellman-Ford.
|
|
194
|
+
source : Any
|
|
195
|
+
Starting vertex.
|
|
196
|
+
target : Any
|
|
197
|
+
Target vertex.
|
|
198
|
+
|
|
199
|
+
Returns
|
|
200
|
+
-------
|
|
201
|
+
list[Any]
|
|
202
|
+
The path from *source* to *target* as a list of vertices.
|
|
203
|
+
|
|
204
|
+
Raises
|
|
205
|
+
------
|
|
206
|
+
NoPathError
|
|
207
|
+
If no path exists.
|
|
208
|
+
"""
|
|
209
|
+
path: list[Any] = []
|
|
210
|
+
v = target
|
|
211
|
+
while v is not None:
|
|
212
|
+
path.append(v)
|
|
213
|
+
v = predecessors[v]
|
|
214
|
+
path.reverse()
|
|
215
|
+
if path[0] != source:
|
|
216
|
+
raise NoPathError(source, target)
|
|
217
|
+
return path
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def reconstruct_path_fw(
|
|
221
|
+
next_nodes: dict[Any, dict[Any, Any | None]], source: Any, target: Any
|
|
222
|
+
) -> list[Any]:
|
|
223
|
+
"""Reconstruct a shortest path from Floyd-Warshall's next_nodes table.
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
next_nodes : dict[Any, dict[Any, Any | None]]
|
|
228
|
+
Next-node map from :func:`floyd_warshall`.
|
|
229
|
+
source : Any
|
|
230
|
+
Starting vertex.
|
|
231
|
+
target : Any
|
|
232
|
+
Target vertex.
|
|
233
|
+
|
|
234
|
+
Returns
|
|
235
|
+
-------
|
|
236
|
+
list[Any]
|
|
237
|
+
The path from *source* to *target*.
|
|
238
|
+
|
|
239
|
+
Raises
|
|
240
|
+
------
|
|
241
|
+
NoPathError
|
|
242
|
+
If no path exists.
|
|
243
|
+
"""
|
|
244
|
+
if next_nodes[source][target] is None and source != target:
|
|
245
|
+
raise NoPathError(source, target)
|
|
246
|
+
path: list[Any] = [source]
|
|
247
|
+
while source != target:
|
|
248
|
+
source = next_nodes[source][target]
|
|
249
|
+
path.append(source)
|
|
250
|
+
return path
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.graphs.topo_sort
|
|
3
|
+
=========================
|
|
4
|
+
Topological sort algorithms: Kahn's algorithm and DFS-based sort.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections import deque, defaultdict
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pkstruct.graphs.graph import Graph
|
|
13
|
+
from pkstruct.graphs.exceptions import InvalidGraphOperationError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def topological_sort_kahn(graph: Graph) -> list[Any]:
|
|
17
|
+
"""Topological sort using Kahn's algorithm (BFS-based).
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
graph : Graph
|
|
22
|
+
A directed graph.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
list[Any]
|
|
27
|
+
Vertices in topological order.
|
|
28
|
+
|
|
29
|
+
Raises
|
|
30
|
+
------
|
|
31
|
+
InvalidGraphOperationError
|
|
32
|
+
If the graph is undirected or contains a cycle.
|
|
33
|
+
"""
|
|
34
|
+
if not graph.is_directed():
|
|
35
|
+
raise InvalidGraphOperationError("Topological sort requires a directed graph.")
|
|
36
|
+
|
|
37
|
+
in_degree: dict[Any, int] = {v: 0 for v in graph}
|
|
38
|
+
for u in graph:
|
|
39
|
+
for v in graph.get_neighbors(u):
|
|
40
|
+
in_degree[v] += 1
|
|
41
|
+
|
|
42
|
+
queue: deque[Any] = deque([v for v in graph if in_degree[v] == 0])
|
|
43
|
+
result: list[Any] = []
|
|
44
|
+
|
|
45
|
+
while queue:
|
|
46
|
+
v = queue.popleft()
|
|
47
|
+
result.append(v)
|
|
48
|
+
for neighbor in graph.get_neighbors(v):
|
|
49
|
+
in_degree[neighbor] -= 1
|
|
50
|
+
if in_degree[neighbor] == 0:
|
|
51
|
+
queue.append(neighbor)
|
|
52
|
+
|
|
53
|
+
if len(result) != len(list(graph)):
|
|
54
|
+
raise InvalidGraphOperationError("Graph contains a cycle; topological sort not possible.")
|
|
55
|
+
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def topological_sort_dfs(graph: Graph) -> list[Any]:
|
|
60
|
+
"""Topological sort using DFS-based algorithm.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
graph : Graph
|
|
65
|
+
A directed graph.
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
list[Any]
|
|
70
|
+
Vertices in topological order.
|
|
71
|
+
|
|
72
|
+
Raises
|
|
73
|
+
------
|
|
74
|
+
InvalidGraphOperationError
|
|
75
|
+
If the graph is undirected or contains a cycle.
|
|
76
|
+
"""
|
|
77
|
+
if not graph.is_directed():
|
|
78
|
+
raise InvalidGraphOperationError("Topological sort requires a directed graph.")
|
|
79
|
+
|
|
80
|
+
WHITE, GRAY, BLACK = 0, 1, 2
|
|
81
|
+
color: dict[Any, int] = {v: WHITE for v in graph}
|
|
82
|
+
result: list[Any] = []
|
|
83
|
+
has_cycle: bool = False
|
|
84
|
+
|
|
85
|
+
def _dfs(v: Any) -> None:
|
|
86
|
+
nonlocal has_cycle
|
|
87
|
+
if has_cycle:
|
|
88
|
+
return
|
|
89
|
+
color[v] = GRAY
|
|
90
|
+
for neighbor in graph.get_neighbors(v):
|
|
91
|
+
if color[neighbor] == GRAY:
|
|
92
|
+
has_cycle = True
|
|
93
|
+
return
|
|
94
|
+
if color[neighbor] == WHITE:
|
|
95
|
+
_dfs(neighbor)
|
|
96
|
+
color[v] = BLACK
|
|
97
|
+
result.append(v)
|
|
98
|
+
|
|
99
|
+
for v in graph:
|
|
100
|
+
if color[v] == WHITE:
|
|
101
|
+
_dfs(v)
|
|
102
|
+
if has_cycle:
|
|
103
|
+
raise InvalidGraphOperationError(
|
|
104
|
+
"Graph contains a cycle; topological sort not possible."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
result.reverse()
|
|
108
|
+
return result
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.graphs.traversal
|
|
3
|
+
=========================
|
|
4
|
+
Graph traversal algorithms: BFS and DFS.
|
|
5
|
+
|
|
6
|
+
All functions operate on any ``Graph``-like object that provides
|
|
7
|
+
``get_neighbors(v)`` and ``has_vertex(v)``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections import deque
|
|
13
|
+
from collections.abc import Iterator
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from pkstruct.graphs.graph import Graph
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def bfs(graph: Graph, start: Any) -> list[Any]:
|
|
20
|
+
"""Breadth-first search returning vertices in visitation order.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
graph : Graph
|
|
25
|
+
The graph to traverse.
|
|
26
|
+
start : Any
|
|
27
|
+
The starting vertex.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
list[Any]
|
|
32
|
+
Vertices in BFS order.
|
|
33
|
+
|
|
34
|
+
Raises
|
|
35
|
+
------
|
|
36
|
+
VertexNotFoundError
|
|
37
|
+
If *start* is not in the graph.
|
|
38
|
+
"""
|
|
39
|
+
if not graph.has_vertex(start):
|
|
40
|
+
from pkstruct.graphs.exceptions import VertexNotFoundError
|
|
41
|
+
|
|
42
|
+
raise VertexNotFoundError(start)
|
|
43
|
+
|
|
44
|
+
visited: set[Any] = set()
|
|
45
|
+
result: list[Any] = []
|
|
46
|
+
queue: deque[Any] = deque([start])
|
|
47
|
+
visited.add(start)
|
|
48
|
+
|
|
49
|
+
while queue:
|
|
50
|
+
v = queue.popleft()
|
|
51
|
+
result.append(v)
|
|
52
|
+
for neighbor in graph.get_neighbors(v):
|
|
53
|
+
if neighbor not in visited:
|
|
54
|
+
visited.add(neighbor)
|
|
55
|
+
queue.append(neighbor)
|
|
56
|
+
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def dfs(graph: Graph, start: Any) -> list[Any]:
|
|
61
|
+
"""Depth-first search returning vertices in visitation order.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
graph : Graph
|
|
66
|
+
The graph to traverse.
|
|
67
|
+
start : Any
|
|
68
|
+
The starting vertex.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
list[Any]
|
|
73
|
+
Vertices in DFS order.
|
|
74
|
+
|
|
75
|
+
Raises
|
|
76
|
+
------
|
|
77
|
+
VertexNotFoundError
|
|
78
|
+
If *start* is not in the graph.
|
|
79
|
+
"""
|
|
80
|
+
if not graph.has_vertex(start):
|
|
81
|
+
from pkstruct.graphs.exceptions import VertexNotFoundError
|
|
82
|
+
|
|
83
|
+
raise VertexNotFoundError(start)
|
|
84
|
+
|
|
85
|
+
visited: set[Any] = set()
|
|
86
|
+
result: list[Any] = []
|
|
87
|
+
_dfs_recursive(graph, start, visited, result)
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _dfs_recursive(graph: Graph, v: Any, visited: set[Any], result: list[Any]) -> None:
|
|
92
|
+
visited.add(v)
|
|
93
|
+
result.append(v)
|
|
94
|
+
for neighbor in graph.get_neighbors(v):
|
|
95
|
+
if neighbor not in visited:
|
|
96
|
+
_dfs_recursive(graph, neighbor, visited, result)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def bfs_paths(graph: Graph, start: Any, goal: Any) -> list[list[Any]]:
|
|
100
|
+
"""Return all shortest paths from *start* to *goal* using BFS.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
graph : Graph
|
|
105
|
+
The graph to search.
|
|
106
|
+
start : Any
|
|
107
|
+
Starting vertex.
|
|
108
|
+
goal : Any
|
|
109
|
+
Target vertex.
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
list[list[Any]]
|
|
114
|
+
All shortest paths from start to goal (may be multiple if
|
|
115
|
+
there are multiple shortest paths).
|
|
116
|
+
"""
|
|
117
|
+
queue: deque[list[Any]] = deque([[start]])
|
|
118
|
+
result: list[list[Any]] = []
|
|
119
|
+
shortest: int | None = None
|
|
120
|
+
|
|
121
|
+
while queue:
|
|
122
|
+
path = queue.popleft()
|
|
123
|
+
v = path[-1]
|
|
124
|
+
if v == goal:
|
|
125
|
+
if shortest is None:
|
|
126
|
+
shortest = len(path)
|
|
127
|
+
if len(path) == shortest:
|
|
128
|
+
result.append(path)
|
|
129
|
+
continue
|
|
130
|
+
if shortest is not None and len(path) >= shortest:
|
|
131
|
+
continue
|
|
132
|
+
for neighbor in graph.get_neighbors(v):
|
|
133
|
+
if neighbor not in path:
|
|
134
|
+
queue.append(path + [neighbor])
|
|
135
|
+
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def dfs_paths(graph: Graph, start: Any, goal: Any) -> list[list[Any]]:
|
|
140
|
+
"""Return all paths from *start* to *goal* using DFS.
|
|
141
|
+
|
|
142
|
+
Parameters
|
|
143
|
+
----------
|
|
144
|
+
graph : Graph
|
|
145
|
+
The graph to search.
|
|
146
|
+
start : Any
|
|
147
|
+
Starting vertex.
|
|
148
|
+
goal : Any
|
|
149
|
+
Target vertex.
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
list[list[Any]]
|
|
154
|
+
All paths from start to goal.
|
|
155
|
+
"""
|
|
156
|
+
result: list[list[Any]] = []
|
|
157
|
+
_dfs_paths_recursive(graph, start, goal, [start], result)
|
|
158
|
+
return result
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _dfs_paths_recursive(
|
|
162
|
+
graph: Graph,
|
|
163
|
+
v: Any,
|
|
164
|
+
goal: Any,
|
|
165
|
+
path: list[Any],
|
|
166
|
+
result: list[list[Any]],
|
|
167
|
+
) -> None:
|
|
168
|
+
if v == goal:
|
|
169
|
+
result.append(list(path))
|
|
170
|
+
return
|
|
171
|
+
for neighbor in graph.get_neighbors(v):
|
|
172
|
+
if neighbor not in path:
|
|
173
|
+
path.append(neighbor)
|
|
174
|
+
_dfs_paths_recursive(graph, neighbor, goal, path, result)
|
|
175
|
+
path.pop()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.graphs.visualization
|
|
3
|
+
=============================
|
|
4
|
+
ASCII visualization utilities for graphs.
|
|
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.exceptions import VertexNotFoundError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def visualize(graph: Graph, show_weights: bool = True) -> str:
|
|
16
|
+
"""Return an ASCII representation of the graph.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
graph : Graph
|
|
21
|
+
The graph to visualize.
|
|
22
|
+
show_weights : bool, default=True
|
|
23
|
+
If *True*, display edge weights.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
str
|
|
28
|
+
Multi-line ASCII art of the graph.
|
|
29
|
+
"""
|
|
30
|
+
lines: list[str] = []
|
|
31
|
+
lines.append(
|
|
32
|
+
f"Graph (directed={graph.is_directed()}, vertices={graph.order()}, edges={graph.edge_count()})"
|
|
33
|
+
)
|
|
34
|
+
lines.append("")
|
|
35
|
+
|
|
36
|
+
if graph.is_empty():
|
|
37
|
+
lines.append("(empty)")
|
|
38
|
+
return "\n".join(lines)
|
|
39
|
+
|
|
40
|
+
for v in graph:
|
|
41
|
+
neighbors = graph.get_neighbors(v)
|
|
42
|
+
if not neighbors:
|
|
43
|
+
lines.append(f" {v!r} -> (isolated)")
|
|
44
|
+
else:
|
|
45
|
+
parts: list[str] = []
|
|
46
|
+
for n in neighbors:
|
|
47
|
+
w = graph.get_weight(v, n)
|
|
48
|
+
if show_weights:
|
|
49
|
+
parts.append(f"{n!r} [{w}]")
|
|
50
|
+
else:
|
|
51
|
+
parts.append(f"{n!r}")
|
|
52
|
+
arrow = " <-> " if not graph.is_directed() else " -> "
|
|
53
|
+
lines.append(f" {v!r} -> {arrow.join(parts)}")
|
|
54
|
+
|
|
55
|
+
return "\n".join(lines)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def adjacency_matrix(graph: Graph) -> str:
|
|
59
|
+
"""Return the adjacency matrix as a formatted string.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
graph : Graph
|
|
64
|
+
The graph to render.
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
str
|
|
69
|
+
String representation of the adjacency matrix.
|
|
70
|
+
"""
|
|
71
|
+
vertices = list(graph)
|
|
72
|
+
n = len(vertices)
|
|
73
|
+
if n == 0:
|
|
74
|
+
return "(empty)"
|
|
75
|
+
|
|
76
|
+
idx = {v: i for i, v in enumerate(vertices)}
|
|
77
|
+
matrix: list[list[str]] = [["."] * n for _ in range(n)]
|
|
78
|
+
|
|
79
|
+
for u in vertices:
|
|
80
|
+
for v in graph.get_neighbors(u):
|
|
81
|
+
w = graph.get_weight(u, v)
|
|
82
|
+
matrix[idx[u]][idx[v]] = str(w)
|
|
83
|
+
|
|
84
|
+
header = " " + " ".join(f"{v!r:>4}" for v in vertices)
|
|
85
|
+
rows: list[str] = [header]
|
|
86
|
+
for i, v in enumerate(vertices):
|
|
87
|
+
row = f"{v!r:>3} " + " ".join(f"{matrix[i][j]:>4}" for j in range(n))
|
|
88
|
+
rows.append(row)
|
|
89
|
+
|
|
90
|
+
return "\n".join(rows)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.graphs.weighted
|
|
3
|
+
========================
|
|
4
|
+
Weighted graph convenience class and weighted-graph utility functions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pkstruct.graphs.graph import Graph
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WeightedGraph(Graph):
|
|
15
|
+
"""A weighted undirected graph (convenience subclass of ``Graph``).
|
|
16
|
+
|
|
17
|
+
Every edge carries a numeric weight (default ``1.0``).
|
|
18
|
+
Useful as a shorthand when all edges are expected to have meaningful
|
|
19
|
+
weights (e.g., for shortest-path or MST algorithms).
|
|
20
|
+
|
|
21
|
+
Example
|
|
22
|
+
-------
|
|
23
|
+
>>> g = WeightedGraph()
|
|
24
|
+
>>> g.add_edge("A", "B", 4.2)
|
|
25
|
+
>>> g.add_edge("B", "C", 2.7)
|
|
26
|
+
>>> g.get_weight("A", "B")
|
|
27
|
+
4.2
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def add_edge(self, u: Any, v: Any, weight: float = 1.0) -> None:
|
|
31
|
+
super().add_edge(u, v, weight)
|
|
32
|
+
|
|
33
|
+
def __repr__(self) -> str:
|
|
34
|
+
with self._lock:
|
|
35
|
+
vertices = list(self._adj.keys())
|
|
36
|
+
edges = self.get_edges()
|
|
37
|
+
return f"WeightedGraph(vertices={len(vertices)}, edges={len(edges)})"
|