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
andn
representing the cake dimensions - Array
horizontalCut
of sizem - 1
, wherehorizontalCut[i]
is the cost to make a cut along horizontal linei
- Array
verticalCut
of sizen - 1
, whereverticalCut[j]
is the cost to make a cut along vertical linej
How cutting works:
- You can only cut a piece that isn't already
1 x 1
- Each cut divides one piece into two separate pieces
- When you make a horizontal cut at position
i
, you payhorizontalCut[i]
for each piece that the cut goes through - When you make a vertical cut at position
j
, you payverticalCut[j]
for each piece that the cut goes through - 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.
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 payh1
once - If we then make a vertical cut (cost
v1
), we payv1 ร 2
because the vertical cut goes through 2 horizontal pieces - If we had done the vertical cut first instead, we would pay
v1
once, thenh1 ร 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:
- Sort both
horizontalCut
andverticalCut
in descending order (most expensive first) - Use two pointers to track which cuts we've made
- Always choose the more expensive remaining cut between horizontal and vertical
- Keep track of how many pieces exist in each direction (
h
for horizontal pieces,v
for vertical pieces) - When making a horizontal cut, multiply its cost by the current number of vertical pieces
v
- 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.
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 costi = 0, j = 0
: pointers forhorizontalCut
andverticalCut
arraysh = 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 throughv
vertical pieces. After cutting, we have one more horizontal piece (h += 1
) - Vertical cut: costs
verticalCut[j] * h
because the cut goes throughh
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 cutsi == 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 EvaluatorExample 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
vsverticalCut[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
vsverticalCut[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
vsverticalCut[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)
andverticalCut.sort(reverse=True)
takeO(m ร log m)
andO(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)
andO(log n)
space for the recursive call stack (assuming Python's Timsort implementation) - The remaining variables (
ans
,i
,j
,h
,v
) use constantO(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 andn
vertical pieces - The counts track how many pieces exist, not how many cuts have been made
Correct initialization: h = 1, v = 1
Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Coding Interview 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
Want a Structured Path to Master System Design Too? Donโt Miss This!