Skip to content

Commit 9881514

Browse files
Merge branch 'master' into trapped_rainwater_space_optimized
2 parents 3ab07d1 + ca5b8c1 commit 9881514

File tree

5 files changed

+240
-3
lines changed

5 files changed

+240
-3
lines changed

.pre-commit-config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ repos:
1919
- id: auto-walrus
2020

2121
- repo: https://github.com/astral-sh/ruff-pre-commit
22-
rev: v0.14.3
22+
rev: v0.14.10
2323
hooks:
2424
- id: ruff-check
2525
- id: ruff-format
@@ -32,7 +32,7 @@ repos:
3232
- tomli
3333

3434
- repo: https://github.com/tox-dev/pyproject-fmt
35-
rev: v2.11.0
35+
rev: v2.11.1
3636
hooks:
3737
- id: pyproject-fmt
3838

@@ -50,7 +50,7 @@ repos:
5050
- id: validate-pyproject
5151

5252
- repo: https://github.com/pre-commit/mirrors-mypy
53-
rev: v1.18.2
53+
rev: v1.19.1
5454
hooks:
5555
- id: mypy
5656
args:

DIRECTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@
398398
* [Minimum Squares To Represent A Number](dynamic_programming/minimum_squares_to_represent_a_number.py)
399399
* [Minimum Steps To One](dynamic_programming/minimum_steps_to_one.py)
400400
* [Minimum Tickets Cost](dynamic_programming/minimum_tickets_cost.py)
401+
* [Narcissistic Number](dynamic_programming/narcissistic_number.py)
401402
* [Optimal Binary Search Tree](dynamic_programming/optimal_binary_search_tree.py)
402403
* [Palindrome Partitioning](dynamic_programming/palindrome_partitioning.py)
403404
* [Range Sum Query](dynamic_programming/range_sum_query.py)
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""
2+
Find all narcissistic numbers up to a given limit using dynamic programming.
3+
4+
A narcissistic number (also known as an Armstrong number or plus perfect number)
5+
is a number that is the sum of its own digits each raised to the power of the
6+
number of digits.
7+
8+
For example, 153 is a narcissistic number because 153 = 1^3 + 5^3 + 3^3.
9+
10+
This implementation uses dynamic programming with memoization to efficiently
11+
compute digit powers and find all narcissistic numbers up to a specified limit.
12+
13+
The DP optimization caches digit^power calculations. When searching through many
14+
numbers, the same digit power calculations occur repeatedly (e.g., 153, 351, 135
15+
all need 1^3, 5^3, 3^3). Memoization avoids these redundant calculations.
16+
17+
Examples of narcissistic numbers:
18+
Single digit: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
19+
Three digit: 153, 370, 371, 407
20+
Four digit: 1634, 8208, 9474
21+
Five digit: 54748, 92727, 93084
22+
23+
Reference: https://en.wikipedia.org/wiki/Narcissistic_number
24+
"""
25+
26+
27+
def find_narcissistic_numbers(limit: int) -> list[int]:
28+
"""
29+
Find all narcissistic numbers up to the given limit using dynamic programming.
30+
31+
This function uses memoization to cache digit power calculations, avoiding
32+
redundant computations across different numbers with the same digit count.
33+
34+
Args:
35+
limit: The upper bound for searching narcissistic numbers (exclusive)
36+
37+
Returns:
38+
list[int]: A sorted list of all narcissistic numbers below the limit
39+
40+
Examples:
41+
>>> find_narcissistic_numbers(10)
42+
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
43+
>>> find_narcissistic_numbers(160)
44+
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 153]
45+
>>> find_narcissistic_numbers(400)
46+
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 153, 370, 371]
47+
>>> find_narcissistic_numbers(1000)
48+
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 153, 370, 371, 407]
49+
>>> find_narcissistic_numbers(10000)
50+
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 153, 370, 371, 407, 1634, 8208, 9474]
51+
>>> find_narcissistic_numbers(1)
52+
[0]
53+
>>> find_narcissistic_numbers(0)
54+
[]
55+
"""
56+
if limit <= 0:
57+
return []
58+
59+
narcissistic_nums = []
60+
61+
# Memoization: cache[(power, digit)] = digit^power
62+
# This avoids recalculating the same power for different numbers
63+
power_cache: dict[tuple[int, int], int] = {}
64+
65+
def get_digit_power(digit: int, power: int) -> int:
66+
"""Get digit^power using memoization (DP optimization)."""
67+
if (power, digit) not in power_cache:
68+
power_cache[(power, digit)] = digit**power
69+
return power_cache[(power, digit)]
70+
71+
# Check each number up to the limit
72+
for number in range(limit):
73+
# Count digits
74+
num_digits = len(str(number))
75+
76+
# Calculate sum of powered digits using memoized powers
77+
remaining = number
78+
digit_sum = 0
79+
while remaining > 0:
80+
digit = remaining % 10
81+
digit_sum += get_digit_power(digit, num_digits)
82+
remaining //= 10
83+
84+
# Check if narcissistic
85+
if digit_sum == number:
86+
narcissistic_nums.append(number)
87+
88+
return narcissistic_nums
89+
90+
91+
if __name__ == "__main__":
92+
import doctest
93+
94+
doctest.testmod()
95+
96+
# Demonstrate the dynamic programming approach
97+
print("Finding all narcissistic numbers up to 10000:")
98+
print("(Using memoization to cache digit power calculations)")
99+
print()
100+
101+
narcissistic_numbers = find_narcissistic_numbers(10000)
102+
print(f"Found {len(narcissistic_numbers)} narcissistic numbers:")
103+
print(narcissistic_numbers)

other/sliding_window_maximum.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from collections import deque
2+
3+
4+
def sliding_window_maximum(numbers: list[int], window_size: int) -> list[int]:
5+
"""
6+
Return a list containing the maximum of each sliding window of size window_size.
7+
8+
This implementation uses a monotonic deque to achieve O(n) time complexity.
9+
10+
Args:
11+
numbers: List of integers representing the input array.
12+
window_size: Size of the sliding window (must be positive).
13+
14+
Returns:
15+
List of maximum values for each valid window.
16+
17+
Raises:
18+
ValueError: If window_size is not a positive integer.
19+
20+
Time Complexity: O(n) - each element is added and removed at most once
21+
Space Complexity: O(k) - deque stores at most window_size indices
22+
23+
Examples:
24+
>>> sliding_window_maximum([1, 3, -1, -3, 5, 3, 6, 7], 3)
25+
[3, 3, 5, 5, 6, 7]
26+
>>> sliding_window_maximum([9, 11], 2)
27+
[11]
28+
>>> sliding_window_maximum([], 3)
29+
[]
30+
>>> sliding_window_maximum([4, 2, 12, 3], 1)
31+
[4, 2, 12, 3]
32+
>>> sliding_window_maximum([1], 1)
33+
[1]
34+
"""
35+
if window_size <= 0:
36+
raise ValueError("Window size must be a positive integer")
37+
if not numbers:
38+
return []
39+
40+
result: list[int] = []
41+
index_deque: deque[int] = deque()
42+
43+
for current_index, current_value in enumerate(numbers):
44+
# Remove the element which is out of this window
45+
if index_deque and index_deque[0] == current_index - window_size:
46+
index_deque.popleft()
47+
48+
# Remove useless elements (smaller than current) from back
49+
while index_deque and numbers[index_deque[-1]] < current_value:
50+
index_deque.pop()
51+
52+
index_deque.append(current_index)
53+
54+
# Start adding to result once we have a full window
55+
if current_index >= window_size - 1:
56+
result.append(numbers[index_deque[0]])
57+
58+
return result

searches/binary_search.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,81 @@ def binary_search_std_lib(sorted_collection: list[int], item: int) -> int:
243243
return -1
244244

245245

246+
def binary_search_with_duplicates(sorted_collection: list[int], item: int) -> list[int]:
247+
"""Pure implementation of a binary search algorithm in Python that supports
248+
duplicates.
249+
250+
Resources used:
251+
https://stackoverflow.com/questions/13197552/using-binary-search-with-sorted-array-with-duplicates
252+
253+
The collection must be sorted in ascending order; otherwise the result will be
254+
unpredictable. If the target appears multiple times, this function returns a
255+
list of all indexes where the target occurs. If the target is not found,
256+
this function returns an empty list.
257+
258+
:param sorted_collection: some ascending sorted collection with comparable items
259+
:param item: item value to search for
260+
:return: a list of indexes where the item is found (empty list if not found)
261+
262+
Examples:
263+
>>> binary_search_with_duplicates([0, 5, 7, 10, 15], 0)
264+
[0]
265+
>>> binary_search_with_duplicates([0, 5, 7, 10, 15], 15)
266+
[4]
267+
>>> binary_search_with_duplicates([1, 2, 2, 2, 3], 2)
268+
[1, 2, 3]
269+
>>> binary_search_with_duplicates([1, 2, 2, 2, 3], 4)
270+
[]
271+
"""
272+
if list(sorted_collection) != sorted(sorted_collection):
273+
raise ValueError("sorted_collection must be sorted in ascending order")
274+
275+
def lower_bound(sorted_collection: list[int], item: int) -> int:
276+
"""
277+
Returns the index of the first element greater than or equal to the item.
278+
279+
:param sorted_collection: The sorted list to search.
280+
:param item: The item to find the lower bound for.
281+
:return: The index where the item can be inserted while maintaining order.
282+
"""
283+
left = 0
284+
right = len(sorted_collection)
285+
while left < right:
286+
midpoint = left + (right - left) // 2
287+
current_item = sorted_collection[midpoint]
288+
if current_item < item:
289+
left = midpoint + 1
290+
else:
291+
right = midpoint
292+
return left
293+
294+
def upper_bound(sorted_collection: list[int], item: int) -> int:
295+
"""
296+
Returns the index of the first element strictly greater than the item.
297+
298+
:param sorted_collection: The sorted list to search.
299+
:param item: The item to find the upper bound for.
300+
:return: The index where the item can be inserted after all existing instances.
301+
"""
302+
left = 0
303+
right = len(sorted_collection)
304+
while left < right:
305+
midpoint = left + (right - left) // 2
306+
current_item = sorted_collection[midpoint]
307+
if current_item <= item:
308+
left = midpoint + 1
309+
else:
310+
right = midpoint
311+
return left
312+
313+
left = lower_bound(sorted_collection, item)
314+
right = upper_bound(sorted_collection, item)
315+
316+
if left == len(sorted_collection) or sorted_collection[left] != item:
317+
return []
318+
return list(range(left, right))
319+
320+
246321
def binary_search_by_recursion(
247322
sorted_collection: list[int], item: int, left: int = 0, right: int = -1
248323
) -> int:

0 commit comments

Comments
 (0)