Facebook Pixel

1891. Cutting Ribbons 🔒

Problem Description

You have an array ribbons where each element ribbons[i] represents the length of a ribbon. You also have an integer k representing the number of ribbon pieces you need.

You can cut any ribbon into smaller pieces of positive integer lengths, or leave it uncut. For example, a ribbon of length 4 can be:

  • Kept as length 4
  • Cut into pieces of lengths 3 and 1
  • Cut into two pieces of length 2
  • Cut into pieces of lengths 2, 1, and 1
  • Cut into four pieces of length 1

Your goal is to find the maximum possible length x such that you can obtain at least k ribbon pieces, where each piece has the same length x. Any leftover ribbon material after cutting can be discarded.

If it's impossible to get k ribbons of the same length, return 0.

For example:

  • If ribbons = [9, 7, 5] and k = 3, you could cut each ribbon to get pieces of length 5: from the ribbon of length 9 you get one piece (with 4 discarded), from length 7 you get one piece (with 2 discarded), and from length 5 you get one piece. This gives you exactly 3 pieces of length 5, so the answer is 5.
  • If ribbons = [7, 5, 9] and k = 4, you could cut ribbons to get pieces of length 4: from length 7 you get one piece, from length 5 you get one piece, and from length 9 you get two pieces. This gives you 4 pieces of length 4, so the answer is 4.
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing a monotonic property in this problem: if we can cut k ribbons of length x, then we can definitely cut k ribbons of any length smaller than x. Why? Because any ribbon that can be cut into pieces of length x can also be cut into pieces of smaller length - we just get more pieces or have less waste.

For example, if a ribbon of length 9 can give us one piece of length 5 (with remainder 4), it can definitely give us at least one piece of length 4, 3, 2, or 1.

This monotonic property suggests we can use binary search. We're looking for the maximum length where we can still get at least k pieces. Think of it as searching for a threshold value:

  • For lengths that are too large, we won't be able to get k pieces
  • For lengths that are small enough, we will be able to get k or more pieces
  • We want to find the largest length in the "possible" range

