Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
72fbed2
feat(datastructures, binary-trees): sum nodes in range
BrianLusina Jan 29, 2026
7075c1b
updating DIRECTORY.md
Jan 29, 2026
7d19b5f
feat(algorithms, dynamic-proramming): frog jumps
BrianLusina Feb 2, 2026
5eec48b
feat(algorithms, backtracking): letter tile possibilities
BrianLusina Feb 2, 2026
df555a5
test(algorithms, dynamic-proramming): word-break additional tests
BrianLusina Feb 2, 2026
7417216
feat(algorithms, dynamic-proramming): maximum profit in job scheduling
BrianLusina Feb 2, 2026
2983f23
feat(algorithms, graphs): network delay time
BrianLusina Feb 3, 2026
77cdb66
feat(algorithms, graphs): single cycle check
BrianLusina Feb 3, 2026
f43beb5
feat(algorithms, graphs): valid tree
BrianLusina Feb 3, 2026
dd4a1d0
feat(datastructures, tree): longest univalue
BrianLusina Feb 3, 2026
0d8cfc2
updating DIRECTORY.md
Feb 3, 2026
67508bb
feat(math, geometry): self crossing
BrianLusina Feb 5, 2026
5b54d51
feat(algorithms, two-pointers): sort by parity
BrianLusina Feb 6, 2026
a87ffb3
docs(algorithms, sliding-window): substring with concatenation
BrianLusina Feb 6, 2026
0bc5654
updating DIRECTORY.md
Feb 6, 2026
a22aa85
Update datastructures/trees/binary/search_tree/binary_search_tree.py
BrianLusina Feb 6, 2026
832a2ac
Update datastructures/trees/binary/search_tree/binary_search_tree.py
BrianLusina Feb 6, 2026
40f7c03
Update algorithms/graphs/network_delay_time/test_network_delay_time.py
BrianLusina Feb 6, 2026
07e2005
Update algorithms/dynamic_programming/frog_jump/README.md
BrianLusina Feb 6, 2026
321f8e7
Update algorithms/sliding_window/substring_concatenation/README.md
BrianLusina Feb 6, 2026
cf79f0c
Update algorithms/dynamic_programming/max_profit_in_job_scheduling/RE…
BrianLusina Feb 6, 2026
48b8618
Update algorithms/dynamic_programming/maximal_rectangle/README.md
BrianLusina Feb 6, 2026
038c144
Update algorithms/intervals/data_stream/README.md
BrianLusina Feb 6, 2026
1a71df5
Update algorithms/sliding_window/minimum_window_substring/README.md
BrianLusina Feb 6, 2026
1536489
Update algorithms/sliding_window/substring_concatenation/__init__.py
BrianLusina Feb 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 43 additions & 12 deletions DIRECTORY.md

Large diffs are not rendered by default.

144 changes: 144 additions & 0 deletions algorithms/backtracking/letter_tile_possibilities/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Letter Tile Possibilities

You are given a string, tiles, consisting of uppercase English letters. You can arrange the tiles into sequences of any
length (from 1 to the length of tiles), and each sequence must include at most one tile, tiles[i], from tiles.

Your task is to return the number of possible non-empty unique sequences you can make using the letters represented on
tiles[i].

## Constraints:

- 1 ≤ `tiles.length` ≤ 7
- The `tiles` string consists of uppercase English letters.

## Examples

Example 1:

```text
Input: tiles = "AAB"
Output: 8
Explanation: The possible sequences are "A", "B", "AA", "AB", "BA", "AAB", "ABA", "BAA".
```

Example 2:
```text
Input: tiles = "AAABBC"
Output: 188
```

Example 3:
```text
Input: tiles = "V"
Output: 1
```

## Topics

- Hash Table
- String
- Backtracking
- Counting

## Solutions

### Permutations and Combinations

This problem would have been straightforward if the tiles had no duplicates and we only needed to find sequences of the
same length as the given tiles. In that case, we could simply calculate the total unique sequences using n! (where n is
the length of the tiles).

