Skip to content

Commit 736636e

Browse files
authored
Merge pull request #154 from BrianLusina/feat/algorithms-dp-bs
feat(algorithms, two-pointers, dynamic-programming): two-pointers dp and bs problems
2 parents c061165 + c1903b0 commit 736636e

File tree

51 files changed

+1586
-71
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1586
-71
lines changed

DIRECTORY.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@
5959
* [Test Climb Stairs](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/climb_stairs/test_climb_stairs.py)
6060
* Countingbits
6161
* [Test Counting Bits](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/countingbits/test_counting_bits.py)
62+
* Decodeways
63+
* [Test Num Decodings](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/decodeways/test_num_decodings.py)
6264
* Domino Tromino Tiling
6365
* [Test Domino Tromino Tiling](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/domino_tromino_tiling/test_domino_tromino_tiling.py)
6466
* Duffle Bug Value
@@ -214,6 +216,8 @@
214216
* [Test Divide Chocolate](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/search/binary_search/divide_chocolate/test_divide_chocolate.py)
215217
* Maxruntime N Computers
216218
* [Test Max Runtime](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/search/binary_search/maxruntime_n_computers/test_max_runtime.py)
219+
* Split Array Largest Sum
220+
* [Test Split Array Largest Sum](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/search/binary_search/split_array_largest_sum/test_split_array_largest_sum.py)
217221
* [Test Binary Search](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/search/binary_search/test_binary_search.py)
218222
* Interpolation
219223
* [Test Interpolation Search](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/search/interpolation/test_interpolation_search.py)
@@ -255,6 +259,8 @@
255259
* Two Pointers
256260
* Array 3 Pointers
257261
* [Test Array 3 Pointers](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/two_pointers/array_3_pointers/test_array_3_pointers.py)
262+
* Count Pairs
263+
* [Test Count Pairs](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/two_pointers/count_pairs/test_count_pairs.py)
258264
* Find Sum Of Three
259265
* [Test Find Sum Of Three](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/two_pointers/find_sum_of_three/test_find_sum_of_three.py)
260266
* Merge Sorted Arrays
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Decode Ways
2+
3+
You have intercepted a secret message encoded as a string of numbers. The message is decoded via the following mapping:
4+
5+
```text
6+
'1' -> "A"
7+
'2' -> "B"
8+
'3' -> "C"
9+
...
10+
'26' -> "Z"
11+
```
12+
However, while decoding the message, you realize that there are many different ways you can decode the message because
13+
some codes are contained in other codes ("2" and "5" vs "25").
14+
15+
For example, "11106" can be decoded into:
16+
17+
- "AAJF" with the grouping (1, 1, 10, 6)
18+
- "KJF" with the grouping (11, 10, 6)
19+
- The grouping (1, 11, 06) is invalid because "06" is not a valid code (only "6" is valid).
20+
21+
Note: there may be strings that are impossible to decode.
22+
23+
Given a string s containing only digits, return the number of ways to decode it. If the entire string cannot be decoded
24+
in any valid way, return 0.
25+
26+
The test cases are generated so that the answer fits in a 32-bit integer.
27+
28+
## Examples
29+
30+
Example 1
31+
32+
```text
33+
Input: s = "12"
34+
35+
Output: 2
36+
37+
Explanation:
38+
39+
"12" could be decoded as "AB" (1 2) or "L" (12).
40+
```
41+
42+
Example 2
43+
44+
```text
45+
Input: s = "226"
46+
47+
Output: 3
48+
49+
Explanation:
50+
51+
"226" could be decoded as "BZ" (2 26), "VF" (22 6), or "BBF" (2 2 6).
52+
```
53+
54+
Example 3
55+
56+
```text
57+
Input: s = "06"
58+
59+
Output: 0
60+
61+
Explanation:
62+
63+
"06" cannot be mapped to "F" because of the leading zero ("6" is different from "06"). In this case, the string is not
64+
a valid encoding, so return 0.
65+
```
66+
67+
Example 4
68+
69+
```text
70+
s = 101
71+
output = 1
72+
Explanation: The only way to decode it is "JA". "01" cannot be decoded into "A" as "1" and "01" are different.
73+
```
74+
75+
## Constraints
76+
77+
- 1 <= s.length <= 100
78+
- s contains only digits and may contain leading zero(s).
79+
80+
## Topics
81+
82+
- String
83+
- Dynamic Programming
84+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
def num_decodings(s: str) -> int:
2+
if not s or s[0] == "0":
3+
return 0
4+
5+
n = len(s)
6+
# Initialize the dp array. dp[i] will store the number of ways to decode the string s[:i]
7+
dp = [0] * (n + 1)
8+
dp[0] = 1
9+
dp[1] = 1
10+
11+
# Iterate through the string
12+
for i in range(2, n + 1):
13+
# Get the current character
14+
current_char = s[i - 1]
15+
# Get the previous character
16+
prev_char = s[i - 2]
17+
18+
# If the current character is not a zero, then we can decode it as a single character
19+
if current_char != "0":
20+
dp[i] += dp[i - 1]
21+
22+
# If the previous character is 1 or 2 and the current character is less than or equal to 6, then we can decode
23+
# it as a two character
24+
if prev_char == "1" or (prev_char == "2" and current_char <= "6"):
25+
dp[i] += dp[i - 2]
26+
27+
# Return the number of ways to decode the string
28+
return dp[n]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import unittest
2+
from parameterized import parameterized
3+
from algorithms.dynamic_programming.decodeways import num_decodings
4+
5+
NUM_DECODINGS_TEST_CASES = [
6+
("11106", 2),
7+
("12", 2),
8+
("226", 3),
9+
("06", 0),
10+
("101", 1),
11+
("12", 2),
12+
("012", 0),
13+
("0", 0),
14+
("30", 0),
15+
("10", 1),
16+
("27", 1),
17+
]
18+
19+
20+
class NumDecodingsTestCase(unittest.TestCase):
21+
@parameterized.expand(NUM_DECODINGS_TEST_CASES)
22+
def test_num_decodings(self, s: str, expected: int):
23+
actual = num_decodings(s)
24+
self.assertEqual(expected, actual)
25+
26+
27+
if __name__ == "__main__":
28+
unittest.main()

