Skip to content

Commit

Permalink
Merge branch 'TheAlgorithms:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
Ramy-Badr-Ahmed authored Oct 7, 2024
2 parents 252fbeb + dba8eec commit aa16d15
Show file tree
Hide file tree
Showing 13 changed files with 654 additions and 63 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ We want your work to be readable by others; therefore, we encourage you to note

```bash
python3 -m pip install ruff # only required the first time
ruff .
ruff check
```

- Original code submission require docstrings or comments to describe your work.
Expand Down
71 changes: 71 additions & 0 deletions backtracking/word_break.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""
Word Break Problem is a well-known problem in computer science.
Given a string and a dictionary of words, the task is to determine if
the string can be segmented into a sequence of one or more dictionary words.
Wikipedia: https://en.wikipedia.org/wiki/Word_break_problem
"""


def backtrack(input_string: str, word_dict: set[str], start: int) -> bool:
"""
Helper function that uses backtracking to determine if a valid
word segmentation is possible starting from index 'start'.
Parameters:
input_string (str): The input string to be segmented.
word_dict (set[str]): A set of valid dictionary words.
start (int): The starting index of the substring to be checked.
Returns:
bool: True if a valid segmentation is possible, otherwise False.
Example:
>>> backtrack("leetcode", {"leet", "code"}, 0)
True
>>> backtrack("applepenapple", {"apple", "pen"}, 0)
True
>>> backtrack("catsandog", {"cats", "dog", "sand", "and", "cat"}, 0)
False
"""

# Base case: if the starting index has reached the end of the string
if start == len(input_string):
return True

# Try every possible substring from 'start' to 'end'
for end in range(start + 1, len(input_string) + 1):
if input_string[start:end] in word_dict and backtrack(
input_string, word_dict, end
):
return True

return False


def word_break(input_string: str, word_dict: set[str]) -> bool:
"""
Determines if the input string can be segmented into a sequence of
valid dictionary words using backtracking.
Parameters:
input_string (str): The input string to segment.
word_dict (set[str]): The set of valid words.
Returns:
bool: True if the string can be segmented into valid words, otherwise False.
Example:
>>> word_break("leetcode", {"leet", "code"})
True
>>> word_break("applepenapple", {"apple", "pen"})
True
>>> word_break("catsandog", {"cats", "dog", "sand", "and", "cat"})
False
"""

return backtrack(input_string, word_dict, 0)
45 changes: 45 additions & 0 deletions ciphers/gronsfeld_cipher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from string import ascii_uppercase


def gronsfeld(text: str, key: str) -> str:
"""
Encrypt plaintext with the Gronsfeld cipher
>>> gronsfeld('hello', '412')
'LFNPP'
>>> gronsfeld('hello', '123')
'IGOMQ'
>>> gronsfeld('', '123')
''
>>> gronsfeld('yes, ¥€$ - _!@#%?', '0')
'YES, ¥€$ - _!@#%?'
>>> gronsfeld('yes, ¥€$ - _!@#%?', '01')
'YFS, ¥€$ - _!@#%?'
>>> gronsfeld('yes, ¥€$ - _!@#%?', '012')
'YFU, ¥€$ - _!@#%?'
>>> gronsfeld('yes, ¥€$ - _!@#%?', '')
Traceback (most recent call last):
...
ZeroDivisionError: integer modulo by zero
"""
ascii_len = len(ascii_uppercase)
key_len = len(key)
encrypted_text = ""
keys = [int(char) for char in key]
upper_case_text = text.upper()

for i, char in enumerate(upper_case_text):
if char in ascii_uppercase:
new_position = (ascii_uppercase.index(char) + keys[i % key_len]) % ascii_len
shifted_letter = ascii_uppercase[new_position]
encrypted_text += shifted_letter
else:
encrypted_text += char

return encrypted_text


if __name__ == "__main__":
from doctest import testmod

testmod()
78 changes: 78 additions & 0 deletions data_structures/binary_tree/maximum_sum_bst.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from __future__ import annotations

import sys
from dataclasses import dataclass

INT_MIN = -sys.maxsize + 1
INT_MAX = sys.maxsize - 1


@dataclass
class TreeNode:
val: int = 0
left: TreeNode | None = None
right: TreeNode | None = None


def max_sum_bst(root: TreeNode | None) -> int:
"""
The solution traverses a binary tree to find the maximum sum of
keys in any subtree that is a Binary Search Tree (BST). It uses
recursion to validate BST properties and calculates sums, returning
the highest sum found among all valid BST subtrees.
>>> t1 = TreeNode(4)
>>> t1.left = TreeNode(3)
>>> t1.left.left = TreeNode(1)
>>> t1.left.right = TreeNode(2)
>>> print(max_sum_bst(t1))
2
>>> t2 = TreeNode(-4)
>>> t2.left = TreeNode(-2)
>>> t2.right = TreeNode(-5)
>>> print(max_sum_bst(t2))
0
>>> t3 = TreeNode(1)
>>> t3.left = TreeNode(4)
>>> t3.left.left = TreeNode(2)
>>> t3.left.right = TreeNode(4)
>>> t3.right = TreeNode(3)
>>> t3.right.left = TreeNode(2)
>>> t3.right.right = TreeNode(5)
>>> t3.right.right.left = TreeNode(4)
>>> t3.right.right.right = TreeNode(6)
>>> print(max_sum_bst(t3))
20
"""
ans: int = 0

def solver(node: TreeNode | None) -> tuple[bool, int, int, int]:
"""
Returns the maximum sum by making recursive calls
>>> t1 = TreeNode(1)
>>> print(solver(t1))
1
"""
nonlocal ans

if not node:
return True, INT_MAX, INT_MIN, 0 # Valid BST, min, max, sum

is_left_valid, min_left, max_left, sum_left = solver(node.left)
is_right_valid, min_right, max_right, sum_right = solver(node.right)

if is_left_valid and is_right_valid and max_left < node.val < min_right:
total_sum = sum_left + sum_right + node.val
ans = max(ans, total_sum)
return True, min(min_left, node.val), max(max_right, node.val), total_sum

return False, -1, -1, -1 # Not a valid BST

solver(root)
return ans


if __name__ == "__main__":
import doctest

doctest.testmod()
38 changes: 38 additions & 0 deletions data_structures/stacks/lexicographical_numbers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from collections.abc import Iterator


def lexical_order(max_number: int) -> Iterator[int]:
"""
Generate numbers in lexical order from 1 to max_number.
>>> " ".join(map(str, lexical_order(13)))
'1 10 11 12 13 2 3 4 5 6 7 8 9'
>>> list(lexical_order(1))
[1]
>>> " ".join(map(str, lexical_order(20)))
'1 10 11 12 13 14 15 16 17 18 19 2 20 3 4 5 6 7 8 9'
>>> " ".join(map(str, lexical_order(25)))
'1 10 11 12 13 14 15 16 17 18 19 2 20 21 22 23 24 25 3 4 5 6 7 8 9'
>>> list(lexical_order(12))
[1, 10, 11, 12, 2, 3, 4, 5, 6, 7, 8, 9]
"""

stack = [1]

while stack:
num = stack.pop()
if num > max_number:
continue

yield num
if (num % 10) != 9:
stack.append(num + 1)

stack.append(num * 10)


if __name__ == "__main__":
from doctest import testmod

testmod()
print(f"Numbers from 1 to 25 in lexical order: {list(lexical_order(26))}")
60 changes: 46 additions & 14 deletions data_structures/stacks/next_greater_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,20 @@

def next_greatest_element_slow(arr: list[float]) -> list[float]:
"""
Get the Next Greatest Element (NGE) for all elements in a list.
Maximum element present after the current one which is also greater than the
current one.
Get the Next Greatest Element (NGE) for each element in the array
by checking all subsequent elements to find the next greater one.
This is a brute-force implementation, and it has a time complexity
of O(n^2), where n is the size of the array.
Args:
arr: List of numbers for which the NGE is calculated.
Returns:
List containing the next greatest elements. If no
greater element is found, -1 is placed in the result.
Example:
>>> next_greatest_element_slow(arr) == expect
True
"""
Expand All @@ -28,9 +39,21 @@ def next_greatest_element_slow(arr: list[float]) -> list[float]:

def next_greatest_element_fast(arr: list[float]) -> list[float]:
"""
Like next_greatest_element_slow() but changes the loops to use
enumerate() instead of range(len()) for the outer loop and
for in a slice of arr for the inner loop.
Find the Next Greatest Element (NGE) for each element in the array
using a more readable approach. This implementation utilizes
enumerate() for the outer loop and slicing for the inner loop.
While this improves readability over next_greatest_element_slow(),
it still has a time complexity of O(n^2).
Args:
arr: List of numbers for which the NGE is calculated.
Returns:
List containing the next greatest elements. If no
greater element is found, -1 is placed in the result.
Example:
>>> next_greatest_element_fast(arr) == expect
True
"""
Expand All @@ -47,14 +70,23 @@ def next_greatest_element_fast(arr: list[float]) -> list[float]:

def next_greatest_element(arr: list[float]) -> list[float]:
"""
Get the Next Greatest Element (NGE) for all elements in a list.
Maximum element present after the current one which is also greater than the
current one.
A naive way to solve this is to take two loops and check for the next bigger
number but that will make the time complexity as O(n^2). The better way to solve
this would be to use a stack to keep track of maximum number giving a linear time
solution.
Efficient solution to find the Next Greatest Element (NGE) for all elements
using a stack. The time complexity is reduced to O(n), making it suitable
for larger arrays.
The stack keeps track of elements for which the next greater element hasn't
been found yet. By iterating through the array in reverse (from the last
element to the first), the stack is used to efficiently determine the next
greatest element for each element.
Args:
arr: List of numbers for which the NGE is calculated.
Returns:
List containing the next greatest elements. If no
greater element is found, -1 is placed in the result.
Example:
>>> next_greatest_element(arr) == expect
True
"""
Expand Down
47 changes: 45 additions & 2 deletions dynamic_programming/floyd_warshall.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,58 @@ def __init__(self, n=0): # a graph with Node 0,1,...,N-1
] # dp[i][j] stores minimum distance from i to j

def add_edge(self, u, v, w):
"""
Adds a directed edge from node u
to node v with weight w.
>>> g = Graph(3)
>>> g.add_edge(0, 1, 5)
>>> g.dp[0][1]
5
"""
self.dp[u][v] = w

def floyd_warshall(self):
"""
Computes the shortest paths between all pairs of
nodes using the Floyd-Warshall algorithm.
>>> g = Graph(3)
>>> g.add_edge(0, 1, 1)
>>> g.add_edge(1, 2, 2)
>>> g.floyd_warshall()
>>> g.show_min(0, 2)
3
>>> g.show_min(2, 0)
inf
"""
for k in range(self.n):
for i in range(self.n):
for j in range(self.n):
self.dp[i][j] = min(self.dp[i][j], self.dp[i][k] + self.dp[k][j])

def show_min(self, u, v):
"""
Returns the minimum distance from node u to node v.
>>> g = Graph(3)
>>> g.add_edge(0, 1, 3)
>>> g.add_edge(1, 2, 4)
>>> g.floyd_warshall()
>>> g.show_min(0, 2)
7
>>> g.show_min(1, 0)
inf
"""
return self.dp[u][v]


if __name__ == "__main__":
import doctest

doctest.testmod()

# Example usage
graph = Graph(5)
graph.add_edge(0, 2, 9)
graph.add_edge(0, 4, 10)
Expand All @@ -38,5 +77,9 @@ def show_min(self, u, v):
graph.add_edge(4, 2, 4)
graph.add_edge(4, 3, 9)
graph.floyd_warshall()
graph.show_min(1, 4)
graph.show_min(0, 3)
print(
graph.show_min(1, 4)
) # Should output the minimum distance from node 1 to node 4
print(
graph.show_min(0, 3)
) # Should output the minimum distance from node 0 to node 3
Loading

0 comments on commit aa16d15

Please sign in to comment.