Facebook Pixel

3219. Minimum Cost for Cutting Cake II

Problem Description

You have an m x n rectangular cake that needs to be cut into individual 1 x 1 pieces.

The cake has:

  • m - 1 horizontal lines where cuts can be made (dividing rows)
  • n - 1 vertical lines where cuts can be made (dividing columns)

You're given:

  • Two integers m and n representing the cake dimensions
  • Array horizontalCut of size m - 1, where horizontalCut[i] is the cost to make a cut along horizontal line i
  • Array verticalCut of size n - 1, where verticalCut[j] is the cost to make a cut along vertical line j

How cutting works:

  1. You can only cut a piece that isn't already 1 x 1
  2. Each cut divides one piece into two separate pieces
  3. When you make a horizontal cut at position i, you pay horizontalCut[i] for each piece that the cut goes through
  4. When you make a vertical cut at position j, you pay verticalCut[j] for each piece that the cut goes through
  5. The cost of using a particular cut line remains the same regardless of when you use it

Key insight: If you make a horizontal cut first, it creates 2 horizontal pieces. If you then make a vertical cut, that vertical cut must go through both pieces, so you pay the vertical cost twice.

Your task is to find the minimum total cost to cut the entire cake into 1 x 1 pieces by choosing the optimal order of cuts.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key observation is that when we make a cut, we pay the cost for each piece that the cut goes through. This means if we've already made some cuts in one direction, a cut in the perpendicular direction becomes more expensive.

For example, imagine we have a 3x3 cake:

  • If we make a horizontal cut first (cost h1), we pay h1 once
  • If we then make a vertical cut (cost v1), we pay v1 ร— 2 because the vertical cut goes through 2 horizontal pieces
  • If we had done the vertical cut first instead, we would pay v1 once, then h1 ร— 2

This reveals the trade-off: delaying expensive cuts makes them even more expensive because they'll have to go through more pieces.

Therefore, the greedy strategy emerges: always make the most expensive cut as early as possible to minimize the multiplication effect. When an expensive cut is made early, it only goes through fewer pieces.

To implement this:

  1. Sort both horizontalCut and verticalCut in descending order (most expensive first)
  2. Use two pointers to track which cuts we've made
  3. Always choose the more expensive remaining cut between horizontal and vertical
  4. Keep track of how many pieces exist in each direction (h for horizontal pieces, v for vertical pieces)
  5. When making a horizontal cut, multiply its cost by the current number of vertical pieces v
  6. When making a vertical cut, multiply its cost by the current number of horizontal pieces h

This greedy approach ensures we minimize the total cost by making expensive cuts when they affect fewer pieces.

Learn more about Greedy and Sorting patterns.

Solution Approach

Following the greedy strategy, here's how we implement the solution:

Step 1: Sort the cuts by cost (descending)

horizontalCut.sort(reverse=True)
verticalCut.sort(reverse=True)

This ensures we process the most expensive cuts first.

Step 2: Initialize variables

  • ans = 0: tracks the total cost
  • i = 0, j = 0: pointers for horizontalCut and verticalCut arrays
  • h = 1, v = 1: initially, we have 1 piece (the whole cake), which counts as 1 horizontal piece and 1 vertical piece

Step 3: Process cuts using two pointers

while i < m - 1 or j < n - 1:

Continue until all cuts are made (we need m-1 horizontal cuts and n-1 vertical cuts).

Step 4: Choose which cut to make next At each step, we compare horizontalCut[i] with verticalCut[j] and choose the larger one:

if j == n - 1 or (i < m - 1 and horizontalCut[i] > verticalCut[j]):
    # Make horizontal cut
    ans += horizontalCut[i] * v
    h, i = h + 1, i + 1
else:
    # Make vertical cut
    ans += verticalCut[j] * h
    v, j = v + 1, j + 1

The logic for each cut:

  • Horizontal cut: costs horizontalCut[i] * v because the cut goes through v vertical pieces. After cutting, we have one more horizontal piece (h += 1)
  • Vertical cut: costs verticalCut[j] * h because the cut goes through h horizontal pieces. After cutting, we have one more vertical piece (v += 1)

