Skip to content

Commit 4fd4e56

Browse files
committed
add python solutions for binary search variants pattern
1 parent 3a3230e commit 4fd4e56

File tree

5 files changed

+581
-0
lines changed

5 files changed

+581
-0
lines changed

python/find-peak-element.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Find Peak Element
2+
A peak element is an element that is strictly greater than its neighbors.
3+
4+
Given an integer array nums, find a peak element, and return its index. If the array contains multiple peaks, return the index to any of the peaks.
5+
6+
You may imagine that nums[-1] = -∞ and nums[n] = -∞ (i.e., the boundary elements are considered lower than all elements).
7+
8+
You must write an algorithm that runs in O(log n) time.
9+
### Constraints:
10+
- 1 <= nums.length <= 1000
11+
- -2^31 <= nums[i] <= 2^31 - 1
12+
- nums[i] != nums[i + 1] for all valid i.
13+
14+
### Examples
15+
```javascript
16+
Input: nums = [1,2,3,1]
17+
Output: 2
18+
Explanation: 3 is a peak element and its index is 2.
19+
20+
Input: nums = [1,2,1,3,5,6,4]
21+
Output: 5
22+
Explanation: Your function can return index 5 (the element 6 is a peak) or index 1 (the element 2 is a peak).
23+
```
24+
25+
## Approaches to Solve the Problem
26+
### Approach 1: Linear Search (Brute Force)
27+
##### Intuition:
28+
A simple approach is to traverse the array linearly and check for each element if it is greater than both of its neighbors. If such an element is found, return its index.
29+
30+
Steps:
31+
1. Iterate through the array from 0 to n - 1.
32+
2. For each element nums[i], check if it is greater than both nums[i-1] and nums[i+1] (handling boundary cases where i == 0 or i == n - 1).
33+
3. If an element satisfies this condition, return the index i.
34+
##### Time Complexity:
35+
O(n), where n is the length of the array. In the worst case, we may need to scan the entire array.
36+
##### Space Complexity:
37+
O(1), because we only use a few variables to track indices and comparisons.
38+
##### Python Code:
39+
```python
40+
def findPeakElement(nums):
41+
for i in range(len(nums)):
42+
if (i == 0 or nums[i] > nums[i - 1]) and (i == len(nums) - 1 or nums[i] > nums[i + 1]):
43+
return i
44+
```
45+
### Approach 2: Binary Search (Optimal Solution)
46+
##### Intuition:
47+
We can use binary search to find a peak element efficiently in O(log n) time. The key observation is that if nums[mid] is greater than nums[mid + 1], then the peak must lie on the left side (including mid), and if nums[mid] is less than nums[mid + 1], then the peak must lie on the right side (excluding mid).
48+
49+
The reason behind this is that if the middle element is smaller than its neighbor, there must be a peak on the side with the larger neighbor, because eventually the array must "fall" to the boundaries where we consider the elements as -∞.
50+
51+
Steps:
52+
1. Initialize two pointers left = 0 and right = len(nums) - 1.
53+
2. While left < right:
54+
- Calculate the midpoint: mid = (left + right) // 2.
55+
- If nums[mid] > nums[mid + 1], it means the peak is in the left half (including mid), so move the right pointer to mid.
56+
- If nums[mid] < nums[mid + 1], it means the peak is in the right half (excluding mid), so move the left pointer to mid + 1.
57+
3. Once left == right, return left (or right), which points to a peak element.
58+
##### Visualization:
59+
For nums = [1, 2, 1, 3, 5, 6, 4]:
60+
61+
```perl
62+
Initial: left = 0, right = 6
63+
Iteration 1: mid = 3 → nums[mid] = 3, nums[mid + 1] = 5 → move left to mid + 1
64+
Iteration 2: mid = 5 → nums[mid] = 6, nums[mid + 1] = 4 → move right to mid
65+
Final result: left = 5, right = 5 → peak is at index 5 with value 6
66+
```
67+
##### Time Complexity:
68+
O(log n), where n is the length of the array. We reduce the search space by half with each iteration.
69+
##### Space Complexity:
70+
O(1), because we only use a few variables for the binary search process.
71+
##### Python Code:
72+
```python
73+
def findPeakElement(nums):
74+
left, right = 0, len(nums) - 1
75+
76+
while left < right:
77+
mid = (left + right) // 2
78+
if nums[mid] > nums[mid + 1]:
79+
right = mid
80+
else:
81+
left = mid + 1
82+
83+
return left # or return right, both will point to the peak
84+
```
85+
### Edge Cases:
86+
1. Single Element: If the array has only one element, that element is trivially the peak.
87+
2. Two Elements: If the array has two elements, the peak is the larger of the two.
88+
3. All Increasing or Decreasing: If the array is strictly increasing, the last element is the peak. If the array is strictly decreasing, the first element is the peak.
89+
## Summary
90+
91+
| Approach | Time Complexity | Space Complexity |
92+
|-----------------------------------|-----------------|------------------|
93+
| Brute Force | O(n) | O(1) |
94+
| Binary Search with Pivot Handling | O(log n) | O(1) |
95+
96+
The Binary Search approach is the most efficient solution, achieving O(log n) time complexity while using constant space. This solution takes advantage of the properties of the array and uses binary search logic to efficiently find a peak.

