Skip to content

Commit 0ea5012

Browse files
Merge branch 'master' into feat/hashing-advanced-algorithms
2 parents 69c996a + 8106aea commit 0ea5012

File tree

6 files changed

+171
-7
lines changed

6 files changed

+171
-7
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 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.7
22+
rev: v0.14.10
2323
hooks:
2424
- id: ruff-check
2525
- id: ruff-format
@@ -50,7 +50,7 @@ repos:
5050
- id: validate-pyproject
5151

5252
- repo: https://github.com/pre-commit/mirrors-mypy
53-
rev: v1.19.0
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)

ciphers/caesar_cipher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def encrypt(input_string: str, key: int, alphabet: str | None = None) -> str:
4545
And our shift is ``2``
4646
4747
We can then encode the message, one letter at a time. ``H`` would become ``J``,
48-
since ``J`` is two letters away, and so on. If the shift is ever two large, or
48+
since ``J`` is two letters away, and so on. If the shift is ever too large, or
4949
our letter is at the end of the alphabet, we just start at the beginning
5050
(``Z`` would shift to ``a`` then ``b`` and so on).
5151
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

strings/reverse_words.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
def reverse_words(input_str: str) -> str:
2-
"""
3-
Reverses words in a given string
1+
def reverse_words(sentence: str) -> str:
2+
"""Reverse the order of words in a given string.
3+
4+
Extra whitespace between words is ignored.
5+
46
>>> reverse_words("I love Python")
57
'Python love I'
68
>>> reverse_words("I Love Python")
79
'Python Love I'
810
"""
9-
return " ".join(input_str.split()[::-1])
11+
return " ".join(sentence.split()[::-1])
1012

1113

1214
if __name__ == "__main__":

0 commit comments

Comments
 (0)