Edge cases handled:

  • j == n - 1: all vertical cuts are done, so we must make horizontal cuts
  • i == m - 1: all horizontal cuts are done, so we must make vertical cuts

Time Complexity: O(m log m + n log n) for sorting, then O(m + n) for the two-pointer traversal.

Space Complexity: O(1) if we can modify the input arrays, otherwise O(m + n) for sorting.

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 a small example with a 3ร—3 cake (m=3, n=3):

  • horizontalCut = [3, 1] (costs for 2 horizontal cuts)
  • verticalCut = [5, 2] (costs for 2 vertical cuts)

Step 1: Sort cuts in descending order

  • horizontalCut = [3, 1] (already sorted)
  • verticalCut = [5, 2] (already sorted)

Step 2: Initialize

  • ans = 0 (total cost)
  • i = 0, j = 0 (pointers)
  • h = 1, v = 1 (number of pieces in each direction)

Step 3: Process cuts

Iteration 1:

  • Compare: horizontalCut[0]=3 vs verticalCut[0]=5
  • Choose vertical (5 > 3)
  • Cost: 5 ร— h = 5 ร— 1 = 5
  • Update: ans = 5, v = 2, j = 1
  • State: Cake is now split into 2 vertical pieces (1ร—3 each)

Iteration 2:

  • Compare: horizontalCut[0]=3 vs verticalCut[1]=2
  • Choose horizontal (3 > 2)
  • Cost: 3 ร— v = 3 ร— 2 = 6 (cut goes through 2 vertical pieces)
  • Update: ans = 11, h = 2, i = 1
  • State: Cake has 2ร—2 = 4 pieces

Iteration 3:

  • Compare: horizontalCut[1]=1 vs verticalCut[1]=2
  • Choose vertical (2 > 1)
  • Cost: 2 ร— h = 2 ร— 2 = 4 (cut goes through 2 horizontal pieces)
  • Update: ans = 15, v = 3, j = 2
  • State: Cake has 2ร—3 = 6 pieces

Iteration 4:

  • Only horizontal cut remains: horizontalCut[1]=1
  • Cost: 1 ร— v = 1 ร— 3 = 3 (cut goes through 3 vertical pieces)
  • Update: ans = 18, h = 3, i = 2
  • Final state: Cake is fully cut into 3ร—3 = 9 pieces

Total minimum cost: 18

This demonstrates how the greedy approach minimizes cost by making expensive cuts early when they affect fewer pieces.

Solution Implementation

