String · 13 of 43
Word Wrap Problem
Break a list of words into lines of a fixed width so the empty space at the line ends is as even as possible, where big gaps cost a lot more than small ones.
- Simple way
- time O(2^n)
room O(n) - Clever way
- time O(n * k)
room O(n)
The problem, simply
You get the lengths of n words, in order, and a line width k. Words on the same line are separated by one space. You must keep the words in order and choose where each line ends. Every word fits on a line by itself.
Each line except the last one leaves some empty spots at its end, the extra spaces. The cost of a line is (extra spaces) times (extra spaces). The last line is free. Give back the smallest total cost.
Squaring is the point. Two lines with 2 empty spots each cost 4 + 4 = 8, but one line with 4 and one with 0 cost 16. Squares punish one big ugly gap more than a few small ones.
A story to remember it
Picture it
cat is so happy with width 6. Best layout: cat / is so / happy. The dashed boxes are extra spaces: 3 on line 1 and 1 on line 2. Cost 9 + 1 + 0 = 10. The underscore is the one space between two words.
- done
- ignored
Examples
A normal case
- In
lengths = [3, 2, 2, 5], k = 6- Out
10
Lines [3], [2, 2], [5]: the extra spaces are 3 and 1, so 9 + 1, and the last line is free.
Every word alone
- In
lengths = [3, 2, 2], k = 4- Out
5
2 + 1 + 2 = 5 is too wide for one line, so every word sits alone. Costs 1 + 4, and the last line is free.
All on one line
- In
lengths = [2, 2, 2], k = 10- Out
0
2 + 1 + 2 + 1 + 2 = 8 fits. One line, and it is the last one, so the cost is 0.
Perfectly full lines
- In
lengths = [1, 1, 1, 1], k = 3- Out
0
Two words per line use exactly 3 spots. No extra spaces on the first line, and the last line is free.
Way 1: the simple way
Try every choice. For the first word, try putting 1 word on the first line, then 2 words, and so on, while they fit. For each choice, solve the rest the same way and keep the cheapest.
- If no words are left, the cost is 0.
- Grow the first line one word at a time. The line length is the word lengths plus one space between each pair.
- Stop when the line is longer than
k. - The line costs
(k - length)^2, or0if it holds the last word. - Add the best cost of the rest, and keep the smallest total.
class Solution {
public int solveWordWrap(int[] nums, int k) {
return best(nums, k, 0);
}
// least total cost to place words i, i+1, ..., n-1
private int best(int[] nums, int k, int i) {
int n = nums.length;
if (i == n) return 0;
int answer = Integer.MAX_VALUE;
int len = -1; // so the first word adds no space
for (int j = i; j < n; j++) {
len += nums[j] + 1; // word j joins the line
if (len > k) break; // too wide
int extra = k - len;
int cost = (j == n - 1) ? 0 : extra * extra; // the last line is free
answer = Math.min(answer, cost + best(nums, k, j + 1));
}
return answer;
}
}How long it takes: every gap between two words is either a line break or not, so there are up to 2^(n - 1) layouts, and the same "rest" is solved many times. O(2^n). Extra room: the call stack, O(n).
Way 2: the clever way
The rest after word j always has the same best answer, no matter how you got there. So compute it once, from the last word to the first.
dp[i]= least cost to place wordsiton - 1. Setdp[n] = 0.- For
ifromn - 1down to0, grow a line that starts at wordi, one wordjat a time, while it fits. - Each choice costs
lineCost + dp[j + 1]. Keep the smallest indp[i]. - The answer is
dp[0].
Sticky notes from the back: dp[i] is the best cost from word i on
01 / 08
Width k = 6. dp[4] means no words left, so it costs 0. We fill dp from right to left.
- looking now
- checking
- done
- answer
- path
- just added
class Solution {
public int solveWordWrap(int[] nums, int k) {
int n = nums.length;
int[] dp = new int[n + 1]; // dp[i]: least cost for words i..n-1
dp[n] = 0; // no words left
for (int i = n - 1; i >= 0; i--) {
dp[i] = Integer.MAX_VALUE;
int len = -1;
for (int j = i; j < n; j++) {
len += nums[j] + 1; // put word j on the line that starts at i
if (len > k) break; // too wide: stop growing
int extra = k - len;
int cost = (j == n - 1) ? 0 : extra * extra;
dp[i] = Math.min(dp[i], cost + dp[j + 1]);
}
}
return dp[0];
}
}How long it takes: for each word, the inner loop runs once per word on its line, and a line holds at most about k / 2 words, so O(n * k), and never more than O(n^2). Extra room: the dp array, O(n).
Watch out for
- Forgetting the free last line. The line that holds word
n - 1costs 0, whatever its gaps. - Counting the spaces wrong.
mwords on a line usem - 1spaces. Startinglenat-1and addinglength + 1for each word does this neatly. - Overflow. With long lines and many words, squares add up fast. Use
longif the limits are big. - Not the same problem as LeetCode 68, Text Justification. There the lines are filled greedily and the spaces are spread inside each line. Ask which one is meant.
What interviewers ask next
- Print the lines too. Store
next[i] = j + 1for the best choice, then follownextfrom0. - Cost with cubes instead of squares, or a penalty for breaking certain words? Same
dp, only the line cost changes. - Faster for huge texts? Real typesetting (Knuth and Plass, used in TeX) uses the same idea with smarter pruning. Because a squared cost grows like a bowl, a known trick with a deque and binary search brings it to
O(n log n). Interviews rarely go that far. - Why is it fine to go right to left? The cost of the rest of the words does not depend on how the earlier lines were made.
Remember this
Grown-up words: the sticky notes are a 1D DP table, planning from the back is a suffix recurrence, filling each box full is the greedy approach, and the squared gaps are a convex cost function.