algorithms/dynamic_programming/painthouse/test_min_cost_to_paint_houses.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import unittest
22
from typing import List
33
from parameterized import parameterized
4-
from algorithms.dynamic_programming.painthouse import min_cost_to_paint_houses_alternate_colors
4+
from algorithms.dynamic_programming.painthouse import (
5+
min_cost_to_paint_houses_alternate_colors,
6+
)
57

68
MIN_COST_PAINT_HOUSE = [
79
([[8, 4, 15], [10, 7, 3], [6, 9, 12]], 13),
@@ -16,5 +18,5 @@ def test_min_cost_to_paint_houses(self, cost: List[List[int]], expected: int):
1618
self.assertEqual(expected, actual)
1719

1820

19-
if __name__ == '__main__':
21+
if __name__ == "__main__":
2022
unittest.main()
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Split Array Largest Sum
2+
3+
Given an integer list nums and an integer k, split nums into k non-empty subarrays such that the largest sum among these
4+
subarrays is minimized. The task is to find the minimized largest sum by choosing the split such that the largest sum of
5+
every split of subarrays is the minimum among the sum of other splits.
6+
7+
## Constraints
8+
9+
- 1 <= nums.length <= 1000
10+
- 0 <= nums[i] <= 10^4
11+
- 1 <= k <= nums.length
12+
13+
## Examples
14+
15+
![Example 1](images/examples/split_array_largest_sum_example_1.png)
16+
![Example 2](images/examples/split_array_largest_sum_example_2.png)
17+
![Example 3](images/examples/split_array_largest_sum_example_3.png)
18+
![Example 4](images/examples/split_array_largest_sum_example_4.png)
19+
20+
## Topics
21+
22+
- Array
23+
- Binary Search
24+
- Dynamic Programming
25+
- Greedy
26+
- Prefix Sum
27+
28+
## Solution
29+
30+
In a brute force method, you would try all possible ways to split the array into k subarrays, calculate the largest sum
31+
for each split, and then find the smallest among those largest sums. This approach is extremely inefficient because the
32+
number of ways to split the array grows exponentially as the size increases.
33+
34+
We reverse the process by guessing a value for the minimum largest sum and checking if it’s feasible:
35+
36+
- Instead of iterating through all splits, we only focus on testing whether a specific value allows us to split the
37+
array into k subarrays.
38+
39+
- But wait—just because one value works doesn’t mean it’s the smallest feasible value. We keep exploring smaller values
40+
to achieve the most optimized result.
41+
42+
The solution uses the binary search approach to find the optimal largest subarray sum without testing all possible splits.
43+
The binary search finds the smallest possible value of the largest subarray sum and applies searching over the range of
44+
possible values for this largest sum. But how do we guess this value? We guess the value using a certain range.
45+
Here’s how:
46+
47+
- **Left boundary**: The maximum element in the array is the minimum possible value for the largest subarray sum. This
48+
is because any valid subarray must have a sum at least as large as the largest element.
49+
50+
- **Right boundary**: The maximum possible value for the largest subarray sum is the sum of all elements in the array.
51+
You would get this sum if the entire array were one single subarray.
52+
53+
The binary search iteratively tests midpoints in the above ranges. It determines whether dividing the array results in
54+
at most k subarrays will result in the smallest largest sum. If it does, the search shifts to lower values to minimize
55+
the largest sum. Otherwise, it shifts to higher values. Still, there might be subarrays whose sum could be smaller, so
56+
the search keeps going until the search range crosses each other, i.e., **left boundary** > **right boundary**.
57+
58+
Here’s the step-by-step implementation of the solution:
59+
60+
- Start by initializing the ranges for search. The left will be the largest number in the array, and the right will be
61+
the sum of all numbers.
62+
- Use a guessing approach. Start by considering a mid value between the left and right as a test value.
63+
- Check if it is possible to divide the array into k subarrays so that the sum of no subarray is greater than mid.
64+
- Start with an empty sum and add numbers from the array. If adding the next number exceeds mid:
65+
- Start a new subarray with that number and increment the count of the subarrays.
66+
- Return FALSE if the count exceeds k. Otherwise, return TRUE.
67+
- Adjust the guessing range by checking if the number of subarrays needed is within the k and reduce the mid to see if
68+
a smaller largest sum is possible.
69+
- Otherwise, if the count of subarrays is more than k:
70+
- Increase the mid to make larger groups possible.
71+
- Continue adjusting the mid until left < right. Return left as it contains the minimized largest possible sum.
72+
73+
- Let’s look at the following illustrations to get a better understanding of the solution:
74+
75+
![Solution 1](images/solutions/split_array_largest_sum_solution_1.png)
76+
![Solution 2](images/solutions/split_array_largest_sum_solution_2.png)
77+
![Solution 3](images/solutions/split_array_largest_sum_solution_3.png)
78+
![Solution 4](images/solutions/split_array_largest_sum_solution_4.png)
79+
![Solution 5](images/solutions/split_array_largest_sum_solution_5.png)
80+
![Solution 6](images/solutions/split_array_largest_sum_solution_6.png)
81+
![Solution 7](images/solutions/split_array_largest_sum_solution_7.png)
82+
![Solution 8](images/solutions/split_array_largest_sum_solution_8.png)
83+
![Solution 9](images/solutions/split_array_largest_sum_solution_9.png)
84+
![Solution 10](images/solutions/split_array_largest_sum_solution_10.png)
85+
![Solution 11](images/solutions/split_array_largest_sum_solution_11.png)
86+
![Solution 12](images/solutions/split_array_largest_sum_solution_12.png)
87+
![Solution 13](images/solutions/split_array_largest_sum_solution_13.png)
88+
![Solution 14](images/solutions/split_array_largest_sum_solution_14.png)
89+
![Solution 15](images/solutions/split_array_largest_sum_solution_15.png)
90+
![Solution 16](images/solutions/split_array_largest_sum_solution_16.png)
91+
![Solution 17](images/solutions/split_array_largest_sum_solution_17.png)
92+
![Solution 18](images/solutions/split_array_largest_sum_solution_18.png)
93+
![Solution 19](images/solutions/split_array_largest_sum_solution_19.png)
94+
![Solution 20](images/solutions/split_array_largest_sum_solution_20.png)
95+
96+
### Time Complexity
97+
98+
The time complexity of this solution is O(n log(m)), where n is the length of the input array, and m is the difference
99+
between `max(nums)` and `sum(nums)` because the range of possible sums considered during the binary search is from
100+
`max(nums)` to `sum(nums)`. This range size determines the number of iterations in the binary search. The tighter this
101+
range, the fewer iterations are needed. However, in the worst case, it spans the full difference: `sum(nums) - max(nums)`.
102+
The time complexity becomes `n log(m)` because the `can_split` function is called `n` times for each iteration.
103+
104+
### Space Complexity
105+
106+
The space complexity of this solution is O(1) because only constant space is used.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from typing import List
2+
3+
4+
def split_array(nums: List[int], k: int) -> int:
5+
if not nums:
6+
return -1
7+
8+
left = max(nums)
9+
right = sum(nums)
10+
first_true_index = -1
11+
12+
def feasible(max_sum: int) -> bool:
13+
"""
14+
Check if we can split the array into at most k sub arrays with each sub array sum less than or equal to max_sum
15+
"""
16+
current_sum = 0
17+
# Start with one
18+
sub_array_count = 1
19+
20+
for num in nums:
21+
if current_sum + num > max_sum:
22+
current_sum = num
23+
sub_array_count += 1
24+
else:
25+
current_sum += num
26+
27+
return sub_array_count <= k
28+
29+
while left <= right:
30+
mid = (right + left) // 2
31+
if feasible(mid):
32+
first_true_index = mid
33+
# Find a smaller valid value
34+
right = mid - 1
35+
else:
36+
# Find a larger valid value
37+
left = mid + 1
38+
39+
return first_true_index
40+
41+
42+
def split_array_2(nums, k):
43+
if not nums:
44+
return -1
45+
46+
# Set the initial search range for the largest sum:
47+
# Minimum is the largest number in the array, and maximum is the sum of all numbers
48+
left, right = max(nums), sum(nums)
49+
50+
def can_split(middle: int):
51+
# Initialize the count of subarrays and the current sum of the current subarray
52+
subarrays = 1
53+
current_sum = 0
54+
55+
for num in nums:
56+
# Check if adding the current number exceeds the allowed sum (mid)
57+
if current_sum + num > middle:
58+
# Increment the count of subarrays
59+
subarrays += 1
60+
# Start a new subarray with the current number
61+
current_sum = num
62+
63+
# If the number of subarrays exceeds the allowed k, return False
64+
if subarrays > k:
65+
return False
66+
else:
67+
# Otherwise, add the number to the current subarray
68+
current_sum += num
69+
70+
# Return True if the array can be split within the allowed subarray count
71+
return True
72+
73+
# Perform binary search to find the minimum largest sum
74+
while left < right:
75+
# Find the middle value of the current range
76+
mid = (left + right) // 2
77+
78+
# Check if the array can be split into k or fewer subarrays with this maximum sum
79+
if can_split(mid):
80+
# If possible, try a smaller maximum sum
81+
right = mid
82+
else:
83+
# Otherwise, increase the range to allow larger sums
84+
left = mid + 1
85+
86+
# Return the smallest maximum sum that satisfies the condition
87+
return left
91 KB
Loading
80.3 KB
Loading
74.1 KB
Loading

0 commit comments

Comments
 (0)