1class Solution:
2    def minimumCost(
3        self, m: int, n: int, horizontalCut: List[int], verticalCut: List[int]
4    ) -> int:
5        # Sort both cut arrays in descending order to prioritize expensive cuts first
6        horizontalCut.sort(reverse=True)
7        verticalCut.sort(reverse=True)
8      
9        # Initialize variables
10        total_cost = 0  # Accumulator for the total minimum cost
11        horizontal_index = 0  # Index for traversing horizontalCut array
12        vertical_index = 0  # Index for traversing verticalCut array
13        horizontal_pieces = 1  # Number of horizontal pieces after cuts
14        vertical_pieces = 1  # Number of vertical pieces after cuts
15      
16        # Process all cuts using a greedy approach
17        # Continue until all horizontal and vertical cuts are made
18        while horizontal_index < m - 1 or vertical_index < n - 1:
19            # Determine which cut to make next based on cost efficiency
20            if vertical_index == n - 1 or (
21                horizontal_index < m - 1 
22                and horizontalCut[horizontal_index] > verticalCut[vertical_index]
23            ):
24                # Make a horizontal cut
25                # Cost is the cut price multiplied by number of vertical pieces
26                total_cost += horizontalCut[horizontal_index] * vertical_pieces
27                horizontal_pieces += 1
28                horizontal_index += 1
29            else:
30                # Make a vertical cut
31                # Cost is the cut price multiplied by number of horizontal pieces
32                total_cost += verticalCut[vertical_index] * horizontal_pieces
33                vertical_pieces += 1
34                vertical_index += 1
35      
36        return total_cost
37
1class Solution {
2    public long minimumCost(int m, int n, int[] horizontalCut, int[] verticalCut) {
3        // Sort both arrays to process cuts from highest to lowest cost
4        Arrays.sort(horizontalCut);
5        Arrays.sort(verticalCut);
6      
7        long totalCost = 0;
8      
9        // Initialize indices to point to the last elements (highest costs after sorting)
10        int horizontalIndex = m - 2;  // m-1 horizontal cuts for m rows
11        int verticalIndex = n - 2;     // n-1 vertical cuts for n columns
12      
13        // Track the number of segments in each direction
14        int horizontalSegments = 1;  // Initially 1 horizontal piece
15        int verticalSegments = 1;    // Initially 1 vertical piece
16      
17        // Process all cuts using a greedy approach - always choose the most expensive cut
18        while (horizontalIndex >= 0 || verticalIndex >= 0) {
19            // Determine which cut to make next based on cost comparison
20            if (verticalIndex < 0 || 
21                (horizontalIndex >= 0 && horizontalCut[horizontalIndex] > verticalCut[verticalIndex])) {
22                // Make a horizontal cut
23                // Cost = horizontal cut cost * number of vertical segments it crosses
24                totalCost += (long) horizontalCut[horizontalIndex] * verticalSegments;
25                horizontalIndex--;
26                horizontalSegments++;  // One more horizontal segment after this cut
27            } else {
28                // Make a vertical cut
29                // Cost = vertical cut cost * number of horizontal segments it crosses
30                totalCost += (long) verticalCut[verticalIndex] * horizontalSegments;
31                verticalIndex--;
32                verticalSegments++;  // One more vertical segment after this cut
33            }
34        }
35      
36        return totalCost;
37    }
38}
39
1class Solution {
2public:
3    long long minimumCost(int m, int n, vector<int>& horizontalCut, vector<int>& verticalCut) {
4        // Sort both cut arrays in descending order to prioritize expensive cuts first
5        sort(horizontalCut.rbegin(), horizontalCut.rend());
6        sort(verticalCut.rbegin(), verticalCut.rend());
7      
8        long long totalCost = 0;
9      
10        // Indices for traversing horizontal and vertical cut arrays
11        int horizontalIndex = 0;
12        int verticalIndex = 0;
13      
14        // Count of horizontal and vertical segments created so far
15        int horizontalSegments = 1;  // Initially we have 1 horizontal piece
16        int verticalSegments = 1;    // Initially we have 1 vertical piece
17      
18        // Process all cuts using a greedy approach
19        while (horizontalIndex < m - 1 || verticalIndex < n - 1) {
20            // Determine which cut to make next based on cost optimization
21            if (verticalIndex == n - 1 || 
22                (horizontalIndex < m - 1 && horizontalCut[horizontalIndex] > verticalCut[verticalIndex])) {
23                // Make a horizontal cut
24                // Cost = horizontal cut cost ร— number of vertical segments it crosses
25                totalCost += static_cast<long long>(horizontalCut[horizontalIndex]) * verticalSegments;
26                horizontalIndex++;
27                horizontalSegments++;
28            } else {
29                // Make a vertical cut
30                // Cost = vertical cut cost ร— number of horizontal segments it crosses
31                totalCost += static_cast<long long>(verticalCut[verticalIndex]) * horizontalSegments;
32                verticalIndex++;
33                verticalSegments++;
34            }
35        }
36      
37        return totalCost;
38    }
39};
40
1/**
2 * Calculates the minimum cost to cut a grid into 1x1 pieces
3 * @param m - Number of rows in the grid
4 * @param n - Number of columns in the grid
5 * @param horizontalCut - Array of costs for each horizontal cut
6 * @param verticalCut - Array of costs for each vertical cut
7 * @returns The minimum total cost to cut the grid into 1x1 pieces
8 */
9function minimumCost(m: number, n: number, horizontalCut: number[], verticalCut: number[]): number {
10    // Sort both cut arrays in descending order to prioritize expensive cuts first
11    horizontalCut.sort((a, b) => b - a);
12    verticalCut.sort((a, b) => b - a);
13  
14    // Initialize total cost accumulator
15    let totalCost: number = 0;
16  
17    // Pointers for traversing horizontal and vertical cut arrays
18    let horizontalIndex: number = 0;
19    let verticalIndex: number = 0;
20  
21    // Count of horizontal and vertical segments created so far
22    let horizontalSegments: number = 1;
23    let verticalSegments: number = 1;
24  
25    // Process all cuts using a greedy approach
26    while (horizontalIndex < m - 1 || verticalIndex < n - 1) {
27        // Determine whether to make a horizontal or vertical cut next
28        if (verticalIndex === n - 1 || 
29            (horizontalIndex < m - 1 && horizontalCut[horizontalIndex] > verticalCut[verticalIndex])) {
30            // Make a horizontal cut
31            // Cost is multiplied by the number of vertical segments it crosses
32            totalCost += horizontalCut[horizontalIndex] * verticalSegments;
33            horizontalSegments++;
34            horizontalIndex++;
35        } else {
36            // Make a vertical cut
37            // Cost is multiplied by the number of horizontal segments it crosses
38            totalCost += verticalCut[verticalIndex] * horizontalSegments;
39            verticalSegments++;
40            verticalIndex++;
41        }
42    }
43  
44    return totalCost;
45}
46

