algorithm-discovery-engine 1.0.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.
ads/__init__.py ADDED
@@ -0,0 +1,85 @@
1
+ """Algorithms & Data Structures solving engine (Python).
2
+
3
+ Two tiers are provided for every problem:
4
+
5
+ * :mod:`ads.problems` / :mod:`ads.structures` — the *regular* tier:
6
+ readable, deliberately simple implementations that mirror the Java, C++
7
+ and Rust ports 1:1.
8
+ * :mod:`ads.problems_advanced` / :mod:`ads.structures_advanced` — the
9
+ *advanced* tier: memory- and speed-optimized variants (rolling DP,
10
+ in-place sorts, slot-allocated nodes, manual heaps).
11
+
12
+ A single catalogue (``catalog/problems.json`` at the repo root) holds the
13
+ shared test vectors used by all four languages.
14
+ """
15
+
16
+ from ads.problems import (
17
+ binary_search,
18
+ edit_distance,
19
+ graph_bfs,
20
+ graph_dfs,
21
+ knapsack_01,
22
+ lcs,
23
+ max_subarray,
24
+ merge_sort,
25
+ quick_sort,
26
+ two_sum,
27
+ )
28
+ from ads.problems_advanced import (
29
+ binary_search_advanced,
30
+ edit_distance_advanced,
31
+ graph_bfs_advanced,
32
+ graph_dfs_advanced,
33
+ knapsack_01_advanced,
34
+ lcs_advanced,
35
+ max_subarray_advanced,
36
+ merge_sort_advanced,
37
+ quick_sort_advanced,
38
+ two_sum_advanced,
39
+ )
40
+ from ads.structures import BST, LinkedList, MinHeap, Queue, Stack, Trie
41
+ from ads.structures_advanced import (
42
+ BSTAdvanced,
43
+ LinkedListAdvanced,
44
+ MinHeapAdvanced,
45
+ QueueAdvanced,
46
+ StackAdvanced,
47
+ TrieAdvanced,
48
+ )
49
+
50
+ __version__ = "1.0.0"
51
+
52
+ __all__ = [
53
+ "BST",
54
+ "BSTAdvanced",
55
+ "LinkedList",
56
+ "LinkedListAdvanced",
57
+ "MinHeap",
58
+ "MinHeapAdvanced",
59
+ "Queue",
60
+ "QueueAdvanced",
61
+ "Stack",
62
+ "StackAdvanced",
63
+ "Trie",
64
+ "TrieAdvanced",
65
+ "binary_search",
66
+ "binary_search_advanced",
67
+ "edit_distance",
68
+ "edit_distance_advanced",
69
+ "graph_bfs",
70
+ "graph_bfs_advanced",
71
+ "graph_dfs",
72
+ "graph_dfs_advanced",
73
+ "knapsack_01",
74
+ "knapsack_01_advanced",
75
+ "lcs",
76
+ "lcs_advanced",
77
+ "max_subarray",
78
+ "max_subarray_advanced",
79
+ "merge_sort",
80
+ "merge_sort_advanced",
81
+ "quick_sort",
82
+ "quick_sort_advanced",
83
+ "two_sum",
84
+ "two_sum_advanced",
85
+ ]
ads/benchmark.py ADDED
@@ -0,0 +1,84 @@
1
+ """Cross-language benchmark workload (mirrors Java/C++/Rust benches).
2
+
3
+ Fixed seed + fixed sizes so `engine/runner.py bench` can tabulate per-algorithm
4
+ wall time. Run directly with `python -m ads.benchmark`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import random
10
+ import time
11
+ from collections.abc import Callable
12
+
13
+ from ads import problems, problems_advanced
14
+
15
+
16
+ def _random_ints(count: int, seed: int) -> list[int]:
17
+ rng = random.Random(seed)
18
+ return [rng.randrange(1_000_000_000) for _ in range(count)]
19
+
20
+
21
+ def _random_string(length: int, seed: int) -> str:
22
+ rng = random.Random(seed)
23
+ return "".join(rng.choice("abcdefgh") for _ in range(length))
24
+
25
+
26
+ def _chain_graph(size: int) -> list[list[int]]:
27
+ return [
28
+ [neighbor for neighbor in (i - 1, i + 1) if 0 <= neighbor < size]
29
+ for i in range(size)
30
+ ]
31
+
32
+
33
+ def _time_it(label: str, task: Callable[[], object]) -> None:
34
+ started = time.perf_counter()
35
+ task()
36
+ elapsed_us = (time.perf_counter() - started) * 1_000_000
37
+ print(f"{label}: {elapsed_us:.0f} us")
38
+
39
+
40
+ def run() -> None:
41
+ data = _random_ints(200_000, 42)
42
+ sorted_data = list(range(1_000_000))
43
+
44
+ _time_it("merge_sort", lambda: problems.merge_sort(data))
45
+ _time_it("quick_sort", lambda: problems.quick_sort(_random_ints(200_000, 42)))
46
+ _time_it(
47
+ "max_subarray",
48
+ lambda: problems_advanced.max_subarray_advanced(_random_ints(500_000, 7)),
49
+ )
50
+ _time_it("two_sum", lambda: problems.two_sum(_random_ints(50_000, 9), -1))
51
+ _time_it(
52
+ "binary_search",
53
+ lambda: problems.binary_search(sorted_data, sorted_data[len(sorted_data) // 2]),
54
+ )
55
+ _time_it(
56
+ "lcs",
57
+ lambda: problems_advanced.lcs_advanced(
58
+ _random_string(2_000, 11), _random_string(2_000, 13)
59
+ ),
60
+ )
61
+ _time_it(
62
+ "knapsack_01",
63
+ lambda: problems_advanced.knapsack_01_advanced(
64
+ 5_000, _random_ints(1_000, 21), _random_ints(1_000, 31)
65
+ ),
66
+ )
67
+ _time_it(
68
+ "edit_distance",
69
+ lambda: problems_advanced.edit_distance_advanced(
70
+ _random_string(1_000, 41), _random_string(1_000, 43)
71
+ ),
72
+ )
73
+ _time_it(
74
+ "graph_bfs",
75
+ lambda: problems_advanced.graph_bfs_advanced(_chain_graph(100_000), 0, 99_999),
76
+ )
77
+ _time_it(
78
+ "graph_dfs",
79
+ lambda: problems_advanced.graph_dfs_advanced(_chain_graph(100_000), 0, 99_999),
80
+ )
81
+
82
+
83
+ if __name__ == "__main__":
84
+ run()
ads/problems.py ADDED
@@ -0,0 +1,190 @@
1
+ """Regular Python tier: readable baseline implementations.
2
+
3
+ These mirror the Java / C++ / Rust ports 1:1 and favour clarity over raw
4
+ performance. The advanced tier (:mod:`ads.problems_advanced`) provides the
5
+ memory/speed-optimized variants.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections import deque
11
+
12
+
13
+ def two_sum(nums: list[int], target: int) -> list[int]:
14
+ """Indices of the two numbers summing to *target* (or ``[-1, -1]``)."""
15
+ seen: dict[int, int] = {}
16
+ for i, value in enumerate(nums):
17
+ complement = target - value
18
+ if complement in seen:
19
+ j = seen[complement]
20
+ return [j, i] if j < i else [i, j]
21
+ seen[value] = i
22
+ return [-1, -1]
23
+
24
+
25
+ def binary_search(sorted_nums: list[int], target: int) -> int:
26
+ """Index of the first occurrence of *target* (lower bound), or ``-1``."""
27
+ low, high = 0, len(sorted_nums)
28
+ while low < high:
29
+ mid = (low + high) // 2
30
+ if sorted_nums[mid] < target:
31
+ low = mid + 1
32
+ else:
33
+ high = mid
34
+ if low < len(sorted_nums) and sorted_nums[low] == target:
35
+ return low
36
+ return -1
37
+
38
+
39
+ def _merge(left: list[int], right: list[int]) -> list[int]:
40
+ merged: list[int] = []
41
+ i = j = 0
42
+ while i < len(left) and j < len(right):
43
+ if left[i] <= right[j]:
44
+ merged.append(left[i])
45
+ i += 1
46
+ else:
47
+ merged.append(right[j])
48
+ j += 1
49
+ merged.extend(left[i:])
50
+ merged.extend(right[j:])
51
+ return merged
52
+
53
+
54
+ def merge_sort(nums: list[int]) -> list[int]:
55
+ """Return a new array sorted ascending (classic merge sort)."""
56
+ if len(nums) <= 1:
57
+ return list(nums)
58
+ mid = len(nums) // 2
59
+ return _merge(merge_sort(nums[:mid]), merge_sort(nums[mid:]))
60
+
61
+
62
+ def _partition(nums: list[int], low: int, high: int) -> int:
63
+ pivot = nums[high]
64
+ i = low
65
+ for j in range(low, high):
66
+ if nums[j] < pivot:
67
+ nums[i], nums[j] = nums[j], nums[i]
68
+ i += 1
69
+ nums[i], nums[high] = nums[high], nums[i]
70
+ return i
71
+
72
+
73
+ def quick_sort(nums: list[int]) -> list[int]:
74
+ """Return a new array sorted ascending (in-place quick sort behind the scenes)."""
75
+ values = list(nums)
76
+
77
+ def sort_region(low: int, high: int) -> None:
78
+ if low >= high:
79
+ return
80
+ split = _partition(values, low, high)
81
+ sort_region(low, split - 1)
82
+ sort_region(split + 1, high)
83
+
84
+ sort_region(0, len(values) - 1)
85
+ return values
86
+
87
+
88
+ def max_subarray(nums: list[int]) -> int:
89
+ """Maximum contiguous subarray sum (Kadane). Empty -> 0."""
90
+ best = 0 if not nums else nums[0]
91
+ running = 0
92
+ for value in nums:
93
+ running = max(value, running + value)
94
+ best = max(best, running)
95
+ return best
96
+
97
+
98
+ def lcs(a: str, b: str) -> int:
99
+ """Longest common subsequence length (full-table DP)."""
100
+ rows, cols = len(a) + 1, len(b) + 1
101
+ table = [[0] * cols for _ in range(rows)]
102
+ for i in range(1, rows):
103
+ for j in range(1, cols):
104
+ if a[i - 1] == b[j - 1]:
105
+ table[i][j] = table[i - 1][j - 1] + 1
106
+ else:
107
+ table[i][j] = max(table[i - 1][j], table[i][j - 1])
108
+ return table[rows - 1][cols - 1]
109
+
110
+
111
+ def knapsack_01(capacity: int, weights: list[int], values: list[int]) -> int:
112
+ """Maximum value with the 0/1 knapsack constraint (full-table DP)."""
113
+ dp = [[0] * (capacity + 1) for _ in range(len(weights) + 1)]
114
+ for item in range(1, len(weights) + 1):
115
+ weight, value = weights[item - 1], values[item - 1]
116
+ row_before, row_now = dp[item - 1], dp[item]
117
+ for cap in range(capacity + 1):
118
+ if weight <= cap:
119
+ row_now[cap] = max(row_before[cap], row_before[cap - weight] + value)
120
+ else:
121
+ row_now[cap] = row_before[cap]
122
+ return dp[len(weights)][capacity]
123
+
124
+
125
+ def edit_distance(a: str, b: str) -> int:
126
+ """Levenshtein distance (full-table DP): insert/delete/substitute."""
127
+ rows, cols = len(a) + 1, len(b) + 1
128
+ table = [[0] * cols for _ in range(rows)]
129
+ for i in range(rows):
130
+ table[i][0] = i
131
+ for j in range(cols):
132
+ table[0][j] = j
133
+ for i in range(1, rows):
134
+ for j in range(1, cols):
135
+ cost = 0 if a[i - 1] == b[j - 1] else 1
136
+ table[i][j] = min(
137
+ table[i - 1][j] + 1,
138
+ table[i][j - 1] + 1,
139
+ table[i - 1][j - 1] + cost,
140
+ )
141
+ return table[rows - 1][cols - 1]
142
+
143
+
144
+ def _parse_adjacency(encoded: str) -> list[list[int]]:
145
+ adj: list[list[int]] = []
146
+ for row in encoded.split(";"):
147
+ parts = row.split(":")
148
+ neighbors: list[int] = []
149
+ if parts[1].strip():
150
+ neighbors = [int(n) for n in filter(None, parts[1].split())]
151
+ adj.append(neighbors)
152
+ return adj
153
+
154
+
155
+ def graph_bfs(adj: list[list[int]], start: int, target: int) -> int:
156
+ """Shortest path length in edges from *start* to *target*, ``-1`` unreachable."""
157
+ if start == target:
158
+ return 0
159
+ if start >= len(adj):
160
+ return -1
161
+ distances = [-1] * len(adj)
162
+ distances[start] = 0
163
+ pending = deque([start])
164
+ while pending:
165
+ node = pending.popleft()
166
+ for neighbor in adj[node]:
167
+ if distances[neighbor] != -1:
168
+ continue
169
+ distances[neighbor] = distances[node] + 1
170
+ if neighbor == target:
171
+ return distances[neighbor]
172
+ pending.append(neighbor)
173
+ return -1
174
+
175
+
176
+ def graph_dfs(adj: list[list[int]], start: int, target: int) -> bool:
177
+ """Whether *target* is reachable from *start* (iterative depth-first search)."""
178
+
179
+ def visit(node: int, seen: set[int] | None = None) -> bool:
180
+ seen = set() if seen is None else seen
181
+ if node == target:
182
+ return True
183
+ seen.add(node)
184
+ return any(
185
+ neighbor not in seen and visit(neighbor, seen) for neighbor in adj[node]
186
+ )
187
+
188
+ if not adj and start != target:
189
+ return False
190
+ return visit(start)
@@ -0,0 +1,203 @@
1
+ """Advanced Python tier: performance-optimized variants.
2
+
3
+ Each function is behaviorally equivalent to its :mod:`ads.problems`
4
+ counterpart but trades verbosity for lower constant factors and better
5
+ space usage (rolling DP rows, in-place sorts, iterative graph walks).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from bisect import bisect_left
11
+ from collections import deque
12
+
13
+
14
+ def two_sum_advanced(nums: list[int], target: int) -> list[int]:
15
+ """Two-sum with a single pass and early exit (same contract)."""
16
+ seen: dict[int, int] = {}
17
+ for i, value in enumerate(nums):
18
+ other = target - value
19
+ if other in seen:
20
+ j = seen[other]
21
+ return [min(j, i), max(j, i)]
22
+ seen[value] = i
23
+ return [-1, -1]
24
+
25
+
26
+ def binary_search_advanced(sorted_nums: list[int], target: int) -> int:
27
+ """Lower-bound binary search delegated to the C-accelerated bisect module."""
28
+ index = bisect_left(sorted_nums, target)
29
+ if index < len(sorted_nums) and sorted_nums[index] == target:
30
+ return index
31
+ return -1
32
+
33
+
34
+ def _merge_into(
35
+ source: list[int], left: int, mid: int, right: int, buffer: list[int]
36
+ ) -> None:
37
+ buffer[left:right] = source[left:right]
38
+ i, j, k = left, mid, left
39
+ while i < mid and j < right:
40
+ if buffer[i] <= buffer[j]:
41
+ source[k] = buffer[i]
42
+ i += 1
43
+ else:
44
+ source[k] = buffer[j]
45
+ j += 1
46
+ k += 1
47
+ while i < mid:
48
+ source[k] = buffer[i]
49
+ i += 1
50
+ k += 1
51
+
52
+
53
+ def merge_sort_advanced(nums: list[int]) -> list[int]:
54
+ """In-place merge sort over a private copy using a single aux buffer."""
55
+ values = list(nums)
56
+ buffer = [0] * len(values)
57
+
58
+ width = 1
59
+ while width < len(values):
60
+ left = 0
61
+ while left < len(values):
62
+ mid = min(left + width, len(values))
63
+ right = min(left + 2 * width, len(values))
64
+ if mid < right:
65
+ _merge_into(values, left, mid, right, buffer)
66
+ left += 2 * width
67
+ width *= 2
68
+ return values
69
+
70
+
71
+ def quick_sort_advanced(nums: list[int]) -> list[int]:
72
+ """Iterative in-place quick sort with median-of-three pivot (Lomuto)."""
73
+ values = list(nums)
74
+ stack: list[tuple[int, int]] = [(0, len(values) - 1)]
75
+
76
+ def median_of_three(lo: int, hi: int) -> int:
77
+ mid = (lo + hi) // 2
78
+ a, b, c = values[lo], values[mid], values[hi]
79
+ if (a < b) != (a < c):
80
+ return lo
81
+ if (b < a) != (b < c):
82
+ return mid
83
+ return hi
84
+
85
+ while stack:
86
+ low, high = stack.pop()
87
+ while low < high:
88
+ pivot_index = median_of_three(low, high)
89
+ values[pivot_index], values[high] = values[high], values[pivot_index]
90
+ pivot = values[high]
91
+ i = low
92
+ for j in range(low, high):
93
+ if values[j] < pivot:
94
+ values[i], values[j] = values[j], values[i]
95
+ i += 1
96
+ values[i], values[high] = values[high], values[i]
97
+ if i - low < high - i:
98
+ stack.append((i + 1, high))
99
+ high = i - 1
100
+ else:
101
+ stack.append((low, i - 1))
102
+ low = i + 1
103
+ return values
104
+
105
+
106
+ def max_subarray_advanced(nums: list[int]) -> int:
107
+ """Kadane with stream-friendly fold (same contract, no pre-scan)."""
108
+ best: int | None = None
109
+ running = 0
110
+ for value in nums:
111
+ running = max(value, running + value)
112
+ best = running if best is None else max(best, running)
113
+ return 0 if best is None else best
114
+
115
+
116
+ def lcs_advanced(a: str, b: str) -> int:
117
+ """LCS with O(min(m, n)) space via two rolling rows."""
118
+ if len(a) < len(b):
119
+ a, b = b, a
120
+ previous = [0] * (len(b) + 1)
121
+ for i in range(1, len(a) + 1):
122
+ current = [0] * (len(b) + 1)
123
+ for j in range(1, len(b) + 1):
124
+ if a[i - 1] == b[j - 1]:
125
+ current[j] = previous[j - 1] + 1
126
+ else:
127
+ up, left = previous[j], current[j - 1]
128
+ current[j] = up if up >= left else left
129
+ previous = current
130
+ return previous[len(b)]
131
+
132
+
133
+ def knapsack_01_advanced(capacity: int, weights: list[int], values: list[int]) -> int:
134
+ """0/1 knapsack with O(capacity) space via a single rolling row."""
135
+ row = [0] * (capacity + 1)
136
+ for weight, value in zip(weights, values, strict=False):
137
+ if weight > capacity:
138
+ continue
139
+ for cap in range(capacity, weight - 1, -1):
140
+ candidate = row[cap - weight] + value
141
+ if candidate > row[cap]:
142
+ row[cap] = candidate
143
+ return row[capacity]
144
+
145
+
146
+ def edit_distance_advanced(a: str, b: str) -> int:
147
+ """Levenshtein distance with O(min(m, n)) space via two rolling rows."""
148
+ if len(a) < len(b):
149
+ a, b = b, a
150
+ previous = list(range(len(b) + 1))
151
+ for i in range(1, len(a) + 1):
152
+ current = [0] * (len(b) + 1)
153
+ current[0] = i
154
+ for j in range(1, len(b) + 1):
155
+ insert_cost = current[j - 1] + 1
156
+ delete_cost = previous[j] + 1
157
+ substitute = previous[j - 1] + (0 if a[i - 1] == b[j - 1] else 1)
158
+ current[j] = min(insert_cost, delete_cost, substitute)
159
+ previous = current
160
+ return previous[len(b)]
161
+
162
+
163
+ def graph_bfs_advanced(adj: list[list[int]], start: int, target: int) -> int:
164
+ """BFS shortest path using a preallocated distance array and deque."""
165
+ if start == target:
166
+ return 0
167
+ if not adj or not (0 <= start < len(adj)):
168
+ return -1 if target != start else 0
169
+ distances = [-1] * len(adj)
170
+ distances[start] = 0
171
+ pending = deque([start])
172
+ while pending:
173
+ node = pending.popleft()
174
+ steps = distances[node] + 1
175
+ for neighbor in adj[node]:
176
+ if distances[neighbor] != -1:
177
+ continue
178
+ distances[neighbor] = steps
179
+ if neighbor == target:
180
+ return steps
181
+ pending.append(neighbor)
182
+ return -1
183
+
184
+
185
+ def graph_dfs_advanced(adj: list[list[int]], start: int, target: int) -> bool:
186
+ """Iterative DFS with an explicit stack (avoids recursion limits)."""
187
+ if not adj:
188
+ return start == target
189
+ if not (0 <= start < len(adj)):
190
+ return False
191
+ visited = [False] * len(adj)
192
+ pending = [start]
193
+ while pending:
194
+ node = pending.pop()
195
+ if node == target:
196
+ return True
197
+ if visited[node]:
198
+ continue
199
+ visited[node] = True
200
+ for neighbor in adj[node]:
201
+ if not visited[neighbor]:
202
+ pending.append(neighbor)
203
+ return False