1891. Cutting Ribbons
Problem Description
In this problem, you are given an array of integers named ribbons
, where each element represents the length of a particular ribbon. Additionally, you are given an integer k
, which represents the target number of ribbons that you want to obtain. The challenge is to cut these ribbons into segments of equal length. Importantly, the length of each segment must be a positive integer, and you're looking for the maximum possible length of these segments that will allow you to create exactly k
segments.
You have the flexibility to cut the ribbons in various ways or even choose not to cut a particular ribbon at all. Any leftover ribbon after achieving your target of k
segments can be discarded. If it is not possible to get k
segments with the same positive integer length, then the answer will be 0
.
The task is to determine the longest segment length that you can achieve to get k
equal-length segments from the initial ribbons.
Intuition
To arrive at the solution for this problem, we utilize a binary search approach due to the monotonic nature of the problem's conditions. The key insight is that if you can make k
ribbons of a certain length x
, then you can certainly make k
ribbons of any length less than x
. This forms the basis of a binary search where we can efficiently narrow down the maximum length that meets the condition.
The binary search starts with the left boundary at 0
and the right boundary at the maximum length of the ribbons available, since the segment length cannot exceed the longest ribbon that we have. We continuously adjust our search range based on whether the mid-value we are testing allows us to obtain k
or more ribbons of that length. If it does, we have a potential solution and we try to find an even longer length by shifting our search range rightward (toward longer lengths). If it does not, we must look for shorter lengths by moving the search range leftward.
By repeating this process, our search range converges to the maximum length that allows us to obtain exactly k
ribbons of that length.
Learn more about Binary Search patterns.
Solution Approach
The solution employs a binary search algorithm to efficiently find the maximum length of ribbon that we can cut to achieve exactly k
segments of equal length. Binary search is chosen as the algorithm because of the monotonic relationship between ribbon length and the number of pieces — more ribbons can be obtained with shorter lengths and fewer with longer lengths. The purpose of binary search here is to maximize the length of the ribbons while still meeting the target quantity k
.
To employ binary search, we establish two pointers left
at 0 and right
at the maximum length in the ribbons
array, which represents the range within which we'll search for the maximum possible length. The algorithm proceeds iteratively, dividing this range by choosing a midpoint mid
. In binary search, mid is typically calculated as (left + right) / 2
, but in this solution, we use a slightly different formula, mid = (left + right + 1) >> 1
to avoid integer underflows when using larger int values, and shift the result rightward by one bit, which is equivalent to integer division by 2.
For each mid
value chosen during the binary search:
- We calculate
cnt
which is the total number of ribbons that can be cut at the givenmid
length. This is done by iterating through theribbons
array and dividing each ribbon's length bymid
(the proposed length). We use integer division here because we can only use complete segments. - If
cnt
is greater than or equal to (>=
)k
, it means that cutting the ribbons intomid
length segments satisfies our requirement for at leastk
segments. Therefore, we updateleft
tomid
, as there might exist a longer segment length that also meets the requirement. - If
cnt
is less than<
k
, then we can't obtain enough ribbons at the proposed length and have to try shorter lengths. Hence, we set the newright
tomid - 1
.
This search range narrowing down continues until left
and right
converge, meaning left
will point to the maximum length that we can use to cut the ribbons into exactly k
segments.
Finally, we return the left
pointer's value, as this will have converged to the correct maximum segment length through the iterations.
Overall, this solution approach takes advantage of the sorted and monotonic nature of the resultant ribbon lengths to apply binary search, which reduces the time complexity from potentially linear in nature to logarithmic — a significant efficiency boost, especially with larger input sizes.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's consider a small example with ribbons = [9, 7, 5]
and k = 5
. We want to cut these ribbons into 5 segments of equal length. We'll use the binary search approach described to find the maximum length of these segments.
-
Initialize
left = 0
andright = 9
(the longest ribbon length). -
The
mid
value is calculated as(left + right + 1) >> 1
. Initially, that would be(0 + 9 + 1) >> 1 = 5
. -
We check if we can get at least
k
segments of length5
from the ribbons. We have:9 // 5
gives us1
segment from the first ribbon7 // 5
gives us1
segment from the second ribbon5 // 5
gives us1
segment from the third ribbon In total, we have3
segments, which is less thank
, so we cannot use length5
.
-
Since the count of segments is less than
k
, we adjust theright
to bemid - 1
, which is now5 - 1 = 4
. -
Recalculate
mid
with the new boundaries:(left + right + 1) >> 1
which now is(0 + 4 + 1) >> 1 = 2
. -
Check if we can get at least
k
segments of length2
from the ribbons. This time, we have:9 // 2
gives us4
segments from the first ribbon7 // 2
gives us3
segments from the second ribbon5 // 2
gives us2
segments from the third ribbon In total,4 + 3 + 2
gives us9
segments, which is more thank
. We can use length2
, and there might be a possibility for a longer length, so we continue.
-
Now, we move the left boundary up to
mid
, makingleft = 2
. -
With the boundaries
left = 2
andright = 4
, we recalculatemid = (2 + 4 + 1) >> 1 = 3
. -
We check for length
3
:9 // 3
gives us3
segments7 // 3
gives us2
segments5 // 3
gives us1
segment In total, we get6
segments, which is again more thank
.
-
Since we can still get more than
k
segments, we moveleft
up tomid
, settingleft
to3
. -
Now
left
andright
are equal at4
, and we calculatemid
one more time:(3 + 4 + 1) >> 1 = 4
. -
Checking length
4
, we find:9 // 4
gives us2
segments7 // 4
gives us1
segment5 // 4
gives us1
segment The total is4
, which is less thank
.
-
Having found that length
4
is not enough, we adjustright
tomid - 1
, resulting inright = 3
, whereleft
was already3
.
At this point, the search range has narrowed such that left
and right
converge to 3
. We have determined that the maximum segment length that can be achieved to get exactly k
segments from the initial ribbons is 3
. Thus, we return 3
as the answer.
Solution Implementation
1from typing import List
2
3class Solution:
4 def maxLength(self, ribbons: List[int], k: int) -> int:
5 # Initialize the search boundaries.
6 left, right = 0, max(ribbons)
7
8 # Perform the binary search to find the maximum length.
9 while left < right:
10 # Calculate the middle value between left and right, rounding up.
11 mid = (left + right + 1) // 2
12
13 # Calculate the number of ribbons after cutting with the current mid length.
14 count = sum(ribbon // mid for ribbon in ribbons)
15
16 # If the current mid allows us to make at least k ribbons, search for a longer length.
17 # Otherwise, search for a shorter length.
18 if count >= k:
19 left = mid
20 else:
21 right = mid - 1
22
23 # Return the maximum length found where at least k ribbons can be made.
24 return left
25
1class Solution {
2 public int maxLength(int[] ribbons, int k) {
3 int minLength = 0; // Set the minimum possible length of a ribbon to 0.
4 int maxLength = 0; // This will hold the maximum length of a ribbon found in the array.
5
6 // Find the longest ribbon in the array.
7 for (int ribbonLength : ribbons) {
8 maxLength = Math.max(maxLength, ribbonLength);
9 }
10
11 // Establish a binary search to find the maximum length of the ribbon
12 // that can be used to cut into k pieces or more.
13 while (minLength < maxLength) {
14 // Calculate the middle length between the current minLength and maxLength,
15 // biased towards the upper bound to avoid infinite loop.
16 int midLength = (minLength + maxLength + 1) >>> 1;
17
18 // Initialize a counter to keep track of how many ribbons we can cut.
19 int count = 0;
20
21 // Calculate the number of ribbons that can be cut with midLength.
22 for (int ribbonLength : ribbons) {
23 count += ribbonLength / midLength; // Integer division automatically floors the result.
24 }
25
26 // If we can cut at least k ribbons of midLength, increase minLength.
27 // This implies we can try longer lengths.
28 if (count >= k) {
29 minLength = midLength;
30 } else {
31 // Otherwise, decrease maxLength to try shorter lengths.
32 maxLength = midLength - 1;
33 }
34 }
35 // Return the maximum length we found that can cut at least k ribbons.
36 return minLength;
37 }
38}
39
1class Solution {
2public:
3 // Function to find the maximum length of ribbon pieces that can be cut
4 // so that we have at least 'k' ribbon pieces.
5 int maxLength(vector<int>& ribbons, int k) {
6 // Initialize the 'left' boundary of our binary search to 0
7 // and 'right' boundary to the length of the longest ribbon.
8 int left = 0, right = *max_element(ribbons.begin(), ribbons.end());
9
10 // Use binary search to find the maximum length of ribbon pieces.
11 while (left < right) {
12 // 'mid' is the midpoint length that we will test to see if we can
13 // get at least 'k' ribbon pieces of this length.
14 int mid = (left + right + 1) / 2; // Use /2 for clarity, alternative to bitwise operation
15 // Initialize count of total ribbon pieces we can get with the current 'mid'.
16 int count = 0;
17
18 // Calculate how many pieces of length 'mid' we can get for each ribbon.
19 for (int ribbon : ribbons) {
20 count += ribbon / mid;
21 }
22 // If we can get at least 'k' pieces, try finding a longer length;
23 // otherwise, we try shorter lengths.
24 if (count >= k) {
25 left = mid; // Notice we use 'mid' rather than 'mid+1' because we're already considering mid
26 } else {
27 right = mid - 1;
28 }
29 }
30 // Return the maximum length of ribbon we found; 'left' is our best valid guess.
31 return left;
32 }
33};
34
1// Define the function with a clear signature, describing the parameter types and return type.
2// This function finds the maximum length of the ribbon pieces that we can cut such that we have at least 'k' pieces.
3function maxLength(ribbons: number[], k: number): number {
4 // Initialize the search bounds for binary search.
5 let lowerBound = 0;
6 let upperBound = Math.max(...ribbons);
7
8 // Utilize a binary search to find the maximum length.
9 while (lowerBound < upperBound) {
10 // Calculate the middle value of the current search range.
11 const middle = Math.floor((lowerBound + upperBound + 1) / 2);
12
13 // Initialize the counter for how many ribbon pieces we can cut.
14 let count = 0;
15
16 // Calculate the number of pieces we can cut from each ribbon.
17 for (const ribbonLength of ribbons) {
18 count += Math.floor(ribbonLength / middle);
19 }
20
21 // If we can cut at least 'k' pieces, the piece length 'middle' is valid, so move the lower bound up.
22 if (count >= k) {
23 lowerBound = middle;
24 } else {
25 // Otherwise, the piece length is too high, so we reduce the upper bound.
26 upperBound = middle - 1;
27 }
28 }
29
30 // Return the maximum length of ribbon pieces we can cut.
31 return lowerBound;
32}
33
Time and Space Complexity
Time Complexity
The time complexity of the code is O(n * log M)
. Here n
refers to the number of ribbons, and M
is the maximum length of the ribbons. The code uses a binary search process to converge on the maximum length of the ribbon that can be cut to produce at least k
ribbons of equal length. The search range is between 0 and the maximum ribbon length, which gives us the log M
term because the search space is being halved in each iteration of the while loop. In each iteration, we go through all n
ribbons to calculate the total number of ribbons of the current length (mid
), leading to the multiplier of n
.
Space Complexity
The space complexity of the code is O(1)
. This is because the space used does not grow with the input size. Only a constant amount of extra space is used for variables left
, right
, mid
, and cnt
, irrespective of the size of the input array ribbons
.
Learn more about how to find time and space complexity quickly using problem constraints.
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
LeetCode Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way we
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!