Time and Space Complexity

The time complexity is O(m ร— log m + n ร— log n), where m and n are the lengths of horizontalCut and verticalCut arrays, respectively.

  • The sorting operations horizontalCut.sort(reverse=True) and verticalCut.sort(reverse=True) take O(m ร— log m) and O(n ร— log n) time respectively
  • The while loop iterates through all elements in both arrays exactly once, performing constant time operations in each iteration, which takes O(m + n) time
  • The dominant operations are the sorting steps, resulting in overall time complexity of O(m ร— log m + n ร— log n)

The space complexity is O(log m + log n).

  • The sorting operations use O(log m) and O(log n) space for the recursive call stack (assuming Python's Timsort implementation)
  • The remaining variables (ans, i, j, h, v) use constant O(1) space
  • Therefore, the total space complexity is O(log m + log n)

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

Common Pitfalls

1. Incorrectly Understanding the Multiplier Effect

Pitfall: Many people mistakenly think that when making a horizontal cut, it should be multiplied by the number of horizontal pieces (h) instead of vertical pieces (v).

Why it's wrong: When you make a horizontal cut, it goes through ALL vertical pieces that exist at that moment. If you've already made 2 vertical cuts (creating 3 vertical pieces), then a horizontal cut must pass through all 3 vertical pieces, costing horizontalCut[i] * 3.

Correct understanding:

  • Horizontal cuts go through vertical pieces โ†’ multiply by v
  • Vertical cuts go through horizontal pieces โ†’ multiply by h

2. Forgetting to Handle Edge Cases in the Comparison

Pitfall: Writing the comparison as simply:

if horizontalCut[horizontal_index] > verticalCut[vertical_index]:
    # Make horizontal cut

Why it's wrong: This will cause an IndexError when one array is exhausted but the other still has cuts remaining.

Solution: Always check boundaries first:

if vertical_index == n - 1 or (
    horizontal_index < m - 1 
    and horizontalCut[horizontal_index] > verticalCut[vertical_index]
):

3. Using Ascending Sort Instead of Descending

Pitfall: Sorting the arrays in ascending order and processing from the smallest cuts first.

Why it's wrong: The greedy strategy requires making the most expensive cuts first when fewer pieces exist. If you make cheap cuts first, the expensive cuts will be multiplied by larger numbers of pieces.

Example:

  • horizontalCut = [4, 1], verticalCut = [3]
  • Wrong (ascending): Cut 1 first (cost 1ร—1=1), then 3 (cost 3ร—2=6), then 4 (cost 4ร—2=8). Total = 15
  • Correct (descending): Cut 4 first (cost 4ร—1=4), then 3 (cost 3ร—2=6), then 1 (cost 1ร—2=2). Total = 12

4. Initializing Piece Counts Incorrectly

Pitfall: Initializing h = 0 and v = 0 or h = m and v = n.

Why it's wrong:

  • Initially, you have ONE whole cake, which counts as 1 horizontal piece AND 1 vertical piece
  • After making all cuts, you'll have m horizontal pieces and n vertical pieces
  • The counts track how many pieces exist, not how many cuts have been made

Correct initialization: h = 1, v = 1

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?


Recommended Readings

Want a Structured Path to Master System Design Too? Donโ€™t Miss This!

Load More