![Sample](./images/examples/letter_tile_possibilities_example_sample.png)

However, in this problem, we need to find all unique sequences of any length—from 1 to the full length of the tiles—while
accounting for duplicate tiles. To achieve this, we take the following approach:

We begin by sorting the letters in tiles so that similar letters are grouped. This allows us to systematically explore
possible letter sets without repeating unnecessary work.

To generate sequences of lengths ranging from 1 to n, we consider each letter one by one. We start with a single
character, determine its possible sequences, then move to two-character combinations, find their possible sequences,
and so on. For each new character, we have two choices: either include the current letter in our selection or skip it
and move to the next one. By exploring both choices, we generate all possible letter sets.

To prevent counting the same sequences multiple times due to duplicate letters, we ensure (in the previous step) that
duplicate letter sets are not counted more than once while generating different letter sets. Then, for each set, we
calculate how many distinct sequences can be formed by applying the updated formula for permutations, which accounts
for repeated letters: `n! / c!`, where c is the count of each repeated letter.

The more repeated letters in a set, the fewer unique ways it can be arranged.

We repeat this process for every possible letter set. Eventually, we sum the count of all these possible sequences to
obtain the total number of unique non-empty sequences. Finally, we subtract one from our final count to exclude the
empty set, as it does not contribute to valid arrangements.

Let’s look at the following illustration to understand this better.

![Solution 0](./images/solutions/letter_tile_possibilities_solution_0.png)

Now, let’s look at the algorithm steps of this solution:

- Initialize a set, unique_letter_sets, to track unique letter sets.
- Sort the input string, tiles, to group identical letters together.
- Generate unique letter sets and count their permutations:
- The function generate_sequences is used to recursively generate all possible letter subsets.
- We calculate the number of valid sequences using `count_permutations` for each unique letter set. The
`count_permutations` uses the helper function `factorial(n)`, which computes the factorial of a given number.
- The recursive function, `generate_sequences(tiles, current_letter_set, index, unique_letter_sets)`, works as follows:
- **Base case**: If `index` reaches the end of tiles, check if `current_letter_set` is unique. If it is:
- Add it to `unique_letter_sets`.
- Compute its permutations and return the count.
- **Recursive cases**:
- Exclude the current character (move to the next index). Store its result in the variable `without_letter`.
- Include the current character (append it and move to the next index). Store its result in the variable `with_letter`.
- The sum of both recursive calls gives the total count of valid sequences.
- Once all the letters in tiles have been explored, return the output. As an empty set is also considered in the
recursive process, we subtract 1 before returning the output.

![Solution 1](./images/solutions/letter_tile_possibilities_solution_1.png)
![Solution 2](./images/solutions/letter_tile_possibilities_solution_2.png)
![Solution 3](./images/solutions/letter_tile_possibilities_solution_3.png)
![Solution 4](./images/solutions/letter_tile_possibilities_solution_4.png)
![Solution 5](./images/solutions/letter_tile_possibilities_solution_5.png)
![Solution 6](./images/solutions/letter_tile_possibilities_solution_6.png)
![Solution 7](./images/solutions/letter_tile_possibilities_solution_7.png)
![Solution 8](./images/solutions/letter_tile_possibilities_solution_8.png)
![Solution 9](./images/solutions/letter_tile_possibilities_solution_9.png)
![Solution 10](./images/solutions/letter_tile_possibilities_solution_10.png)
![Solution 11](./images/solutions/letter_tile_possibilities_solution_11.png)
![Solution 12](./images/solutions/letter_tile_possibilities_solution_12.png)
![Solution 13](./images/solutions/letter_tile_possibilities_solution_13.png)
![Solution 14](./images/solutions/letter_tile_possibilities_solution_14.png)
![Solution 15](./images/solutions/letter_tile_possibilities_solution_15.png)
![Solution 16](./images/solutions/letter_tile_possibilities_solution_16.png)
![Solution 17](./images/solutions/letter_tile_possibilities_solution_17.png)
![Solution 18](./images/solutions/letter_tile_possibilities_solution_18.png)
![Solution 19](./images/solutions/letter_tile_possibilities_solution_19.png)
![Solution 10](./images/solutions/letter_tile_possibilities_solution_20.png)
![Solution 11](./images/solutions/letter_tile_possibilities_solution_21.png)

