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.
Files changed (72) hide show
  1. pkstruct/__init__.py +167 -0
  2. pkstruct/graphs/__init__.py +127 -0
  3. pkstruct/graphs/connectivity.py +157 -0
  4. pkstruct/graphs/directed.py +95 -0
  5. pkstruct/graphs/exceptions.py +63 -0
  6. pkstruct/graphs/graph.py +262 -0
  7. pkstruct/graphs/mst.py +118 -0
  8. pkstruct/graphs/scc.py +138 -0
  9. pkstruct/graphs/shortest_path.py +250 -0
  10. pkstruct/graphs/topo_sort.py +108 -0
  11. pkstruct/graphs/traversal.py +175 -0
  12. pkstruct/graphs/visualization.py +90 -0
  13. pkstruct/graphs/weighted.py +37 -0
  14. pkstruct/linear/__init__.py +95 -0
  15. pkstruct/linear/deques/__init__.py +33 -0
  16. pkstruct/linear/deques/deque.py +194 -0
  17. pkstruct/linear/deques/linked_deque.py +198 -0
  18. pkstruct/linear/exceptions.py +26 -0
  19. pkstruct/linear/linked_lists/__init__.py +5 -0
  20. pkstruct/linear/linked_lists/_base.py +608 -0
  21. pkstruct/linear/linked_lists/circular_linked_list.py +230 -0
  22. pkstruct/linear/linked_lists/doubly_linked_list.py +151 -0
  23. pkstruct/linear/linked_lists/nodes.py +68 -0
  24. pkstruct/linear/linked_lists/singly_linked_list.py +136 -0
  25. pkstruct/linear/queues/__init__.py +44 -0
  26. pkstruct/linear/queues/circular_queue.py +258 -0
  27. pkstruct/linear/queues/linked_queue.py +186 -0
  28. pkstruct/linear/queues/priority_queue.py +202 -0
  29. pkstruct/linear/queues/queue.py +174 -0
  30. pkstruct/linear/stacks/__init__.py +38 -0
  31. pkstruct/linear/stacks/array_stack.py +165 -0
  32. pkstruct/linear/stacks/linked_stack.py +168 -0
  33. pkstruct/linear/stacks/stack.py +158 -0
  34. pkstruct/linear/utils/__init__.py +18 -0
  35. pkstruct/linear/utils/benchmark.py +255 -0
  36. pkstruct/linear/utils/debug_tools.py +239 -0
  37. pkstruct/linear/utils/helpers.py +143 -0
  38. pkstruct/linear/utils/iterators.py +148 -0
  39. pkstruct/linear/visualization/__init__.py +0 -0
  40. pkstruct/linear/visualization/ascii_visualizer.py +114 -0
  41. pkstruct/linear/visualization/linked_list_visualizer.py +126 -0
  42. pkstruct/shared/__init__.py +67 -0
  43. pkstruct/shared/benchmarking/__init__.py +78 -0
  44. pkstruct/shared/debugging/__init__.py +69 -0
  45. pkstruct/shared/exceptions/__init__.py +59 -0
  46. pkstruct/shared/serializers/__init__.py +65 -0
  47. pkstruct/shared/threading/__init__.py +43 -0
  48. pkstruct/shared/validators/__init__.py +98 -0
  49. pkstruct/shared/visualization/__init__.py +21 -0
  50. pkstruct/trees/__init__.py +92 -0
  51. pkstruct/trees/avl.py +321 -0
  52. pkstruct/trees/balancing.py +253 -0
  53. pkstruct/trees/bplus.py +425 -0
  54. pkstruct/trees/bst.py +948 -0
  55. pkstruct/trees/btree.py +504 -0
  56. pkstruct/trees/exceptions.py +96 -0
  57. pkstruct/trees/fenwick_tree.py +312 -0
  58. pkstruct/trees/interval_tree.py +541 -0
  59. pkstruct/trees/node.py +356 -0
  60. pkstruct/trees/red_black.py +710 -0
  61. pkstruct/trees/segment_tree.py +398 -0
  62. pkstruct/trees/traversal.py +456 -0
  63. pkstruct/trees/tree_helpers.py +366 -0
  64. pkstruct/trees/utils/__init__.py +15 -0
  65. pkstruct/trees/utils/complexity_helpers.py +231 -0
  66. pkstruct/trees/visualization/__init__.py +0 -0
  67. pkstruct/trees/visualization/ascii_renderer.py +220 -0
  68. pkstruct/trees/visualization/tree_printer.py +129 -0
  69. pkstruct-0.1.0.dist-info/METADATA +482 -0
  70. pkstruct-0.1.0.dist-info/RECORD +72 -0
  71. pkstruct-0.1.0.dist-info/WHEEL +4 -0
  72. 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)})"