Greedy · 29 of 35
CHOCOLA - Chocolate
Snap a chocolate bar into single squares for the least total price by making the expensive snaps first, while they cross the fewest pieces.
- Simple way
- time O((r+c)! * (r+c))
room O(r+c) - Clever way
- time O(r log r + c log c)
room O(r+c)
The problem, simply
A chocolate bar is a grid of squares. Between the rows there are r straight lines, and between the columns there are c straight lines. Each line has a price: rowCut[i] for a line that runs across, colCut[j] for a line that runs down.
You break the bar into single squares. Each snap breaks one piece along one line. The price of a snap is the line's price, so cutting the same line in 3 different pieces costs 3 times its price. Give back the least total price. (This is SPOJ CHOCOLA, also known as "minimum cost to cut a board into squares".)
A story to remember it
Picture it
A 3 by 4 bar. Letters are squares of chocolate (all one piece, A). Numbers between them are the prices of the cut lines: 2 and 4 across, 3, 1 and 2 down.
Examples
A normal case
- In
rowCut = [2, 4], colCut = [3, 1, 2]- Out
23
Cuts in price order cost 4 x 1, 3 x 2, 2 x 2, 2 x 3 and 1 x 3.
The SPOJ sample
- In
rowCut = [4, 1, 2], colCut = [2, 1, 3, 1, 4]- Out
42
A 4 by 6 bar. Sorted prices: 4, 4, 3, 2, 2, 1, 1, 1.
A 2 by 2 bar
- In
rowCut = [2], colCut = [1]- Out
4
Cut 2 first through one piece (2), then 1 through two pieces (2). The other order costs 1 + 2 x 2 = 5.
A single row
- In
rowCut = [], colCut = [5, 1, 3]- Out
9
With no lines across, every line down is cut once. The order does not matter.
Way 1: the simple way
Try every order of cuts. Keep track of how many strips each direction has so far, because that decides the price of the next cut.
- Across strips start at 1, down strips start at 1.
- A line across costs
price * (down strips)and adds one across strip. A line down costsprice * (across strips)and adds one down strip. - Try every order and keep the cheapest total.
class Solution {
private long best;
public long minCost(int[] rowCut, int[] colCut) {
best = Long.MAX_VALUE;
go(rowCut, colCut, new boolean[rowCut.length], new boolean[colCut.length], 1, 1, 0, 0);
return best;
}
// across = strips made by lines across, down = strips made by lines down
private void go(int[] rc, int[] cc, boolean[] usedR, boolean[] usedC,
int across, int down, int done, long cost) {
if (done == rc.length + cc.length) { best = Math.min(best, cost); return; }
for (int i = 0; i < rc.length; i++) {
if (usedR[i]) continue;
usedR[i] = true; // a line across is cut once in every down strip
go(rc, cc, usedR, usedC, across + 1, down, done + 1, cost + (long) rc[i] * down);
usedR[i] = false;
}
for (int j = 0; j < cc.length; j++) {
if (usedC[j]) continue;
usedC[j] = true; // a line down is cut once in every across strip
go(rc, cc, usedR, usedC, across, down + 1, done + 1, cost + (long) cc[j] * across);
usedC[j] = false;
}
}
}How long it takes: (r + c)! orders, each step O(r + c) to scan, so O((r+c)! * (r+c)). Extra room: the used flags and the recursion, O(r + c).
Way 2: the clever way
Expensive lines first.
- Sort the across prices and the down prices, biggest first.
- Keep
across = 1anddown = 1strip counts. - Repeatedly take the bigger of the two next prices. A line across costs
price * downand makesacrossone bigger. A line down costsprice * acrossand makesdownone bigger. - When one list runs out, finish the other.
Why? Every line is cut eventually, and its price gets multiplied by how many pieces it crosses. That number only grows over time. Swapping two neighboring cuts so the pricier one goes first never costs more. So sorted order, biggest first, is best.
Snap the pricey lines while the bar is still whole
01 / 07
A 3 by 4 bar. The numbers between squares are the price of each cut line. Right now the whole bar is one piece, A. Sort all five lines by price, biggest first: 4, 3, 2, 2, 1.
- pieces
- 1
- cost
- 0
- looking now
- done
- answer
class Solution {
public long minCost(int[] rowCut, int[] colCut) {
int[] rc = rowCut.clone(), cc = colCut.clone();
Arrays.sort(rc);
Arrays.sort(cc);
int i = rc.length - 1, j = cc.length - 1; // walk both from the biggest price
long across = 1, down = 1, cost = 0;
while (i >= 0 || j >= 0) {
boolean takeRow = j < 0 || (i >= 0 && rc[i] >= cc[j]);
if (takeRow) {
cost += rc[i--] * down; // a line across crosses every down strip
across++;
} else {
cost += cc[j--] * across; // a line down crosses every across strip
down++;
}
}
return cost;
}
}How long it takes: two sorts, O(r log r + c log c), then one merge walk. Extra room: the sorted copies, O(r + c).
Watch out for
- Ties between an across price and a down price can be taken in either order. Both give the same total.
- The total can be large: up to about 1000 lines, each up to 1000, times up to 1000 pieces. Use
long. - Strip counts start at 1, not 0. One whole bar is one strip in each direction.
- On SPOJ, the input gives
mandnand thenm - 1andn - 1prices. Read them in the right order.
What interviewers ask next
- Prove that sorting is right. Take two cuts next to each other in some order, the cheaper one first. Swapping them makes the pricier one cross fewer or equal pieces and the cheaper one cross more or equal. The change in cost is never positive.
- What if a snap may only break one piece along a part of a line, instead of the whole line? That is the same thing here, because every part of every line must be cut in the end.
- Minimum cost to cut a stick at given points, where each cut costs the current stick length (LeetCode 1547)? That one is not greedy. It needs interval dynamic programming.
- Can you skip sorting? If prices are small, counting sort makes it
O(r + c + maxPrice).
Remember this
Grown-up words: grooves are cut lines, strips are the segments between cuts, picking the biggest price each time is a greedy merge of two sorted lists, and the swapping proof is an exchange argument.