Skip to content

Commit 1e3f6c3

Browse files
committed
Add solutions to new binary search problem
1 parent 4a99a2a commit 1e3f6c3

11 files changed

+605
-85
lines changed

.cursor/rules/note-items.mdc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ description:
33
globs:
44
alwaysApply: true
55
---
6-
## My Notes
6+
## Note Items
77

8-
If I'm asking you to help modify or improve my notes, please provide:
8+
If I'm asking you to help modify or improve my notes, please provide the following sections and notes:
99

1010
### Hints
1111
- A lightweight nudge without revealing too much.
@@ -14,6 +14,7 @@ If I'm asking you to help modify or improve my notes, please provide:
1414
### Breakdowns
1515
- Break down the problem into smaller, simpler sub-problems.
1616
- Answer questions like: _"Can we solve a simpler version first?"_ or _"Can we divide the problem into smaller steps?"_
17+
1718
### Key Insights
1819
- Share the intuitions and key observations that lead to different approaches.
1920
- Explain _how to come up with the idea_. When is it a good idea to think of this technique? I want to know the thought process and train my instinct when encountering new problems.

.cursor/rules/note-overall.mdc

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,27 @@ description:
33
globs:
44
alwaysApply: true
55
---
6-
## Overall
6+
## Overall Rules for Notes
77
- I'm practicing LeetCode in **Kotlin**. Please always give me **Kotlin** solutions.
88
- Please **keep the Kotlin code simple and clean**, preferring interview-friendly style (no unnecessary Kotlin tricks or extension functions).
9-
- For **binary search**, always use the inclusive search range: `while (left <= right)`.
109
- You must **search on `leetcode.com` and `leetcode.cn`** for that problem, find the most **popular and up-voted solutions**, **summarize them**, and **provide the best one**, if there are multiple popular solutions, please also include them. (At most 3 solutions)
11-
- If there are valuable insights from **up-voted comments** (in either Chinese or English), please also include them.
1210
- Please **reference any related online resources** if helpful, and ensure the final solution is **correct (AC)** and **robust across edge cases**.
11+
- If there are valuable insights from **up-voted comments** (in either Chinese or English) during the search, please also include them.
1312
- Please use the word "two pointers", not "two-pointer" or "two-pointers" in any description.
14-
- If the descriptions are sentence, please add `.` at the end of sentence, especially for single sentence.
13+
- If the descriptions are sentence, please add `.` at the end of sentence, especially for single sentence.
14+
15+
## Existing Note (If Exists)
16+
- If I have some existing notes / implementations / WA, please preserve it in a clearly labeled "My Original Notes" section at the end of the file. Then also provide your implementation with comments or explain (from your search result above)
17+
- For the existing notes reference, please use file link `@[email protected]`, don't prefix `mdc:leetcode/`, and please verify to ensure that file exists from the link reference.
18+
- For the implementation, don't write the line comment: `// Used in solutions`.
19+
20+
> The following sections is topic specific, please always follow if the problem or approach fits the topic
21+
22+
## Binary Search
23+
- For **binary search**, always use the inclusive search range: `while (left <= right)`.
24+
- Please always elaborate further the key intuition, explain how to observe/analyze/breakdown the problem and figure out that we can solve this problem by binary search approach.
25+
- For the problem type: "binary search on value", please always provide the following items in `Key Insights` section:
26+
- Monotonicity
27+
- Lower bound
28+
- Upper bound
29+
- Feasibility
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# [1283. Find the Smallest Divisor Given a Threshold](https://leetcode.com/problems/find-the-smallest-divisor-given-a-threshold/)
2+
3+
## Hints
4+
- What happens to the sum if you increase the divisor? Try a few examples.
5+
- Can you check if a given divisor is feasible in `O(n)`?
6+
7+
## Key Insights
8+
The core intuition is to recognize that as the divisor increases, the sum of all `ceil(num / divisor)` strictly decreases. This is because dividing by a larger number always produces a smaller (or equal) result for each element, and rounding up preserves this monotonicity. This monotonic relationship is the key to applying binary search.
9+
10+
How do we observe this?
11+
- Try a few divisors by hand from linear search: for small divisors, the sum is large; for large divisors, the sum is small.
12+
- For example, with `nums = [1,2,5,9]` and `threshold = 6`,
13+
- divisor = 1: sum = 17
14+
- divisor = 4: sum = 7
15+
- divisor = 5: sum = 5
16+
- As divisor increases, sum decreases.
17+
18+
How do we analyze the feasibility?
19+
- For any fixed divisor, we can check in `O(n)` if the sum of all `ceil(num / divisor)` is `<= threshold`.
20+
- If a divisor is feasible (sum <= threshold), then any larger divisor will also be feasible (sum will be even smaller).
21+
- If a divisor is not feasible, any smaller divisor will also not be feasible (sum will be even larger).
22+
23+
**Summary:**
24+
- When you see a problem where increasing a parameter makes a condition easier/harder in a predictable way, and you can check feasibility efficiently, consider binary search on that parameter.
25+
- Here, the divisor is the parameter, and the sum is monotonic with respect to it, so binary search is the optimal approach.
26+
27+
## Binary Search
28+
Use binary search to find the smallest divisor such that the sum of all `ceil(num / divisor)` is `<= threshold`.
29+
30+
- Monotonicity: `divisor` is greater, `sum` is less <= `threshold`, more feasible.
31+
- Lower bound: `1`, smallest possible divisor.
32+
- Upper bound: `max(nums)`, the number can divide all numbers into `1` (the sum is the sum of all numbers).
33+
- Feasibility: For a given divisor, is the sum of all `ceil(num / divisor)` <= `threshold`?
34+
35+
> For the check, you can use integer math: `ceil(num / divisor)` is equivalent to `(num + divisor - 1) / divisor`.
36+
37+
```kotlin
38+
fun smallestDivisor(nums: IntArray, threshold: Int): Int {
39+
var left = 1
40+
var right = nums.max()
41+
while (left <= right) {
42+
val middle = left + (right - left) / 2
43+
val feasible = check(nums, threshold, middle)
44+
if (feasible) {
45+
right = middle - 1
46+
} else {
47+
left = middle + 1
48+
}
49+
}
50+
return left
51+
}
52+
53+
private fun check(nums: IntArray, threshold: Int, divisor: Int): Boolean {
54+
var sum = 0
55+
for (num in nums) {
56+
sum += ceil(num * 1f / divisor * 1f).toInt()
57+
}
58+
return sum <= threshold
59+
}
60+
```
61+
62+
- **Time Complexity**: `O(n * log m)`, where `n` is the length of `nums`, `m` is the maximum value in `nums`.
63+
- **Space Complexity**: `O(1)`.
64+
65+
## Edge Cases & Pitfalls
66+
- `nums` contains only one element: The answer is `ceil(nums[0] / threshold)`.
67+
- `threshold` is equal to `nums.size`: The answer is `max(nums)`.
68+
- All elements in `nums` are the same: The answer is `ceil(nums[0] * nums.size / threshold)`.
69+
- **Large values**: Make sure to avoid integer overflow in the sum calculation.
70+
71+
## Pitfalls
72+
- Forgetting to use integer math for `ceil(num / divisor)`, which can lead to floating-point errors or inefficiency.

