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 boundary in a sequence of feasibility values:

Length: 1 2 3 4 5 6 7 8 9 Feasible: T T T T T F F F F  first_true_index (searching from right)

Since we want the maximum feasible length, we can reframe the problem: find the first length where we cannot get k pieces. The answer is one less than that. Alternatively, we search for the last true in the feasible sequence.

The search space is from 1 (minimum valid ribbon length) to max(ribbons) (we can't cut pieces longer than our longest ribbon).

For any candidate length mid, we define feasible(mid) as: can we cut at least k pieces of length mid? We count pieces by summing ribbons[i] // mid for each ribbon. If the count is at least k, it's feasible.

Learn more about Binary Search patterns.

Solution Implementation

1class Solution: 2 def maxLength(self, ribbons: List[int], k: int) -> int: 3 def feasible(length: int) -> bool: 4 """Check if we can cut at least k pieces of the given length.""" 5 count = sum(ribbon // length for ribbon in ribbons) 6 return count >= k 7 8 # Binary search boundaries 9 left, right = 1, max(ribbons) 10 first_true_index = 0 # Default: no valid length found 11 12 while left <= right: 13 mid = (left + right) // 2 14 15 if feasible(mid): 16 # Length mid is feasible, record it and try larger 17 first_true_index = mid 18 left = mid + 1 19 else: 20 # Length mid is not feasible, try smaller 21 right = mid - 1 22 23 return first_true_index 24
1class Solution { 2 public int maxLength(int[] ribbons, int k) { 3 // Find the maximum ribbon length for upper bound 4 int maxRibbon = 0; 5 for (int ribbon : ribbons) { 6 maxRibbon = Math.max(maxRibbon, ribbon); 7 } 8 9 // Binary search boundaries 10 int left = 1; 11 int right = maxRibbon; 12 int firstTrueIndex = 0; // Default: no valid length found 13 14 while (left <= right) { 15 int mid = left + (right - left) / 2; 16 17 // Check if we can cut at least k pieces of length mid 18 int count = 0; 19 for (int ribbon : ribbons) { 20 count += ribbon / mid; 21 } 22 23 if (count >= k) { 24 // Length mid is feasible, record it and try larger 25 firstTrueIndex = mid; 26 left = mid + 1; 27 } else { 28 // Length mid is not feasible, try smaller 29 right = mid - 1; 30 } 31 } 32 33 return firstTrueIndex; 34 } 35} 36
1class Solution { 2public: 3 int maxLength(vector<int>& ribbons, int k) { 4 // Binary search boundaries 5 int left = 1; 6 int right = *max_element(ribbons.begin(), ribbons.end()); 7 int firstTrueIndex = 0; // Default: no valid length found 8 9 while (left <= right) { 10 int mid = left + (right - left) / 2; 11 12 // Check if we can cut at least k pieces of length mid 13 int count = 0; 14 for (int ribbon : ribbons) { 15 count += ribbon / mid; 16 } 17 18 if (count >= k) { 19 // Length mid is feasible, record it and try larger 20 firstTrueIndex = mid; 21 left = mid + 1; 22 } else { 23 // Length mid is not feasible, try smaller 24 right = mid - 1; 25 } 26 } 27 28 return firstTrueIndex; 29 } 30}; 31
1function maxLength(ribbons: number[], k: number): number { 2 const feasible = (length: number): boolean => { 3 let count = 0; 4 for (const ribbon of ribbons) { 5 count += Math.floor(ribbon / length); 6 } 7 return count >= k; 8 }; 9 10 // Binary search boundaries 11 let left = 1; 12 let right = Math.max(...ribbons); 13 let firstTrueIndex = 0; // Default: no valid length found 14 15 while (left <= right) { 16 const mid = Math.floor((left + right) / 2); 17 18 if (feasible(mid)) { 19 // Length mid is feasible, record it and try larger 20 firstTrueIndex = mid; 21 left = mid + 1; 22 } else { 23 // Length mid is not feasible, try smaller 24 right = mid - 1; 25 } 26 } 27 28 return firstTrueIndex; 29} 30

Solution Approach

We implement a binary search to find the maximum ribbon length using the standard template. Since we want the maximum feasible length, we search for the first infeasible length and track the last feasible one.

Template Structure:

left, right = 1, max(ribbons) first_true_index = -1 # Tracks the answer (or 0 for this problem)  while left <= right:  mid = (left + right) // 2  if feasible(mid):  first_true_index = mid # Record this feasible length  left = mid + 1 # Try larger lengths  else:  right = mid - 1 # Try smaller lengths  return first_true_index if first_true_index != -1 else 0

Key components:

  1. Initialize: Set left = 1 (minimum valid ribbon length) and right = max(ribbons). Use first_true_index = 0 to handle the case where no valid length exists.

  2. Feasibility check: For each mid, count pieces: count = sum(ribbon // mid for ribbon in ribbons). If count >= k, the length is feasible.

  3. Binary search logic:

    • If feasible: Record first_true_index = mid and search right (left = mid + 1) for potentially larger lengths
    • If not feasible: Search left (right = mid - 1) for smaller lengths
  4. Return: first_true_index contains the maximum feasible length, or 0 if none exists.

The time complexity is O(n × log(max_ribbon)) where n is the number of ribbons. The space complexity is O(1).

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

  • left = 1, right = 9 (max ribbon length)
  • first_true_index = 0 (default: no valid length found)

Step 2: Binary search iterations

Iteration 1: left = 1, right = 9

  • mid = (1 + 9) // 2 = 5
  • Count pieces of length 5:
    • Ribbon 9: 9 // 5 = 1 piece
    • Ribbon 7: 7 // 5 = 1 piece
    • Ribbon 5: 5 // 5 = 1 piece
    • Total: 3 >= k = 3 ✓ feasible
  • Update: first_true_index = 5, left = 5 + 1 = 6

Iteration 2: left = 6, right = 9

  • mid = (6 + 9) // 2 = 7
  • Count pieces of length 7:
    • Ribbon 9: 9 // 7 = 1 piece
    • Ribbon 7: 7 // 7 = 1 piece
    • Ribbon 5: 5 // 7 = 0 pieces
    • Total: 2 < k = 3 ✗ not feasible
  • Update: right = 7 - 1 = 6

Iteration 3: left = 6, right = 6

  • mid = (6 + 6) // 2 = 6
  • Count pieces of length 6:
    • Ribbon 9: 9 // 6 = 1 piece
    • Ribbon 7: 7 // 6 = 1 piece
    • Ribbon 5: 5 // 6 = 0 pieces
    • Total: 2 < k = 3 ✗ not feasible
  • Update: right = 6 - 1 = 5

Step 3: Loop terminates

  • Now left = 6 > right = 5, so loop exits
  • Return first_true_index = 5

The maximum length where we can obtain at least 3 pieces is 5.

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. Using the Wrong Binary Search Template

The Pitfall: Using while left < right with left = mid instead of the standard while left <= right template with first_true_index tracking.

Why It Happens: There are multiple binary search variants. The left < right with left = mid variant requires careful mid = (left + right + 1) // 2 calculation to avoid infinite loops. The standard template is more straightforward.

Solution: Use the standard template:

left, right = 1, max(ribbons) first_true_index = 0  while left <= right:  mid = (left + right) // 2  if feasible(mid):  first_true_index = mid  left = mid + 1  else:  right = mid - 1  return first_true_index

2. Division by Zero Error

The Pitfall: If left starts at 0, then mid could be 0, causing division by zero when calculating ribbon // mid.

Solution: Start left = 1 since ribbon length must be at least 1. The template naturally avoids this issue.

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.

Solution: The binary search naturally handles this by returning first_true_index = 0 when no feasible length exists. You could add an early check for optimization:

if sum(ribbons) < k:  return 0
Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

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

Load More