#### Time Complexity

Let’s break down and analyze the time complexity of this solution:
- Sorting the input string takes `O(n log(n))` time, where n is the length of the tiles.
- The recursive function explores all possible subsets of the given tiles. As each tile can either be included or
skipped, there are 2^n subsets in the worst case.
- For each subset, we calculate the number of unique sequences that can be formed using the formula that accounts for
duplicate letters.
- Computing the factorial and handling character frequencies takes at most `O(n)` time per subset.
- As we do this for all 2^n subsets, the total time complexity is `O(2 ^n × n)`, which dominates the sorting step.

If we sum these up, the overall time complexity simplifies to:

`O(nlogn) + O(2^n) + O(2^n×n) = O(2^n×n)`

#### Space Complexity
Let’s analyze the space complexity of this solution:
- As the recursion goes as deep as the length of the string n, the worst case space used by the function calls is `O(n)`.
- We store all unique subsets of tiles in a set, which can hold up to O(2^n) subsets in the worst case. Each sequence in
the set can be up to length n. So, the set uses `O(2^n × n)` space.
- The permutation calculation uses extra space, but this is at most O(n).

If we add these up, the overall space complexity simplifies to:

`O(n) + O(2^n × n) + O(n) = O(2^n × n)`
94 changes: 94 additions & 0 deletions algorithms/backtracking/letter_tile_possibilities/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from typing import List, Set
from itertools import permutations


def num_tile_possibilities(tiles: str) -> int:
# Store unique sequences to avoid duplicates
unique_letter_sets: Set[str] = set()

# Sort characters to handle duplicates efficiently
sorted_tiles: str = "".join(sorted(tiles))

def generate_sequences(current: str, index: int):
# Recursively generates all possible sequences by including/excluding each tile.
# Once a new sequence is found, its unique permutations are counted.
if index >= len(sorted_tiles):
# If a new sequence is found, count its unique permutations
if current not in unique_letter_sets:
unique_letter_sets.add(current)

total_permutations = len(set(permutations(current)))
return total_permutations
return 0

# Explore two possibilities: skipping the current character or including it
without_letter = generate_sequences(current, index=index + 1)
with_letter = generate_sequences(current + sorted_tiles[index], index=index + 1)

# Return all the possible sequences
return without_letter + with_letter

# Optionally, you can write the count_permutations and the factorial method if not using builtin methods
# def factorial(n):
#
# # Computes the factorial of a given number.
# if n <= 1:
# return 1
#
# result = 1
# for num in range(2, n + 1):
# result *= num
# return result
#
# def count_permutations(sequence):
#
# # Calculates the number of unique permutations of a sequence, accounting for duplicate characters.
# permutations = factorial(len(sequence))
#
# # Adjust for repeated characters by dividing by factorial of their counts
# for count in Counter(sequence).values():
# permutations //= factorial(count)
#
# return permutations

output = generate_sequences("", 0)

return output - 1


def num_tile_possibilities_with_recursion(tiles: str) -> int:
sequences: Set[str] = set()
used: List[bool] = [False] * len(tiles)

def generate_sequences(current: str) -> None:
sequences.add(current)

for idx, char in enumerate(tiles):
if not used[idx]:
used[idx] = True
generate_sequences(current + char)
used[idx] = False

generate_sequences("")
return len(sequences) - 1


def num_tile_possibilities_with_optimized_recursion(tiles: str) -> int:
char_count: List[int] = [0] * 26
for letter in tiles:
char_count[ord(letter) - ord("A")] += 1