leetcode/153.find-minimum-in-rotated-sorted-array.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
## [153. Find Minimum in Rotated Sorted Array](https://leetcode.com/problems/find-minimum-in-rotated-sorted-array/)
22

3-
## Clarification Questions
4-
* Is the input array empty?
5-
63
## Test Cases
74
### Normal Cases
85
```
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# [1760. Minimum Limit of Balls in a Bag](https://leetcode.com/problems/minimum-limit-of-balls-in-a-bag/description/)
2+
3+
## Hints
4+
- What happens to the number of operations if you decrease the allowed maximum size of a bag?
5+
- Can you check if a given maximum size is feasible in `O(n)`?
6+
7+
## Breakdowns
8+
> 1. Can we check if a given maximum size is feasible with at most `maxOperations` splits?
9+
10+
Try simulating the process: for each bag, how many splits are needed to make all resulting bags ≤ the given size?
11+
12+
## Key Insights
13+
> NOTE: `penalty` is the maximum size of the bag.
14+
15+
- The key is to realize that as the allowed `penalty` decreases, we need more splits, the number of required operations (splits) increases. This is a classic monotonic property.
16+
- If the total operations needed is ≤ `maxOperations`, then `penalty` is feasible. If not, we need to allow a larger `penalty`.
17+
- For a given `penalty`, the number of operations needed is the sum over all bags of `parts - 1`, which is `ceil(num / penalty) - 1`. This counts how many splits are needed for each bag to ensure all resulting bags are ≤ `penalty`:
18+
19+
```js
20+
penalty = 3
21+
8 = [3, 3, 2] 3 - 1
22+
9 = [3, 3, 3] 3 - 1
23+
10 = [3, 3, 3, 1] 4 - 1
24+
11 = [3, 3, 3, 2] 4 - 1
25+
12 = [3, 3, 3, 3] 4 - 1
26+
13 = [3, 3, 3, 3, 1] 5 - 1
27+
```
28+
29+
As above example, 13 needs 5 parts `ceil(13 / 3) = 5`, it needs 4 operations.
30+
31+
> For each bag, the number of splits needed to make all resulting bags ≤ penalty is `(num - 1) / penalty`. This avoids floating point and is more efficient than using `ceil(num / penalty) - 1`.
32+
33+
## Binary Search
34+
- **Monotonicity**: If a penalty is feasible, any larger penalty is also feasible. If not, any smaller penalty is not feasible.
35+
- **Lower bound**: `1`, we split as much as possible.
36+
- **Upper bound**: `max(nums)` (no split).
37+
- **Feasibility**: Given a `penality`, can we split the balls so that maximum of all balls ≤ `penality` and operations ≤ `maxOperations`?
38+
39+
```kotlin
40+
fun minimumSize(nums: IntArray, maxOperations: Int): Int {
41+
val maxOperations = maxOperations.toLong()
42+
var left = 1L
43+
var right = nums.max().toLong()
44+
while (left <= right) {
45+
val middle = left + (right - left) / 2
46+
val feasible = check(nums, maxOperations, middle)
47+
if (feasible) {
48+
right = middle - 1
49+
} else {
50+
left = middle + 1
51+
}
52+
}
53+
return left.toInt()
54+
}
55+
56+
private fun check(nums: IntArray, maxOperations: Long, max: Long): Boolean {
57+
var operations = 0L
58+
for (num in nums) {
59+
val parts = ceil(num * 1.0 / max).toInt()
60+
operations += parts - 1
61+
}
62+
return operations <= maxOperations
63+
}
64+
```
65+
66+
- **Time Complexity**: `O(n * log(max(nums)))`, where `n` is the number of bags.
67+
- **Space Complexity**: `O(1)`.
68+
69+
## Edge Cases
70+
- All bags are already ≤ the answer: No operation needed.
71+
- `maxOperations` is 0: The answer is `max(nums)`.
72+
- **Large values in `nums`**: Use integer division to avoid overflow and floating point errors.
73+
- Only one bag: The answer is `ceil(num / (maxOperations + 1))`.
74+
75+
## WA
76+
My original idea is `9` = `[8,1] / [7,2] / [6,3] / [5,4]`, `num / 2` leads optimal split.
77+
78+
But splitting into equal halves is not optimal, for example, `9 -> [4, 5] -> [2, 2, 5] -> [2, 2, 2, 3]` which needs 3 operations. But `9 -> [6, 3] -> [3, 3, 3]` which needs 2 operations that is optimal.
79+
80+
```kotlin
81+
private fun checkWA(nums: IntArray, maxOperations: Long, max: Long): Boolean {
82+
var operations = 0L
83+
val maxHeap = PriorityQueue(reverseOrder<Int>())
84+
for (num in nums) {
85+
maxHeap.add(num)
86+
}
87+
/**
88+
Give a number, how many operations are needed to split num <= max
89+
9 -> [5, 4] -> [2, 3, 4] -> [2, 3, 2, 2]
90+
91+
7 -> [4, 3] -> [2, 2, 3]
92+
num / 2
93+
94+
[2, 4, 8, 2]
95+
96+
97+
*/
98+
while (maxHeap.peek() > max) {
99+
val num = maxHeap.poll()
100+
val half = num / 2
101+
maxHeap.add(half)
102+
maxHeap.add(num - half)
103+
operations++
104+
if (operations > maxOperations) return false
105+
}
106+
return operations <= maxOperations
107+
}
108+
```
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# [2439. Minimize Maximum of Array](https://leetcode.com/problems/minimize-maximum-of-array/description/)
2+
3+
## Hints
4+
- What happens if you always move excess from right to left?
5+
- Can you check if a given maximum value is feasible using a greedy simulation?
6+
- Is there a way to relate the answer to prefix sums or averages?
7+
8+
## Breakdowns
9+
> 1. Can we check if a given value is feasible as the maximum after all operations?
10+
11+
Try simulating the process from right to left, moving excess to the left. If the first element can be kept ≤ threshold, it's feasible.
12+
13+
> 2. Is there a direct formula for the answer?
14+
15+
Notice that after all possible operations, the array becomes as flat as possible. The answer is the maximum of all prefix averages (rounded up).
16+
17+
## Key Insights
18+
- The operation allows you to move any excess from right to left, so the leftmost elements can absorb all surplus.
19+
- The minimum possible maximum is determined by the largest prefix average (since you can't make the prefix sum smaller than its average).
20+
- (TODO) There is also a direct greedy solution using prefix averages, which is more efficient and elegant.
21+
22+
## Binary Search
23+
- **Monotonicity**: If a value is feasible, any larger value is also feasible.
24+
- **Lower bound**: The minimum value in the array would be acceptable. (Consider the single element case)
25+
- **Upper bound**: The maximum value in the array. (Without any modification)
26+
- **Feasibility**: There are two ways to check if a value is feasible:
27+
- Check 1: Check if we have enough buffer to absorb all excess.
28+
- Check 2: Simulate moving excess from right to left and check if the first element stays ≤ threshold.
29+
30+
```kotlin
31+
fun minimizeArrayValue(nums: IntArray): Int {
32+
var left = nums.min()
33+
var right = nums.max()
34+
while (left <= right) {
35+
val middle = left + (right - left) / 2
36+
val feasible = check(nums, middle)
37+
if (feasible) {
38+
right = middle - 1
39+
} else {
40+
left = middle + 1
41+
}
42+
}
43+
return left
44+
}
45+
46+
// Check 1: Check if we have enough buffer to absorb all excess.
47+
private fun check(nums: IntArray, threshold: Int): Boolean {
48+
var buffer = 0L
49+
for (num in nums) {
50+
buffer += (threshold - num).toLong()
51+
if (buffer < 0L) return false
52+
}
53+
return true
54+
}
55+
56+
// Or alternatively, check 2: Simulate moving excess from right to left and check if the first element stays ≤ threshold.
57+
private fun check2(nums: IntArray, threshold: Int): Boolean {
58+
var excess = 0L
59+
for (i in nums.size - 1 downTo 1) {
60+
if (nums[i] + excess > threshold) {
61+
excess = nums[i] + excess - threshold
62+
} else {
63+
// We can't pass negative excess to the left because:
64+
// 1. The operation only allows moving excess from right to left
65+
// 2. If we have a deficit (negative excess), we can't "borrow" from the left
66+
// 3. The left elements can only absorb surplus, not create it
67+
excess = 0L
68+
}
69+
}
70+
return nums[0] + excess <= threshold
71+
}
72+
```
73+
74+
- **Time Complexity**: `O(n log(max(nums)))`
75+
- **Space Complexity**: `O(1)`
76+
77+
## Prefix Average (Optimal Greedy)
78+
79+
> TODO: Understand this approach later.
80+
81+
Key idea: The answer is the maximum of all prefix averages (rounded up).
82+
83+
```kotlin
84+
fun minimizeArrayValue(nums: IntArray): Int {
85+
var maxAvg = 0L
86+
var prefixSum = 0L
87+
for (i in nums.indices) {
88+
prefixSum += nums[i]
89+
val avg = (prefixSum + i) / (i + 1) // ceil division
90+
maxAvg = maxOf(maxAvg, avg)
91+
}
92+
return maxAvg.toInt()
93+
}
94+
```
95+
96+
- **Time Complexity**: `O(n)`
97+
- **Space Complexity**: `O(1)`
98+
99+
## Edge Cases
100+
- All elements are the same: The answer is that value.
101+
- Large values at the end: The excess will be pushed to the leftmost element.
102+
- Single element: The answer is the element itself.
103+
- Very large input: Use `Long` for prefix sum to avoid overflow.
104+
105+
## Pitfalls
106+
- **Forgetting to use `Long` for prefix sum or excess, causing overflow.**
107+
- **Not resetting excess to 0 when redistributing in the binary search check.**
108+
- Off-by-one error in prefix average calculation (should use `(prefixSum + i) / (i + 1)` for ceiling division).
109+
110+
## WA
111+
```kotlin
112+
fun minimizeArrayValue(nums: IntArray): Int {
113+
// Binary search bounds are correct - we search between min and max values
114+
var left = nums.min()
115+
var right = nums.max()
116+
while (left <= right) {
117+
val middle = left + (right - left) / 2
118+
val feasible = check(nums, middle)
119+
if (feasible) {
120+
right = middle - 1
121+
} else {
122+
left = middle + 1
123+
}
124+
}
125+
return left
126+
}
127+
128+
private fun check(nums: IntArray, threshold: Int): Boolean {
129+
// ISSUE 1: Modifying input array directly is dangerous and can affect future checks
130+
// ISSUE 2: The logic doesn't properly handle the case where we need to move values from right to left
131+
var max = nums.last()
132+
for (i in nums.size - 1 downTo 0) {
133+
if (i > 0) {
134+
while (nums[i - 1] < nums[i]) {
135+
nums[i]--
136+
nums[i - 1]++
137+
}
138+
}
139+
max = maxOf(max, nums[i])
140+
if (threshold < max) return false
141+
}
142+
return true
143+
}
144+
```

0 commit comments

Comments
 (0)