pizza-lib 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.
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: pizza-lib
3
+ Version: 0.1.0
4
+ Summary: Data structure and algorithm helpers for LeetCode-style Python: grids, adjacency, binary search bounds, heaps, trees, linked lists.
5
+ Author: Shyal
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/shyal/pydsa
8
+ Project-URL: Repository, https://github.com/shyal/pydsa
9
+ Keywords: leetcode,algorithms,data-structures,grid,graph,heap,binary-search
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Topic :: Software Development :: Libraries
13
+ Classifier: Topic :: Education
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Provides-Extra: draw
18
+ Requires-Dist: rich; extra == "draw"
19
+ Requires-Dist: colorama; extra == "draw"
20
+ Requires-Dist: PrettyPrintTree; extra == "draw"
21
+ Requires-Dist: graphviz; extra == "draw"
22
+ Provides-Extra: test
23
+ Requires-Dist: pytest; extra == "test"
24
+ Dynamic: license-file
25
+
26
+ # pydsa
27
+
28
+ Data structure and algorithm helpers for LeetCode-style Python: the
29
+ functions one keeps rewriting at the top of a solution, written once and
30
+ tested.
31
+
32
+ ```
33
+ pip install pizza-lib # the import name is pydsa
34
+ ```
35
+
36
+ ```python
37
+ from pydsa import table, cells, nbrs, first_true, Multiset, maxheappush
38
+
39
+ dp = table(3, 4) # a 3 by 4 grid of zeros
40
+ for i, j in cells(grid): # every cell, row by row
41
+ for ni, nj in nbrs(grid, i, j): # its on-grid neighbours
42
+ ...
43
+ first_true(1, 10**9, lambda x: x * x >= n) # the first x in [1, 10^9] with ok(x)
44
+ ```
45
+
46
+ ## Modules
47
+
48
+ | Module | Names |
49
+ | --- | --- |
50
+ | `grid` | `table`, `like`, `shape`, `cells`, `nbrs`, `in_bounds`, `is_edge`, `edges`, `put`, `row`, `col`, `set_row`, `set_col`, `grid_bfs`, `CARDINALS`, `DIAGONALS`, `ALL_EIGHT` |
51
+ | `adj` | `adjacency`, `indegrees`, `levels`, `components`, `Graph`, `graph`, `dijkstra` |
52
+ | `search` | `first_true`, `last_true`, `first_false`, `last_false`, `min_chunks`, `viz_binary_search` |
53
+ | `heaps` | `maxheappush`, `maxheappop`, `maxheapify`, `maxheappushpop`, `maxheapreplace`, `maxheappeek`, `maxheapitems`, `heap`, `draw_heap` |
54
+ | `counting` | `Multiset`, `count_pairs` |
55
+ | `combo` | `pairs`, `triples` |
56
+ | `digits` | `to_digits`, `to_int`, `even`, `odd` |
57
+ | `arith` | `ceil_div`, `batched` |
58
+ | `seq` | `lcs` |
59
+ | `generators` | `as_list` |
60
+ | `nodes` | `TreeNode`, `ListNode`, `GraphNode`, `Node` |
61
+ | `tree` | `build_tree`, `get_level_order`, `find_node`, `get_inorder`, `is_balanced`, `build_nary_tree`, `build_general_tree`, `get_general_tree`, `generate_random_tree`, `generate_full_binary_tree`, `draw_tree`, `draw_general_tree` |
62
+ | `bst` | `insert`, `generate_random_bst`, `tree_to_list`, `is_valid_bst`, `generate_and_print_random_bst` |
63
+ | `linked` | `build_linked_list`, `get_list_values`, `print_linked_list`, `linked_list_reverse_k`, `draw_linked_list` |
64
+ | `graphs` | `build_graph`, `get_adj_list`, `build_graph_from_edge_list`, `as_dict_of_dicts`, `is_edge_list`, `draw_graph`, `draw_graphviz`, `draw_ascii_graph` |
65
+
66
+ Every public name is importable from the package itself. The package
67
+ imports only the standard library. The `draw_*` functions, `tabulate`-style
68
+ output and `viz_binary_search` import rich, colorama, PrettyPrintTree and
69
+ graphviz when first called:
70
+
71
+ ```
72
+ pip install 'pizza-lib[draw]'
73
+ ```
74
+
75
+ Every function has a docstring with its contract; the tests in `tests/`
76
+ are the examples.
77
+
78
+ ## Where it comes from
79
+
80
+ These helpers were the harness of the `leet` repo, preloaded into
81
+ `builtins` for every solve there, and the library that the `mu` language
82
+ pastes into the Python it emits. Both still use them from here.
83
+
84
+ ## Tests
85
+
86
+ ```
87
+ pip install -e '.[test]'
88
+ pytest
89
+ ```
@@ -0,0 +1,22 @@
1
+ pizza_lib-0.1.0.dist-info/licenses/LICENSE,sha256=vOQUZGMtzHZUlEUJS8CIA0smrZ10pfWGVC4fRC4qBcs,1062
2
+ pydsa/__init__.py,sha256=aAtJdSX_fcZsEwzwaTiEGUACy221kJ9BZMZ1dWISOik,942
3
+ pydsa/_treeformat.py,sha256=XiIh8kyEk0QFcyHKvzZQUL-Yz1bzPSULSl7MkBTiNBw,8871
4
+ pydsa/adj.py,sha256=jCnqZNUML2Qbrh_wOGumcnIG5VAjLu2JLEIjaycRcDQ,5192
5
+ pydsa/arith.py,sha256=a-QnwA7kuJIg6A6xGXyKQP8sDOLszaNMeOHwgBQOiig,591
6
+ pydsa/bst.py,sha256=ar51gVTepAKDid-ACWJUih-btZlY1I2c0NQLy4ADrNo,2144
7
+ pydsa/combo.py,sha256=rV6LNm2Q_bb6AcF0HDby1EyIKN5mB6YIot84_Q1BAGc,1228
8
+ pydsa/counting.py,sha256=uo4t2oi0iyPEN2Q3SuxFZ1gq3AG7S57p-qDXgcy4Apk,2046
9
+ pydsa/digits.py,sha256=GnbbGELQH9N8qWdwBLvr59neEtG3EG0giwUaGSUk2Co,1493
10
+ pydsa/generators.py,sha256=J-uZ1Bd4xVO5lj0Dbd5GT1IhrVsW1SQv9aDxmAuz9gA,549
11
+ pydsa/graphs.py,sha256=5pYlDB9SGAYPuymkq8dq6gXmyvfQv7UMT7kq0eHs8lY,13842
12
+ pydsa/grid.py,sha256=iWLN-eFJ7vC_6cT_XnoB2Li2rY3KNGvKjDR4zwvkJ9w,6736
13
+ pydsa/heaps.py,sha256=mRKk9uvfsvaCU0cm2s4kd1EGLSkIzfRjU7Sbs3gP0GQ,3273
14
+ pydsa/linked.py,sha256=CoRkhdSvEV4rxHJxGLKCtqSGeuk8LrDtTdPhFiforeM,1864
15
+ pydsa/nodes.py,sha256=moX0ROFsHx81qlfPTtkFw6XDxp5KrytFabIsENJLTH8,1907
16
+ pydsa/search.py,sha256=lXkzhZDQST3Dasaih8Pto4TvzItrRg5SMS-IQE4M8DQ,8282
17
+ pydsa/seq.py,sha256=yiVNM2s91qYDaPKr0__M4oDswXjzzRwpOqIjW_bk98Q,942
18
+ pydsa/tree.py,sha256=bBJbTiLW2IDGdWiKQFDiDlv57lF0FZYYnvo3JQqfhqM,9564
19
+ pizza_lib-0.1.0.dist-info/METADATA,sha256=ryXhtaqCPQYcp4QZ3hdWaRtaViOsJUdzoTk_nmB80-w,3660
20
+ pizza_lib-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
21
+ pizza_lib-0.1.0.dist-info/top_level.txt,sha256=lv_hZp9lUdb6dd-5ucCTCquBfyW9892YzMJgaIkoaeM,6
22
+ pizza_lib-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shyal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ pydsa
pydsa/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """pydsa: data structures and algorithm helpers for LeetCode-style Python.
2
+
3
+ Every public name of every module is importable from the package. The
4
+ package imports only the standard library; the drawing functions import
5
+ rich, colorama, PrettyPrintTree and graphviz when first called
6
+ (`pip install pydsa[draw]`).
7
+ """
8
+
9
+ from .adj import * # noqa: F401,F403
10
+ from .arith import * # noqa: F401,F403
11
+ from .bst import * # noqa: F401,F403
12
+ from .combo import * # noqa: F401,F403
13
+ from .counting import * # noqa: F401,F403
14
+ from .digits import * # noqa: F401,F403
15
+ from .generators import * # noqa: F401,F403
16
+ from .graphs import * # noqa: F401,F403
17
+ from .grid import * # noqa: F401,F403
18
+ from .heaps import * # noqa: F401,F403
19
+ from .linked import * # noqa: F401,F403
20
+ from .nodes import * # noqa: F401,F403
21
+ from .search import * # noqa: F401,F403
22
+ from .seq import * # noqa: F401,F403
23
+ from .tree import * # noqa: F401,F403
24
+
25
+ __version__ = "0.1.0"
pydsa/_treeformat.py ADDED
@@ -0,0 +1,224 @@
1
+ from typing import Any, Callable, Generic, Iterable, TypeVar
2
+
3
+ from PrettyPrint.PrintTree.HorizontalTree import (
4
+ add_parent as add_parent_left,
5
+ )
6
+ from PrettyPrint.PrintTree.HorizontalTree import (
7
+ join_vertically,
8
+ )
9
+ from PrettyPrint.PrintTree.VerticalTree import (
10
+ add_parent as add_parent_top,
11
+ )
12
+ from PrettyPrint.PrintTree.VerticalTree import (
13
+ join_horizontally,
14
+ )
15
+ from PrettyPrint.Utils.NodeFormatter import NodeFormatter
16
+ from PrettyPrint.Utils.Orientation import Orientation
17
+ from PrettyPrint.Utils.StyleAwareUtils import trim_text
18
+
19
+ T = TypeVar("T")
20
+
21
+ # ANSI escape codes for connector foreground colors
22
+ RED = "\033[31m"
23
+ BLUE = "\033[34m"
24
+ RESET = "\033[0m"
25
+
26
+
27
+ class TreeFormatter(Generic[T]):
28
+ def __init__(
29
+ self,
30
+ get_children: Callable[[T], Iterable[T]],
31
+ get_val: Callable[[T], Any],
32
+ get_label: Callable[[T], Any] | None,
33
+ label_color: str,
34
+ show_newline_literal: bool,
35
+ newline_literal: str,
36
+ trim: int,
37
+ trim_symbol: str,
38
+ start_message: Callable[[T], str] | None,
39
+ color: str,
40
+ border: bool,
41
+ max_depth: int,
42
+ orientation: bool,
43
+ ):
44
+ self.get_children = get_children
45
+ self.get_node_val = get_val
46
+ self.get_label = get_label
47
+ self.label_color = label_color
48
+ self.trim = trim
49
+ self.trim_symbol = trim_symbol
50
+ self.show_newline = show_newline_literal
51
+ self.newline_literal = newline_literal
52
+ self.start_message = start_message
53
+ self.color = color
54
+ self.border = border
55
+ self.max_depth = max_depth
56
+ self.orientation = orientation
57
+
58
+ def format(self, node: T) -> str:
59
+ if self.orientation == Orientation.Vertical:
60
+ res = self.tree_vertical_join(node)
61
+ else:
62
+ res = self.tree_horizontal_join(node)
63
+ res = res.to_str().rstrip()
64
+ if self.start_message:
65
+ return f"{ self.start_message(node) }\n{ res }"
66
+ return res
67
+
68
+ def tree_vertical_join(
69
+ self, tree_node: T, depth: int = 0, is_left: bool | None = None
70
+ ) -> NodeFormatter:
71
+ """
72
+ tree_node: actual user tree node (T)
73
+ returns: NodeFormatter for the subtree rooted at tree_node
74
+ is_left: whether tree_node itself is a left child of its parent (used to color the connector to its parent)
75
+ """
76
+ label = self.get_label(tree_node) if self.get_label else None
77
+
78
+ # Get raw children as provided by get_children (may omit None)
79
+ children_raw = list(self.get_children(tree_node))
80
+
81
+ # Create NodeFormatter for current node (styles/colors/trim etc.)
82
+ node = self.add_styles(tree_node)
83
+
84
+ if children_raw and (self.max_depth == -1 or depth < self.max_depth):
85
+ # Build formatted children and remember whether each child is left/right
86
+ children_with_flags = []
87
+ for i, child in enumerate(children_raw):
88
+ # Determine is_left flag correctly for binary trees:
89
+ # - If two children are present, index 0 is left, index 1 is right.
90
+ # - If only one child is present, try to inspect tree_node.left/right attributes.
91
+ if len(children_raw) == 2:
92
+ child_is_left = i == 0
93
+ elif len(children_raw) == 1:
94
+ left_attr = getattr(tree_node, "left", object())
95
+ right_attr = getattr(tree_node, "right", object())
96
+ if child is left_attr:
97
+ child_is_left = True
98
+ elif child is right_attr:
99
+ child_is_left = False
100
+ else:
101
+ # Fallback (best-effort): treat single child as left if we can't detect
102
+ child_is_left = i == 0
103
+ else:
104
+ # No children (shouldn't reach here because children_raw is truthy)
105
+ child_is_left = None
106
+
107
+ child_fmt = self.tree_vertical_join(
108
+ child, depth + 1, is_left=child_is_left
109
+ )
110
+ children_with_flags.append((child_fmt, child_is_left))
111
+
112
+ if len(children_with_flags) == 1:
113
+ # Single-child case: insert a colored '|' connector above the child
114
+ child_node_fmt, child_is_left = children_with_flags[0]
115
+ if child_is_left is True:
116
+ edge_str = f"{RED}/{RESET}"
117
+ elif child_is_left is False:
118
+ edge_str = f"{BLUE}\\{RESET}"
119
+ else:
120
+ edge_str = "|"
121
+ child_node_fmt.lines.insert(
122
+ 0, " " * child_node_fmt.get_middle_width() + edge_str
123
+ )
124
+ children_node = child_node_fmt
125
+ else:
126
+ # Two children: join them horizontally (we rely on join_horizontally for placement)
127
+ children_node = join_horizontally(
128
+ [fmt for fmt, _ in children_with_flags]
129
+ )
130
+
131
+ node = add_parent_top(node, children_node)
132
+
133
+ node = self.add_label(label, node, add_parent_top, "|")
134
+ return node
135
+
136
+ def tree_horizontal_join(
137
+ self, tree_node: T, depth: int = 0, is_left: bool | None = None
138
+ ) -> NodeFormatter:
139
+ """
140
+ Horizontal join variant (similar logic to vertical).
141
+ """
142
+ label = self.get_label(tree_node) if self.get_label else None
143
+ children_raw = list(self.get_children(tree_node))
144
+ node = self.add_styles(tree_node)
145
+ node_padding = " " * node.width
146
+ node.lines = node.lines + [node_padding]
147
+ node.height += 1
148
+
149
+ if children_raw and (self.max_depth == -1 or depth < self.max_depth):
150
+ children_with_flags = []
151
+ for i, child in enumerate(children_raw):
152
+ if len(children_raw) == 2:
153
+ child_is_left = i == 0
154
+ elif len(children_raw) == 1:
155
+ left_attr = getattr(tree_node, "left", object())
156
+ right_attr = getattr(tree_node, "right", object())
157
+ if child is left_attr:
158
+ child_is_left = True
159
+ elif child is right_attr:
160
+ child_is_left = False
161
+ else:
162
+ child_is_left = i == 0
163
+ else:
164
+ child_is_left = None
165
+
166
+ child_fmt = self.tree_horizontal_join(
167
+ child, depth + 1, is_left=child_is_left
168
+ )
169
+ children_with_flags.append((child_fmt, child_is_left))
170
+
171
+ if len(children_with_flags) == 1:
172
+ children_node, child_is_left = children_with_flags[0]
173
+ middle = children_node.get_middle_height()
174
+ new_lines = []
175
+ for r_idx, line in enumerate(children_node.lines):
176
+ if r_idx == middle:
177
+ if child_is_left is True:
178
+ edge_str = f"{RED}─{RESET}"
179
+ elif child_is_left is False:
180
+ edge_str = f"{BLUE}─{RESET}"
181
+ else:
182
+ edge_str = "─"
183
+ new_lines.append(edge_str + line)
184
+ else:
185
+ new_lines.append(" " + line)
186
+ children_node.lines = new_lines
187
+ else:
188
+ children_node = join_vertically([fmt for fmt, _ in children_with_flags])
189
+
190
+ node = add_parent_left(node, children_node)
191
+
192
+ node = self.add_label(label, node, add_parent_left, "─")
193
+ return node
194
+
195
+ def add_label(
196
+ self,
197
+ label: Any,
198
+ node: NodeFormatter,
199
+ parent_adder: Callable[[NodeFormatter, NodeFormatter], NodeFormatter],
200
+ seperator: str,
201
+ ) -> NodeFormatter:
202
+ if label:
203
+ label = NodeFormatter.from_string(str(label))
204
+ if self.label_color:
205
+ label.color_bg(self.label_color, True)
206
+ node = parent_adder(NodeFormatter.from_string(seperator), node)
207
+ node = parent_adder(label, node)
208
+ return node
209
+
210
+ def add_styles(self, tree_node: T) -> NodeFormatter:
211
+ """
212
+ Build and return a NodeFormatter for the given tree node (apply trim, newline literal, background color, border).
213
+ """
214
+ contents = str(self.get_node_val(tree_node))
215
+ if self.show_newline:
216
+ contents = contents.replace("\n", self.newline_literal)
217
+ if self.trim != -1:
218
+ contents = trim_text(contents, self.trim, self.trim_symbol)
219
+ node = NodeFormatter.from_string(contents)
220
+ if self.border:
221
+ node.add_border()
222
+ if self.color:
223
+ node.color_bg(self.color, add_space=not self.border)
224
+ return node
pydsa/adj.py ADDED
@@ -0,0 +1,166 @@
1
+ from collections import defaultdict
2
+ from heapq import heappop, heappush
3
+ from math import inf
4
+ from typing import Any, Deque, Iterator, List, Optional, Set, Tuple
5
+
6
+ from .grid import _holds, table
7
+
8
+
9
+ def adjacency(
10
+ edges: List[Any],
11
+ n: Optional[int] = None,
12
+ reverse: bool = False,
13
+ directed: bool = True,
14
+ weighted: bool = False,
15
+ ) -> Any:
16
+ """Adjacency from an edge list. Each edge is [a, b] or, with weighted,
17
+ [a, b, w]. Unweighted returns a dict of lists in edge order; weighted a
18
+ dict of dicts, adj[a][b] = w. With n, nodes 0 to n - 1 are all keys,
19
+ isolated ones with an empty list. reverse reads each edge as b to a.
20
+ directed=False adds both directions."""
21
+ adj: Any = defaultdict(dict) if weighted else defaultdict(list)
22
+ for c in range(n or 0):
23
+ adj[c]
24
+ for e in edges:
25
+ a, b = e[:2]
26
+ if reverse:
27
+ a, b = b, a
28
+ if weighted:
29
+ adj[a][b] = e[2]
30
+ if not directed:
31
+ adj[b][a] = e[2]
32
+ else:
33
+ adj[a].append(b)
34
+ if not directed:
35
+ adj[b].append(a)
36
+ return adj
37
+
38
+
39
+ def indegrees(
40
+ edges: List[Any],
41
+ n: Optional[int] = None,
42
+ reverse: bool = False,
43
+ directed: bool = True,
44
+ type: Any = defaultdict,
45
+ ) -> Any:
46
+ """In-degree of every node from an edge list, each edge [a, b] counting
47
+ one into b. Default is a defaultdict(int), any node reading 0; with n,
48
+ nodes 0 to n - 1 are all keys. type=list needs n and returns a plain
49
+ list. reverse reads each edge as b to a. directed=False counts the edge
50
+ into a as well, so the result is the plain degree of every node."""
51
+ indeg: Any
52
+ if type is list:
53
+ if n is None:
54
+ raise ValueError("type=list needs n")
55
+ indeg = table(n)
56
+ else:
57
+ indeg = defaultdict(int)
58
+ for c in range(n or 0):
59
+ indeg[c]
60
+ for e in edges:
61
+ a, b = e[:2]
62
+ if reverse:
63
+ a, b = b, a
64
+ indeg[b] += 1
65
+ if not directed:
66
+ indeg[a] += 1
67
+ return indeg
68
+
69
+
70
+ def levels(
71
+ q: Deque[Any],
72
+ grid: Optional[Any] = None,
73
+ seen: Optional[Set[Any]] = None,
74
+ grouped: bool = False,
75
+ eq: Any = None,
76
+ lt: Any = None,
77
+ lte: Any = None,
78
+ gt: Any = None,
79
+ gte: Any = None,
80
+ ) -> Iterator[Tuple[int, Any]]:
81
+ """Level-order BFS over a deque the caller keeps pushing onto.
82
+
83
+ Yields (d, item) for every item popped, where d is the level the item
84
+ was pushed at, starting from 0 for the items already in q. The caller
85
+ appends the next level's items to q inside the loop body. With grouped=True
86
+ yields (d, items) once per level, items being the whole level as a list.
87
+
88
+ Every popped item is checked before it is yielded. With eq, lt, lte, gt or
89
+ gte, an item whose value fails one of them is dropped; the value is
90
+ grid[item[0]][item[1]] when grid is given, else the item itself. With
91
+ seen, an item already in seen is dropped and a kept item is added to it,
92
+ so the caller can push without checking.
93
+ """
94
+
95
+ def keep(x: Any) -> bool:
96
+ v = x if grid is None else grid[x[0]][x[1]]
97
+ if not _holds(v, eq, lt, lte, gt, gte) or (seen is not None and x in seen):
98
+ return False
99
+ if seen is not None:
100
+ seen.add(x)
101
+ return True
102
+
103
+ d = 0
104
+ while q:
105
+ popped = (q.popleft() for _ in range(len(q)))
106
+ if grouped:
107
+ level = [x for x in popped if keep(x)]
108
+ if level:
109
+ yield d, level
110
+ else:
111
+ for x in popped:
112
+ if keep(x):
113
+ yield d, x
114
+ d += 1
115
+
116
+
117
+ def components(vertices, edges):
118
+ """The connected components of a graph given as its vertices and a
119
+ function edges(u) listing u's neighbours, each as a list of vertices."""
120
+ left, out = set(vertices), []
121
+ while left:
122
+ stack, comp = [left.pop()], []
123
+ while stack:
124
+ u = stack.pop()
125
+ comp.append(u)
126
+ for v in edges(u):
127
+ if v in left:
128
+ left.remove(v)
129
+ stack.append(v)
130
+ out.append(comp)
131
+ return out
132
+
133
+
134
+ class Graph:
135
+ """Vertices and weighted adjacency lists, adj[u] = [(v, w), ...]. An
136
+ edge (u, v) weighs 1; an edge (u, v, w) weighs w."""
137
+
138
+ def __init__(self, vertices, edges, directed=False):
139
+ self.vertices = list(vertices)
140
+ self.adj = {u: [] for u in self.vertices}
141
+ for e in edges:
142
+ u, v, w = e if len(e) == 3 else (*e, 1)
143
+ self.adj[u].append((v, w))
144
+ if not directed:
145
+ self.adj[v].append((u, w))
146
+
147
+
148
+ def graph(vertices, edges, directed=False):
149
+ """A Graph of these vertices and edges."""
150
+ return Graph(vertices, edges, directed)
151
+
152
+
153
+ def dijkstra(g, src):
154
+ """Shortest distance from src to every vertex of g, inf when unreachable."""
155
+ dist = {u: inf for u in g.vertices}
156
+ dist[src] = 0
157
+ pq = [(0, src)]
158
+ while pq:
159
+ d, u = heappop(pq)
160
+ if d > dist[u]:
161
+ continue
162
+ for v, w in g.adj[u]:
163
+ if d + w < dist[v]:
164
+ dist[v] = d + w
165
+ heappush(pq, (d + w, v))
166
+ return dist
pydsa/arith.py ADDED
@@ -0,0 +1,19 @@
1
+ """Integer arithmetic helpers."""
2
+
3
+ from itertools import zip_longest
4
+ from typing import Any, List, Sequence
5
+
6
+
7
+ def ceil_div(a: int, b: int) -> int:
8
+ """a / b rounded up, for a >= 0 and b > 0."""
9
+ if b <= 0:
10
+ raise ValueError("Denominator b must be positive.")
11
+ if a < 0:
12
+ raise ValueError("Numerator a must be non-negative.")
13
+ return (a + b - 1) // b
14
+
15
+
16
+ def batched(s: Sequence[Any], n: int = 1) -> List[Any]:
17
+ """s cut into pieces of n; the last piece holds what is left."""
18
+ r = list(range(0, len(s), n))
19
+ return [s[a:b] for a, b in zip_longest(r, r[1:])]
pydsa/bst.py ADDED
@@ -0,0 +1,78 @@
1
+ import random
2
+ from collections import deque
3
+ from typing import List, Optional
4
+
5
+ from .nodes import TreeNode
6
+ from .tree import draw_tree
7
+
8
+
9
+ def insert(root: Optional[TreeNode], val: int) -> TreeNode:
10
+ """Insert a value into the BST and return the root."""
11
+ if not root:
12
+ return TreeNode(val)
13
+ if val < root.val:
14
+ root.left = insert(root.left, val)
15
+ elif val > root.val:
16
+ root.right = insert(root.right, val)
17
+ # If val == root.val, ignore (no duplicates in this BST)
18
+ return root
19
+
20
+
21
+ def generate_random_bst(n: int, seed: Optional[int] = None) -> Optional[TreeNode]:
22
+ """Generate a random BST with values 1..n inserted in random order."""
23
+ if n <= 0:
24
+ return None
25
+ if seed:
26
+ random.seed(seed)
27
+ values = list(range(1, n + 1))
28
+ random.shuffle(values)
29
+ root = None
30
+ for val in values:
31
+ root = insert(root, val)
32
+ return root
33
+
34
+
35
+ def tree_to_list(root: Optional[TreeNode]) -> List[Optional[int]]:
36
+ """Convert BST to list using level-order traversal."""
37
+ if not root:
38
+ return []
39
+ result: List[Optional[int]] = []
40
+ queue = deque([root])
41
+ while queue:
42
+ node = queue.popleft()
43
+ if node is not None:
44
+ result.append(node.val)
45
+ queue.append(node.left)
46
+ queue.append(node.right)
47
+ else:
48
+ result.append(None)
49
+ # Trim trailing Nones
50
+ while result and result[-1] is None:
51
+ result.pop()
52
+ return result
53
+
54
+
55
+ def is_valid_bst(root: Optional[TreeNode]) -> bool:
56
+ """Check if the tree is a valid BST using inorder traversal."""
57
+
58
+ def inorder(node):
59
+ if not node:
60
+ return []
61
+ return inorder(node.left) + [node.val] + inorder(node.right)
62
+
63
+ vals = inorder(root)
64
+ return all(vals[i] < vals[i + 1] for i in range(len(vals) - 1))
65
+
66
+
67
+ def generate_and_print_random_bst(
68
+ n: int, seed=None, verbose=True
69
+ ) -> Optional[TreeNode]:
70
+ root = generate_random_bst(n, seed)
71
+ valid = is_valid_bst(root)
72
+ assert valid
73
+ if verbose:
74
+ draw_tree(root)
75
+ lst = tree_to_list(root)
76
+ print(lst)
77
+ print("Valid BST:", valid)
78
+ return root