def generate_sequences() -> int:
result = 0
for idx in range(26):
if char_count[idx] == 0:
continue

result += 1
char_count[idx] -= 1
result += generate_sequences()
char_count[idx] += 1

return result

return generate_sequences()
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import unittest
from parameterized import parameterized
from algorithms.backtracking.letter_tile_possibilities import (
num_tile_possibilities,
num_tile_possibilities_with_recursion,
num_tile_possibilities_with_optimized_recursion,
)

LETTER_TILE_POSSIBILITIES = [
("AAB", 8),
("ABC", 15),
("ABC", 15),
("CDB", 15),
("ZZZ", 3),
("AAABBC", 188),
("V", 1),
]


class NumTilePossibilitiesTestCase(unittest.TestCase):
@parameterized.expand(LETTER_TILE_POSSIBILITIES)
def test_num_tile_possibilities(self, tiles: str, expected: int):
actual = num_tile_possibilities(tiles)
self.assertEqual(expected, actual)

@parameterized.expand(LETTER_TILE_POSSIBILITIES)
def test_num_tile_possibilities_with_recursion(self, tiles: str, expected: int):
actual = num_tile_possibilities_with_recursion(tiles)
self.assertEqual(expected, actual)

@parameterized.expand(LETTER_TILE_POSSIBILITIES)
def test_num_tile_possibilities_with_optimized_recursion(
self, tiles: str, expected: int
):
actual = num_tile_possibilities_with_optimized_recursion(tiles)
self.assertEqual(expected, actual)


if __name__ == "__main__":
unittest.main()
6 changes: 3 additions & 3 deletions algorithms/dynamic_programming/coin_change/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ solutions of its subproblems.
To split the problem into subproblems, let's assume we know the number of coins required for some total value and the
last coin denomination is C. Because of the optimal substructure property, the following equation will be true:

Min(total)=Min(total−C)+1
`Min(total) = Min(total − C) + 1`

But, we don't know what is the value of C yet, so we compute it for each element of the coins array and select the
minimum from among them. This creates the following recurrence relation:
Expand Down Expand Up @@ -84,13 +84,13 @@ three base cases to cover about what to return if the remaining amount is:

- Less than zero
- Equal to zero
- Neither less than zero nor equal to zero
- Neither less than zero nor equal to zero (Greater than 0)

> The top-down solution, commonly known as the memoization technique, is an enhancement of the recursive solution. It
> solves the problem of calculating redundant solutions over and over by storing them in an array.

In the last case, when the remaining amount is neither of the base cases, we traverse the coins array, and at each
element, we recursively call the calculate_minimum_coins() function, passing the updated remaining amount remaining_amount
element, we recursively call the `calculate_minimum_coins()` function, passing the updated remaining amount remaining_amount
minus the value of the current coin. This step effectively evaluates the number of coins needed for each possible
denomination to make up the remaining amount. We store the return value of the base cases for each subproblem in a
variable named result. We then add 1 to the result variable indicating that we're using this coin denomination in the
Expand Down
84 changes: 84 additions & 0 deletions algorithms/dynamic_programming/frog_jump/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Frog Jump

A frog is trying to cross a river by jumping on stones placed at various positions along the river. The river is divided
into units, and some units contain stones while others do not. The frog can only land on a stone, but it must not jump
into the water.

You are given an array, stones, that represents the positions of the stones in ascending order (in units). The frog
starts on the first stone, and its first jump must be exactly 1 unit.

If the frog’s last jump was of length k units, its next jump must be either k−1, k, or k+1 units. The frog can only move
forward.

Your task is to determine whether the frog can successfully reach the last stone.

## Constraints

- 2 ≤ `stones.length` ≤ 1000
- 0 ≤ `stones[i]` ≤ 2^20 − 1
- stones[0] == 0
- The stones array is sorted in a strictly increasing order.

## Topics

- Array
- Dynamic Programming

## Solution