The search space is from 0 (minimum possible length, representing the case where no valid solution exists) to max(ribbons) (we can't cut pieces longer than our longest ribbon).

For any candidate length mid, we can quickly check if it's feasible by counting how many pieces of length mid we can get from all ribbons: for each ribbon of length ribbons[i], we can get ribbons[i] // mid pieces of length mid. If the total count is at least k, then mid is feasible and we should try larger values. Otherwise, we need to try smaller values.

Learn more about Binary Search patterns.

Solution Approach

We implement a binary search to find the maximum ribbon length. Here's how the algorithm works:

  1. Initialize search boundaries: Set left = 0 and right = max(ribbons). The left boundary starts at 0 (representing the case where no valid solution exists), and the right boundary is the maximum ribbon length (we can't cut pieces longer than our longest ribbon).

  2. Binary search loop: While left < right, we:

    • Calculate the middle value: mid = (left + right + 1) >> 1
      • Note: We use (left + right + 1) instead of (left + right) to avoid infinite loops when left and right are adjacent. The >> 1 operation is equivalent to integer division by 2.
    • Count how many pieces of length mid we can obtain: cnt = sum(x // mid for x in ribbons)
      • For each ribbon of length x, we can cut x // mid pieces of length mid
      • We sum up all pieces from all ribbons
    • Make a decision based on the count:
      • If cnt >= k: We can obtain at least k pieces of length mid, so this length is feasible. We try to find a larger length by setting left = mid
      • If cnt < k: We cannot obtain k pieces of length mid, so we need a smaller length. Set right = mid - 1
  3. Return the result: After the loop ends, left contains the maximum length where we can obtain at least k pieces. If no valid length exists, left will be 0.

The time complexity is O(n * log(max_ribbon)) where n is the number of ribbons, as we perform binary search over the range [0, max_ribbon] and for each mid value, we iterate through all ribbons to count pieces. The space complexity is O(1) as we only use a constant amount of extra space.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with ribbons = [9, 7, 5] and k = 3.

Step 1: Initialize boundaries

  • left = 0 (minimum possible length)
  • right = 9 (maximum ribbon length)

Step 2: Binary search iterations

Iteration 1:

  • mid = (0 + 9 + 1) >> 1 = 5
  • Count pieces of length 5:
    • From ribbon of length 9: 9 // 5 = 1 piece
    • From ribbon of length 7: 7 // 5 = 1 piece
    • From ribbon of length 5: 5 // 5 = 1 piece
    • Total: cnt = 1 + 1 + 1 = 3
  • Since cnt = 3 >= k = 3, length 5 is feasible
  • Update: left = 5 (try to find a larger length)

Iteration 2:

  • mid = (5 + 9 + 1) >> 1 = 7
  • Count pieces of length 7:
    • From ribbon of length 9: 9 // 7 = 1 piece
    • From ribbon of length 7: 7 // 7 = 1 piece
    • From ribbon of length 5: 5 // 7 = 0 pieces
    • Total: cnt = 1 + 1 + 0 = 2
  • Since cnt = 2 < k = 3, length 7 is not feasible
  • Update: right = 7 - 1 = 6

Iteration 3:

  • mid = (5 + 6 + 1) >> 1 = 6
  • Count pieces of length 6:
    • From ribbon of length 9: 9 // 6 = 1 piece
    • From ribbon of length 7: 7 // 6 = 1 piece
    • From ribbon of length 5: 5 // 6 = 0 pieces
    • Total: cnt = 1 + 1 + 0 = 2
  • Since cnt = 2 < k = 3, length 6 is not feasible
  • Update: right = 6 - 1 = 5

Step 3: Loop terminates

  • Now left = 5 and right = 5, so left >= right
  • Return left = 5

The maximum length where we can obtain at least 3 pieces is 5. Each ribbon contributes one piece of length 5, giving us exactly 3 pieces total.

Solution Implementation

1class Solution:
2    def maxLength(self, ribbons: List[int], k: int) -> int:
3        # Initialize binary search boundaries
4        # left: minimum possible ribbon length (0 means we might not be able to cut any ribbons)
5        # right: maximum possible ribbon length (the longest ribbon in the array)
6        left, right = 0, max(ribbons)
7      
8        # Binary search for the maximum valid ribbon length
9        while left < right:
10            # Calculate middle value, using (left + right + 1) // 2 to avoid infinite loop
11            # This ensures mid is biased towards the upper half when left and right differ by 1
12            mid = (left + right + 1) // 2
13          
14            # Count how many ribbons of length 'mid' we can cut from all ribbons
15            # For each ribbon, we can get ribbon_length // mid pieces
16            ribbon_count = sum(ribbon_length // mid for ribbon_length in ribbons)
17          
18            # If we can cut at least k ribbons of length mid
19            if ribbon_count >= k:
20                # The answer is at least mid, search in the upper half
21                left = mid
22            else:
23                # We cannot cut k ribbons of length mid, search in the lower half
24                right = mid - 1
25      
26        # Return the maximum length that allows cutting at least k ribbons
27        return left
28
1class Solution {
2    public int maxLength(int[] ribbons, int k) {
3        // Initialize binary search boundaries
4        // left: minimum possible ribbon length (0 means we might not be able to cut any ribbons)
5        // right: maximum possible ribbon length (the longest ribbon in the array)
6        int left = 0;
7        int right = 0;
8      
9        // Find the maximum ribbon length to set as upper bound
10        for (int ribbon : ribbons) {
11            right = Math.max(right, ribbon);
12        }
13      
14        // Binary search for the maximum valid ribbon length
15        while (left < right) {
16            // Calculate mid point, using (left + right + 1) / 2 to avoid infinite loop
17            // The +1 ensures we round up, preventing stuck conditions when left = right - 1
18            int mid = (left + right + 1) >>> 1;
19          
20            // Count how many ribbons of length 'mid' we can cut
21            int count = 0;
22            for (int ribbon : ribbons) {
23                count += ribbon / mid;  // Integer division gives number of pieces
24            }
25          
26            // Check if we can cut at least k ribbons of length 'mid'
27            if (count >= k) {
28                // If yes, try to find a longer length
29                left = mid;
30            } else {
31                // If no, the length is too long, reduce upper bound
32                right = mid - 1;
33            }
34        }
35      
36        // Return the maximum valid ribbon length found
37        return left;
38    }
39}
40
1class Solution {
2public:
3    int maxLength(vector<int>& ribbons, int k) {
4        // Binary search on the answer: find the maximum length that allows cutting k ribbons
5        // Left boundary: 0 (worst case, no valid ribbon can be cut)
6        int left = 0;
7        // Right boundary: maximum ribbon length in the array
8        int right = *max_element(ribbons.begin(), ribbons.end());
9      
10        // Binary search to find the maximum valid ribbon length
11        while (left < right) {
12            // Calculate mid point, using (left + right + 1) / 2 to avoid infinite loop
13            // when left = mid (this ensures mid is biased towards the upper half)
14            int mid = (left + right + 1) >> 1;
15          
16            // Count how many ribbons of length 'mid' can be cut from all ribbons
17            int ribbonCount = 0;
18            for (int ribbon : ribbons) {
19                ribbonCount += ribbon / mid;  // Integer division gives number of pieces
20            }
21          
22            // Check if we can cut at least k ribbons of length 'mid'
23            if (ribbonCount >= k) {
24                // If yes, try to find a larger length (move left boundary up)
25                left = mid;
26            } else {
27                // If no, the length is too large (move right boundary down)
28                right = mid - 1;
29            }
30        }
31      
32        // Return the maximum valid ribbon length found
33        return left;
34    }
35};
36
1/**
2 * Finds the maximum length of ribbon pieces that can be cut from given ribbons
3 * to obtain at least k pieces of equal length
4 * @param ribbons - Array of ribbon lengths
5 * @param k - Target number of ribbon pieces needed
6 * @returns Maximum possible length of each piece, or 0 if impossible
7 */
8function maxLength(ribbons: number[], k: number): number {
9    // Binary search bounds: minimum possible length is 0
10    let left: number = 0;
11    // Maximum possible length is the longest ribbon
12    let right: number = Math.max(...ribbons);
13  
14    // Binary search for the maximum valid ribbon length
15    while (left < right) {
16        // Calculate middle value, using (left + right + 1) to avoid infinite loop
17        // when left and right are adjacent (ensures mid rounds up)
18        const mid: number = (left + right + 1) >> 1;
19      
20        // Count how many pieces of length 'mid' can be obtained
21        let pieceCount: number = 0;
22        for (const ribbonLength of ribbons) {
23            // Each ribbon can be cut into floor(ribbonLength / mid) pieces
24            pieceCount += Math.floor(ribbonLength / mid);
25        }
26      
27        // Check if we can obtain at least k pieces
28        if (pieceCount >= k) {
29            // Valid length found, try to find a larger one
30            left = mid;
31        } else {
32            // Cannot obtain k pieces, try smaller length
33            right = mid - 1;
34        }
35    }
36  
37    // Return the maximum valid length found
38    return left;
39}
40

Time and Space Complexity

The time complexity is O(n × log M), where n is the number of ribbons and M is the maximum length among all ribbons.

  • The binary search runs for O(log M) iterations, as it searches through the range [0, max(ribbons)] where max(ribbons) represents the maximum ribbon length M.
  • In each iteration of the binary search, we calculate sum(x // mid for x in ribbons), which takes O(n) time as it iterates through all n ribbons.
  • Therefore, the overall time complexity is O(n × log M).

The space complexity is O(1).

  • The algorithm only uses a constant amount of extra space for variables left, right, mid, and cnt.
  • The sum operation with generator expression sum(x // mid for x in ribbons) doesn't create additional data structures and computes the sum on-the-fly.
  • No additional data structures that scale with input size are created.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Division by Zero Error

The Pitfall: When calculating ribbon_count = sum(ribbon_length // mid for ribbon_length in ribbons), if mid equals 0, this will cause a division by zero error. This happens when the binary search explores the lower boundary where mid could potentially be 0.

Why It Happens: In the binary search loop, when left = 0 and right = 0 or right = 1, the calculated mid value could be 0, leading to a runtime error.

Solution: Add a check to handle the case when mid = 0:

def maxLength(self, ribbons: List[int], k: int) -> int:
    left, right = 0, max(ribbons)
  
    while left < right:
        mid = (left + right + 1) // 2
      
        # Handle the edge case where mid is 0
        if mid == 0:
            return 0
      
        ribbon_count = sum(ribbon_length // mid for ribbon_length in ribbons)
      
        if ribbon_count >= k:
            left = mid
        else:
            right = mid - 1
  
    return left

2. Incorrect Mid Calculation Leading to Infinite Loop

The Pitfall: Using mid = (left + right) // 2 instead of mid = (left + right + 1) // 2 can cause an infinite loop when left and right are adjacent values.

Why It Happens: When left = n and right = n + 1, using (left + right) // 2 gives mid = n. If ribbon_count >= k, we set left = mid = n, which doesn't change the state, causing an infinite loop.

Example Scenario:

  • left = 4, right = 5
  • mid = (4 + 5) // 2 = 4
  • If condition is satisfied, left = 4 (no change!)
  • Loop continues infinitely

Solution: Always use mid = (left + right + 1) // 2 when the update condition is left = mid, or alternatively restructure the binary search to use left = mid + 1:

# Alternative approach with different update logic
def maxLength(self, ribbons: List[int], k: int) -> int:
    left, right = 1, max(ribbons)
    result = 0
  
    while left <= right:
        mid = (left + right) // 2
      
        ribbon_count = sum(ribbon_length // mid for ribbon_length in ribbons)
      
        if ribbon_count >= k:
            result = mid  # Store the valid answer
            left = mid + 1  # Try to find a larger length
        else:
            right = mid - 1
  
    return result

3. Not Handling the Case Where k > Total Ribbon Length

The Pitfall: Not considering that if k is larger than the sum of all ribbon lengths, it's impossible to cut k pieces even of length 1.

Why It Happens: The problem states we need positive integer lengths, so the minimum piece length is 1. If sum(ribbons) < k, we cannot possibly get k pieces.

Solution: While the binary search naturally handles this by returning 0, you could add an early check for clarity and optimization:

def maxLength(self, ribbons: List[int], k: int) -> int:
    # Early termination if impossible
    if sum(ribbons) < k:
        return 0
  
    left, right = 0, max(ribbons)
    # ... rest of the binary search logic
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What does the following code do?

1def f(arr1, arr2):
2  i, j = 0, 0
3  new_arr = []
4  while i < len(arr1) and j < len(arr2):
5      if arr1[i] < arr2[j]:
6          new_arr.append(arr1[i])
7          i += 1
8      else:
9          new_arr.append(arr2[j])
10          j += 1
11  new_arr.extend(arr1[i:])
12  new_arr.extend(arr2[j:])
13  return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2  int i = 0, j = 0;
3  List<Integer> newArr = new ArrayList<>();
4
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.add(arr1[i]);
8          i++;
9      } else {
10          newArr.add(arr2[j]);
11          j++;
12      }
13  }
14
15  while (i < arr1.length) {
16      newArr.add(arr1[i]);
17      i++;
18  }
19
20  while (j < arr2.length) {
21      newArr.add(arr2[j]);
22      j++;
23  }
24
25  return newArr;
26}
27
1function f(arr1, arr2) {
2  let i = 0, j = 0;
3  let newArr = [];
4  
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.push(arr1[i]);
8          i++;
9      } else {
10          newArr.push(arr2[j]);
11          j++;
12      }
13  }
14  
15  while (i < arr1.length) {
16      newArr.push(arr1[i]);
17      i++;
18  }
19  
20  while (j < arr2.length) {
21      newArr.push(arr2[j]);
22      j++;
23  }
24  
25  return newArr;
26}
27

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More