python/koko-eating-bananas.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Koko Eating Bananas
2+
Koko loves to eat bananas. There are n piles of bananas, and the i-th pile has piles[i] bananas. The guards have gone, and Koko has h hours to eat all the bananas. Each hour, she chooses a pile of bananas and eats k bananas from that pile. If the pile has fewer than k bananas, she eats all of them instead, and there will be no bananas left in that pile for the next hour.
3+
4+
Return the minimum integer k such that Koko can eat all the bananas within h hours.
5+
### Constraints:
6+
- 1 <= piles.length <= 10^4
7+
- piles.length <= h <= 10^9
8+
- 1 <= piles[i] <= 10^9
9+
10+
### Examples
11+
```javascript
12+
Input: piles = [3,6,7,11], h = 8
13+
Output: 4
14+
15+
Input: piles = [30,11,23,4,20], h = 5
16+
Output: 30
17+
18+
Input: piles = [30,11,23,4,20], h = 6
19+
Output: 23
20+
```
21+
22+
## Approaches to Solve the Problem
23+
### Approach 1: Brute Force (Inefficient)
24+
##### Intuition:
25+
In a brute-force approach, we can iterate over all possible eating speeds k from 1 to the maximum number of bananas in the largest pile, checking for each speed if Koko can eat all the bananas in h hours. However, this approach is inefficient because it involves checking many values of k and can lead to a time complexity of O(n * max(piles)), where n is the number of piles.
26+
27+
Steps:
28+
1. Iterate through possible values of k starting from 1.
29+
2. For each k, simulate the process to see if Koko can finish eating all piles within h hours.
30+
3. Return the smallest k that satisfies the condition.
31+
##### Time Complexity:
32+
O(n * max(piles)), where n is the number of piles and max(piles) is the largest pile size.
33+
##### Space Complexity:
34+
O(1), because we only use a few variables to track the possible values of k and time.
35+
##### Python Code:
36+
```python
37+
def can_eat_in_time(piles, h, k):
38+
hours = 0
39+
for pile in piles:
40+
hours += (pile + k - 1) // k # Equivalent to ceil(pile / k)
41+
return hours <= h
42+
43+
def minEatingSpeed(piles, h):
44+
for k in range(1, max(piles) + 1):
45+
if can_eat_in_time(piles, h, k):
46+
return k
47+
```
48+
### Approach 2: Binary Search on Eating Speed (Optimal Solution)
49+
##### Intuition:
50+
We can use binary search to efficiently find the minimum possible eating speed k. The possible values for k range from 1 to the largest pile size in piles. The idea is to apply binary search over this range to minimize k while ensuring that Koko can finish all bananas within h hours.
51+
52+
- Lower bound (low): The smallest possible speed is 1 banana per hour.
53+
- Upper bound (high): The maximum possible speed is the size of the largest pile (max(piles)), since Koko can finish the largest pile in one hour at this speed.
54+
55+
Steps:
56+
1. Initialize low = 1 and high = max(piles).
57+
2. Perform binary search:
58+
- Calculate the middle point mid = (low + high) // 2.
59+
- Check if Koko can finish all the bananas at speed mid using the helper function can_eat_in_time(piles, h, mid).
60+
- If she can finish, update high = mid to try smaller values.
61+
- If she cannot finish, update low = mid + 1 to increase the eating speed.
62+
3. The binary search will converge, and low will be the minimum speed k.
63+
##### Time Complexity:
64+
O(n log max(piles)), where n is the number of piles, and log max(piles) comes from the binary search.
65+
##### Space Complexity:
66+
O(1), because we only use a few variables for binary search and time calculations.
67+
##### Python Code:
68+
```python
69+
def can_eat_in_time(piles, h, k):
70+
hours = 0
71+
for pile in piles:
72+
hours += (pile + k - 1) // k # Equivalent to ceil(pile / k)
73+
return hours <= h
74+
75+
def minEatingSpeed(piles, h):
76+
# Binary search on the possible values of k
77+
low, high = 1, max(piles)
78+
79+
while low < high:
80+
mid = (low + high) // 2
81+
if can_eat_in_time(piles, h, mid):
82+
high = mid # Try for smaller k
83+
else:
84+
low = mid + 1 # Increase k
85+
86+
return low
87+
```
88+
### Explanation of Code:
89+
- Binary Search:
90+
The binary search starts with a range of possible eating speeds from 1 to max(piles). We progressively narrow down this range by checking the midpoint (mid) using the can_eat_in_time function, which simulates the process of eating bananas at speed mid.
91+
92+
- Helper Function can_eat_in_time(piles, h, k):
93+
This function calculates how many hours it would take Koko to finish eating all the piles if she eats k bananas per hour. For each pile, the time taken is ceil(pile / k), which is computed as (pile + k - 1) // k (this avoids using floating-point division).
94+
95+
- Binary Search Condition:
96+
If Koko can finish eating all bananas at speed mid, we reduce the search space to find a potentially smaller value of k. Otherwise, we increase the speed and continue searching.
97+
### Edge Cases:
98+
1. Single Pile: If there is only one pile, the eating speed is equal to the size of the pile if h == 1. Otherwise, it will depend on how many hours Koko has.
99+
2. Many Small Piles, Few Hours: When there are many small piles but few hours, the binary search should quickly find the largest k needed to eat all piles in the limited time.
100+
3. Large Pile, Many Hours: If there's a very large pile and many hours, Koko can eat slowly, so the binary search should return a small k.
101+
## Summary
102+
| Approach | Time Complexity | Space Complexity |
103+
|-----------------------------------|-----------------|------------------|
104+
| Brute Force | O(n * max(piles)) | O(1) |
105+
| Binary Search (Optimal) | O(n log max(piles)) | O(1) |
106+
107+
The Binary Search approach is the most efficient, reducing the time complexity to O(n log max(piles)) while using constant space.