The solution’s essence lies in leveraging dynamic programming to track all the jump lengths that can reach each stone.
The frog begins on the first stone with a jump length of zero, and from every reachable stone, it tries jumps of the
same size, one unit longer, or one unit shorter. Each valid move updates the DP table to record new reachable states.
The algorithm stores each stone’s position along with its index in a map, allowing for quick checks to determine if a
stone exists at a given position. Finally, if any jump length reaches the last stone, the frog can successfully cross
the river.

Using the intuition above, we implement the algorithm as follows:
- Store the number of stones in a variable n.
- Create an unordered map, `mapper`, to store each stone’s position as the key and its index as the value for fast
lookups.
- Populate the `mapper` by mapping every stone position, `stones[i]`, to its index, `i`.
- Initialize a 2D array, `dp`, of size `1001 x 1001` with zeros to track reachable states.
> Note that this is highly dependable on the constraints, larger sizes of stones will cause an Index out of bounds
> error. So, ensure to initialize the size correctly
- Set `dp[0][0]` to `1` to indicate that the frog starts on the first stone with a previous jump of 0.
- Iterate over each stone index `i`:
- For each stone, iterate over all possible previous jump lengths `prevJump`.
- Check if `dp[i][prevJump]` is 1 to confirm that the frog can reach stone `i` with jump size `prevJump`:
- If reachable, store the current position as `currPos = stones[i]`.
- Check if a stone exists at `currPos + prevJump`; if yes, mark `dp[mapper[currPos + prevJump]][prevJump]` as `1`.
- Check if a stone exists at `currPos + prevJump + 1`; if yes, mark `dp[mapper[currPos + prevJump + 1]][prevJump + 1]` as `1`
- If `prevJump > 1` and a stone exists at `currPos + prevJump - 1`, mark `dp[mapper[currPos + prevJump - 1]][prevJump - 1]` as `1`.
- After filling the DP table, iterate over all possible jump sizes `jump` for the last stone.
- If any `dp[n - 1][jump]` equals `1`, return TRUE to indicate that the frog can cross the river.
- If no valid jump leads to the last stone, return FALSE.

![Solution 1](./images/solutions/frog_jump_solution_1.png)
![Solution 2](./images/solutions/frog_jump_solution_2.png)
![Solution 3](./images/solutions/frog_jump_solution_3.png)
![Solution 4](./images/solutions/frog_jump_solution_4.png)
![Solution 5](./images/solutions/frog_jump_solution_5.png)
![Solution 6](./images/solutions/frog_jump_solution_6.png)
![Solution 7](./images/solutions/frog_jump_solution_7.png)
![Solution 8](./images/solutions/frog_jump_solution_8.png)
![Solution 9](./images/solutions/frog_jump_solution_9.png)
![Solution 10](./images/solutions/frog_jump_solution_10.png)
![Solution 11](./images/solutions/frog_jump_solution_11.png)
![Solution 12](./images/solutions/frog_jump_solution_12.png)
![Solution 13](./images/solutions/frog_jump_solution_13.png)
![Solution 14](./images/solutions/frog_jump_solution_14.png)
![Solution 15](./images/solutions/frog_jump_solution_15.png)
![Solution 16](./images/solutions/frog_jump_solution_16.png)
![Solution 17](./images/solutions/frog_jump_solution_17.png)
![Solution 18](./images/solutions/frog_jump_solution_18.png)
![Solution 19](./images/solutions/frog_jump_solution_19.png)
![Solution 20](./images/solutions/frog_jump_solution_20.png)

### Time Complexity

The algorithm’s time complexity is O(n^2), where n is the number of stones. For each stone, it may check all possible
previous jump lengths, and for each valid state, it performs constant-time lookups in the map.

### Space Complexity

The algorithm's space complexity is `O(n)` for the mapper map, plus `O(1)` for the fixed-size 2D DP array (1001 x 1001), assuming the constraints guarantee `n ≤ 1000`.
Loading
Loading