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.
- pizza_lib-0.1.0.dist-info/METADATA +89 -0
- pizza_lib-0.1.0.dist-info/RECORD +22 -0
- pizza_lib-0.1.0.dist-info/WHEEL +5 -0
- pizza_lib-0.1.0.dist-info/licenses/LICENSE +21 -0
- pizza_lib-0.1.0.dist-info/top_level.txt +1 -0
- pydsa/__init__.py +25 -0
- pydsa/_treeformat.py +224 -0
- pydsa/adj.py +166 -0
- pydsa/arith.py +19 -0
- pydsa/bst.py +78 -0
- pydsa/combo.py +37 -0
- pydsa/counting.py +64 -0
- pydsa/digits.py +50 -0
- pydsa/generators.py +21 -0
- pydsa/graphs.py +441 -0
- pydsa/grid.py +206 -0
- pydsa/heaps.py +128 -0
- pydsa/linked.py +77 -0
- pydsa/nodes.py +72 -0
- pydsa/search.py +248 -0
- pydsa/seq.py +22 -0
- pydsa/tree.py +299 -0
|
@@ -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,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
|