python/random-pick-with-weight.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Random Pick with Weight
2+
You are given an array of positive integers w where w[i] describes the weight of index i (0-indexed). You need to implement the function pickIndex(), which randomly picks an index in proportion to its weight.
3+
4+
- pickIndex() should return an index such that the probability of picking index i is w[i] / sum(w).
5+
### Constraints:
6+
- 1 <= w.length <= 10^4
7+
- 1 <= w[i] <= 10^5
8+
- pickIndex will be called at most 10^5 times.
9+
10+
### Examples
11+
```javascript
12+
Input:
13+
["Solution","pickIndex"]
14+
[[[1]],[]]
15+
16+
Output:
17+
[null,0]
18+
19+
Explanation:
20+
Solution solution = new Solution([1]);
21+
solution.pickIndex(); // return 0
22+
Since there is only one element, the function should always return 0.
23+
24+
25+
Input:
26+
["Solution","pickIndex","pickIndex","pickIndex","pickIndex","pickIndex"]
27+
[[[1,3]],[],[],[],[],[]]
28+
29+
Output:
30+
[null,1,1,1,1,0]
31+
32+
Explanation:
33+
Solution solution = new Solution([1,3]);
34+
solution.pickIndex(); // return 1 with probability 3/4, and return 0 with probability 1/4.
35+
The picks are made randomly, so this output is just an example.
36+
```
37+
38+
## Approaches to Solve the Problem
39+
### Approach 1: Brute Force (Inefficient)
40+
##### Intuition:
41+
The simplest solution is to map each index i to a number of occurrences proportional to its weight w[i]. Then, randomly select an index from this expanded array. This is very inefficient in terms of both space and time, especially for large weights.
42+
43+
Steps:
44+
1. Create a list where each index i appears w[i] times.
45+
2. Randomly pick an index from this expanded list.
46+
##### Time Complexity:
47+
O(n * w_max), where n is the length of the input array and w_max is the maximum weight value. This is very inefficient for large arrays or large weights.
48+
##### Space Complexity:
49+
O(n * w_max), because we need to store the expanded list.
50+
##### Python Code:
51+
```python
52+
import random
53+
54+
class Solution:
55+
def __init__(self, w):
56+
self.weights = []
57+
for i in range(len(w)):
58+
self.weights += [i] * w[i]
59+
60+
def pickIndex(self):
61+
return random.choice(self.weights)
62+
```
63+
### Approach 2: Prefix Sum + Binary Search (Optimal Solution)
64+
##### Intuition:
65+
A more efficient solution involves using the prefix sum of the weights to create a cumulative sum array. The idea is to transform the weights into a continuous probability distribution, where each weight occupies a segment proportional to its size.
66+
67+
- Construct a prefix sum array where each element at index i represents the cumulative weight up to that index.
68+
- Use binary search to efficiently find which segment of the cumulative sum a randomly generated number falls into.
69+
70+
Steps:
71+
1. Precompute the prefix sum of the weights.
72+
- Create a cumulative sum array where prefix_sum[i] = w[0] + w[1] + ... + w[i].
73+
- This transforms the weights into a continuous range.
74+
2. Generate a random number r between 0 and total_sum (where total_sum is the sum of all weights).
75+
3. Use binary search to find the first index i such that prefix_sum[i] >= r.
76+
4. Return the index i.
77+
##### Visualization:
78+
```perl
79+
For w = [1, 3], the prefix sum array will be [1, 4], meaning:
80+
81+
Index 0 has a range of [0, 1) (probability 1/4).
82+
Index 1 has a range of [1, 4) (probability 3/4).
83+
```
84+
##### Time Complexity:
85+
- O(n) for the precomputation step (building the prefix sum array).
86+
- O(log n) for each call to pickIndex() because of the binary search.
87+
##### Space Complexity:
88+
- O(n), where n is the length of the input array, for storing the prefix sum array.
89+
##### Python Code:
90+
```python
91+
import random
92+
import bisect
93+
94+
class Solution:
95+
def __init__(self, w):
96+
self.prefix_sums = []
97+
prefix_sum = 0
98+
99+
for weight in w:
100+
prefix_sum += weight
101+
self.prefix_sums.append(prefix_sum)
102+
103+
self.total_sum = prefix_sum
104+
105+
def pickIndex(self):
106+
# Generate a random number in the range [1, total_sum]
107+
target = random.randint(1, self.total_sum)
108+
109+
# Binary search to find the index where the random number fits
110+
return bisect.bisect_left(self.prefix_sums, target)
111+
```
112+
### Explanation of Code:
113+
- Precomputation (Constructor):
114+
The constructor builds the prefix_sums array and calculates the total sum of weights. This preprocessing is done in O(n) time.
115+
116+
- pickIndex() Function:
117+
In the pickIndex() function, a random integer between 1 and total_sum is generated, representing a point in the cumulative probability distribution. Then, bisect.bisect_left is used to perform binary search on the prefix_sums array to efficiently find the index corresponding to this point. This is done in O(log n) time.
118+
### Edge Cases:
119+
1. Single Weight: If the input array has only one element, pickIndex() should always return 0 because there's only one possible index.
120+
2. Large Weights: The solution efficiently handles large weights using the prefix sum approach without expanding the list.
121+
3. Small Weights: Even with small weights, the solution scales well because the binary search operates on the prefix sums rather than on the original weights.
122+
## Summary
123+
| Approach | Time Complexity | Space Complexity |
124+
|-----------------------------------|-----------------|------------------|
125+
| Brute Force | O(n * w_max) | O(n * w_max) |
126+
| Prefix Sum + Binary Search (Optimal) | O(n) for preprocessing, O(log n) for each pick | O(n) |
127+
128+
The Prefix Sum + Binary Search approach is the most efficient solution. It achieves O(n) time for preprocessing and O(log n) time for each call to pickIndex().

0 commit comments

Comments
 (0)