2052. Minimum Cost to Separate Sentence Into Rows 🔒
Problem Description
You are given a string sentence
containing words separated by spaces and an integer k
. Your task is to split the sentence into multiple rows where each row can contain at most k
characters.
Rules for splitting:
- Words cannot be broken across rows - each word must appear completely in one row
- Words must maintain their original order
- Adjacent words in the same row are separated by a single space
- Rows cannot start or end with spaces
- Each row's length (including spaces between words) must not exceed
k
Cost calculation:
- The cost of a row with length
n
is(k - n)²
- The total cost is the sum of costs for all rows except the last row
- You need to find the minimum possible total cost
Example:
Given sentence = "i love leetcode"
and k = 12
:
-
Splitting into
"i"
,"love"
,"leetcode"
:- Row 1:
"i"
has length 1, cost =(12 - 1)² = 121
- Row 2:
"love"
has length 4, cost =(12 - 4)² = 64
- Row 3:
"leetcode"
has length 8, no cost (last row) - Total cost =
121 + 64 = 185
- Row 1:
-
Splitting into
"i love"
,"leetcode"
:- Row 1:
"i love"
has length 6, cost =(12 - 6)² = 36
- Row 2:
"leetcode"
has length 8, no cost (last row) - Total cost =
36
- Row 1:
-
Splitting into
"i"
,"love leetcode"
is invalid because"love leetcode"
has length 13 > 12
The goal is to find the splitting strategy that minimizes the total cost.
Intuition
This is a classic optimization problem where we need to make decisions about where to place line breaks. At each point, we face a choice: which words should go on the current row before we start a new row?
The key insight is that this problem has optimal substructure - once we decide to end a row at a certain word, the minimum cost for the remaining words is independent of how we got to that point. This suggests a dynamic programming approach.
Think of it this way: suppose we're at word i
and need to decide where to end the current row. We could:
- Put just word
i
on this row and continue from wordi+1
- Put words
i
andi+1
on this row and continue from wordi+2
- Put words
i
,i+1
, andi+2
on this row and continue from wordi+3
- And so on...
Each choice has two components:
- The cost of the current row (which we can calculate immediately)
- The minimum cost for arranging all remaining words (which is a subproblem)
The constraint is that we can only fit words on the current row as long as their total length plus spaces doesn't exceed k
.
To efficiently calculate row lengths, we can use prefix sums. If s[i]
represents the sum of lengths of the first i
words, then the length of words from index i
to j
(inclusive) plus the spaces between them is: s[j+1] - s[i] + (j - i)
(the last term accounts for spaces).
The special case is when all remaining words can fit on one row - since the last row has no cost, the total cost would be 0
in this case.
This naturally leads to a recursive solution with memoization: for each position, try all valid ways to form the current row, calculate its cost, add the minimum cost for the remaining words, and choose the option with minimum total cost.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution implements a memoized recursive approach with prefix sums to efficiently calculate row lengths.
Step 1: Preprocessing
- Extract word lengths into array
nums
wherenums[i]
is the length of thei
-th word - Build prefix sum array
s
wheres[i]
represents the total length of the firsti
words - This allows us to calculate the length of any substring of words in O(1) time
Step 2: Define the recursive function dfs(i)
The function dfs(i)
returns the minimum cost to arrange words starting from index i
to the end.
Step 3: Base case check
if s[n] - s[i] + n - i - 1 <= k: return 0
- Calculate if all remaining words (from
i
ton-1
) can fit in one row - Length calculation:
s[n] - s[i]
gives total word lengths,n - i - 1
adds the spaces between words - If they fit, return
0
(no cost for the last row)
Step 4: Try all valid split points
ans = inf
j = i + 1
while j < n and (m := s[j] - s[i] + j - i - 1) <= k:
ans = min(ans, dfs(j) + (k - m) ** 2)
j += 1
- Start from
j = i + 1
(at least one word must go on the current row) - Calculate length
m
of words fromi
toj-1
inclusive:s[j] - s[i]
: sum of word lengthsj - i - 1
: number of spaces between words
- While this length
≤ k
, it's a valid row configuration - Cost for this choice:
(k - m)²
for current row +dfs(j)
for remaining words - Track the minimum cost among all valid choices
Step 5: Memoization
The @cache
decorator automatically memoizes the results, preventing redundant calculations for the same subproblems.
Time Complexity: O(n²) where n is the number of words, as we have n possible starting positions and for each we might check up to n ending positions.
Space Complexity: O(n) for the recursion stack and memoization cache.
The final answer is dfs(0)
, which gives the minimum cost to arrange all words starting from index 0.
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 sentence = "a bb cc"
and k = 5
.
Step 1: Preprocessing
- Words:
["a", "bb", "cc"]
- Word lengths
nums = [1, 2, 2]
- Prefix sums
s = [0, 1, 3, 5]
s[0] = 0
(no words)s[1] = 1
(length of "a")s[2] = 3
(length of "a" + "bb")s[3] = 5
(length of "a" + "bb" + "cc")
Step 2: Start with dfs(0)
- arranging words from index 0
Check base case: Can all words fit in one row?
- Length =
s[3] - s[0] + 3 - 0 - 1 = 5 + 2 = 7
- Since 7 > 5, we cannot fit all words in one row
Try different split points:
-
Option 1: Put only "a" on first row (j=1)
- Row length:
s[1] - s[0] + 1 - 0 - 1 = 1 + 0 = 1
- Valid (1 ≤ 5)
- Cost:
(5 - 1)² + dfs(1) = 16 + dfs(1)
- Row length:
-
Option 2: Put "a bb" on first row (j=2)
- Row length:
s[2] - s[0] + 2 - 0 - 1 = 3 + 1 = 4
- Valid (4 ≤ 5)
- Cost:
(5 - 4)² + dfs(2) = 1 + dfs(2)
- Row length:
-
Option 3: Put "a bb cc" on first row (j=3)
- Row length:
s[3] - s[0] + 3 - 0 - 1 = 5 + 2 = 7
- Invalid (7 > 5)
- Row length:
Step 3: Calculate dfs(1)
- arranging words from index 1
Check base case: Can "bb cc" fit in one row?
- Length =
s[3] - s[1] + 3 - 1 - 1 = 4 + 1 = 5
- Since 5 ≤ 5, yes! Return 0 (last row has no cost)
Step 4: Calculate dfs(2)
- arranging words from index 2
Check base case: Can "cc" fit in one row?
- Length =
s[3] - s[2] + 3 - 2 - 1 = 2 + 0 = 2
- Since 2 ≤ 5, yes! Return 0 (last row has no cost)
Step 5: Back to dfs(0)
, choose minimum
- Option 1:
16 + 0 = 16
- Option 2:
1 + 0 = 1
- Minimum = 1
Final arrangement:
- Row 1: "a bb" (length 4, cost = 1)
- Row 2: "cc" (length 2, no cost as it's the last row)
- Total cost = 1
Solution Implementation
1from functools import cache
2from itertools import accumulate
3from math import inf
4from typing import List
5
6
7class Solution:
8 def minimumCost(self, sentence: str, k: int) -> int:
9 """
10 Find the minimum cost to split a sentence into lines where each line
11 has at most k characters (including spaces between words).
12 Cost for a line with unused space m is m^2 (except the last line which has 0 cost).
13
14 Args:
15 sentence: Input sentence with words separated by spaces
16 k: Maximum characters allowed per line
17
18 Returns:
19 Minimum total cost to split the sentence
20 """
21
22 # Split sentence into words and get their lengths
23 word_lengths = [len(word) for word in sentence.split()]
24 num_words = len(word_lengths)
25
26 # Create prefix sum array for quick range sum calculation
27 # prefix_sums[i] = sum of lengths of first i words
28 prefix_sums = list(accumulate(word_lengths, initial=0))
29
30 @cache
31 def find_min_cost(start_index: int) -> int:
32 """
33 Find minimum cost to arrange words from start_index to end.
34
35 Args:
36 start_index: Index of the first word to consider
37
38 Returns:
39 Minimum cost to arrange remaining words
40 """
41
42 # Calculate total length needed for remaining words (including spaces)
43 # This represents putting all remaining words on one line
44 remaining_length = prefix_sums[num_words] - prefix_sums[start_index]
45 spaces_needed = num_words - start_index - 1
46 total_length_needed = remaining_length + spaces_needed
47
48 # Base case: if all remaining words fit in one line, cost is 0
49 # (last line has no penalty)
50 if total_length_needed <= k:
51 return 0
52
53 min_cost = inf
54 current_index = start_index + 1
55
56 # Try different split points for the current line
57 while current_index < num_words:
58 # Calculate length of current line (words from start_index to current_index-1)
59 words_length = prefix_sums[current_index] - prefix_sums[start_index]
60 spaces_in_line = current_index - start_index - 1
61 line_length = words_length + spaces_in_line
62
63 # If current line exceeds limit, stop trying further splits
64 if line_length > k:
65 break
66
67 # Calculate cost: unused space squared + cost of remaining arrangement
68 unused_space = k - line_length
69 current_cost = find_min_cost(current_index) + (unused_space ** 2)
70 min_cost = min(min_cost, current_cost)
71
72 current_index += 1
73
74 return min_cost
75
76 return find_min_cost(0)
77
1class Solution {
2 // Memoization array to store minimum cost from index i to end
3 private Integer[] memo;
4 // Prefix sum array to store cumulative word lengths
5 private int[] prefixSum;
6 // Maximum allowed characters per line
7 private int maxLineLength;
8 // Total number of words
9 private int wordCount;
10
11 public int minimumCost(String sentence, int k) {
12 this.maxLineLength = k;
13 String[] words = sentence.split(" ");
14 this.wordCount = words.length;
15 this.memo = new Integer[wordCount];
16 this.prefixSum = new int[wordCount + 1];
17
18 // Build prefix sum array for quick range sum calculation
19 // prefixSum[i+1] stores the sum of lengths of words from index 0 to i
20 for (int i = 0; i < wordCount; i++) {
21 prefixSum[i + 1] = prefixSum[i] + words[i].length();
22 }
23
24 return findMinimumCost(0);
25 }
26
27 private int findMinimumCost(int startIndex) {
28 // Base case: if remaining words can fit in one line, no cost
29 // Calculate total length of remaining words plus spaces between them
30 int remainingLength = prefixSum[wordCount] - prefixSum[startIndex] + wordCount - startIndex - 1;
31 if (remainingLength <= maxLineLength) {
32 return 0;
33 }
34
35 // Return memoized result if already computed
36 if (memo[startIndex] != null) {
37 return memo[startIndex];
38 }
39
40 int minimumCost = Integer.MAX_VALUE;
41
42 // Try placing words from startIndex to endIndex-1 on current line
43 for (int endIndex = startIndex + 1; endIndex < wordCount; endIndex++) {
44 // Calculate length of words from startIndex to endIndex-1 including spaces
45 int lineLength = prefixSum[endIndex] - prefixSum[startIndex] + endIndex - startIndex - 1;
46
47 // If current line exceeds maximum length, stop trying
48 if (lineLength > maxLineLength) {
49 break;
50 }
51
52 // Calculate cost: (maxLineLength - lineLength)^2 plus cost of remaining words
53 int extraSpaces = maxLineLength - lineLength;
54 int currentCost = findMinimumCost(endIndex) + extraSpaces * extraSpaces;
55 minimumCost = Math.min(minimumCost, currentCost);
56 }
57
58 // Memoize and return the result
59 memo[startIndex] = minimumCost;
60 return minimumCost;
61 }
62}
63
1class Solution {
2public:
3 int minimumCost(string sentence, int k) {
4 // Parse the sentence into words and build prefix sum array
5 istringstream iss(sentence);
6 vector<int> prefixSum = {0}; // prefixSum[i] = total length of first i words (without spaces)
7 string word;
8 while (iss >> word) {
9 prefixSum.push_back(prefixSum.back() + word.size());
10 }
11
12 int wordCount = prefixSum.size() - 1; // Total number of words
13
14 // Memoization array for dynamic programming
15 // memo[i] = minimum cost to arrange words starting from index i
16 int memo[wordCount];
17 memset(memo, -1, sizeof(memo));
18
19 // Recursive function with memoization to find minimum cost
20 auto dfs = [&](this auto&& dfs, int startIdx) -> int {
21 // Base case: if remaining words can fit in one line, no extra cost
22 // Calculate total length: sum of word lengths + spaces between words
23 int remainingLength = prefixSum[wordCount] - prefixSum[startIdx] +
24 (wordCount - startIdx - 1);
25 if (remainingLength <= k) {
26 return 0;
27 }
28
29 // Return memoized result if already computed
30 if (memo[startIdx] != -1) {
31 return memo[startIdx];
32 }
33
34 int minCost = INT_MAX;
35
36 // Try placing words [startIdx, endIdx) on the current line
37 for (int endIdx = startIdx + 1; endIdx < wordCount; ++endIdx) {
38 // Calculate line length: word lengths + spaces between words
39 int lineLength = prefixSum[endIdx] - prefixSum[startIdx] +
40 (endIdx - startIdx - 1);
41
42 // Skip if line length exceeds k
43 if (lineLength > k) {
44 break;
45 }
46
47 // Calculate cost for this line and recursively compute remaining cost
48 int extraSpaces = k - lineLength;
49 int currentCost = dfs(endIdx) + extraSpaces * extraSpaces;
50 minCost = min(minCost, currentCost);
51 }
52
53 // Store and return the minimum cost
54 return memo[startIdx] = minCost;
55 };
56
57 // Start the recursive computation from the first word
58 return dfs(0);
59 }
60};
61
1/**
2 * Finds the minimum cost to split a sentence into lines with a maximum width of k characters.
3 * The cost of a line is the square of the number of trailing spaces (k - line_length).
4 *
5 * @param sentence - The input sentence to be split
6 * @param k - The maximum width of each line
7 * @returns The minimum total cost to split the sentence
8 */
9function minimumCost(sentence: string, k: number): number {
10 // Build prefix sum array of word lengths
11 // prefixSum[i] represents the total length of first i words (without spaces)
12 const prefixSum: number[] = [0];
13 const words: string[] = sentence.split(' ');
14
15 for (const word of words) {
16 prefixSum.push(prefixSum[prefixSum.length - 1] + word.length);
17 }
18
19 const wordCount: number = prefixSum.length - 1;
20
21 // Memoization array for dynamic programming
22 // memo[i] stores the minimum cost starting from word i
23 const memo: number[] = Array(wordCount).fill(-1);
24
25 /**
26 * Recursive function with memoization to calculate minimum cost
27 * starting from word at index wordIndex
28 *
29 * @param wordIndex - The starting word index for current subproblem
30 * @returns The minimum cost for arranging words from wordIndex to end
31 */
32 const calculateMinCost = (wordIndex: number): number => {
33 // Base case: if remaining words can fit in one line, no cost
34 // Calculate total length including spaces between words
35 const remainingLength: number = prefixSum[wordCount] - prefixSum[wordIndex] +
36 (wordCount - wordIndex - 1);
37
38 if (remainingLength <= k) {
39 return 0;
40 }
41
42 // Return memoized result if already calculated
43 if (memo[wordIndex] !== -1) {
44 return memo[wordIndex];
45 }
46
47 let minCost: number = Infinity;
48
49 // Try placing words from wordIndex to nextLineStart-1 on current line
50 for (let nextLineStart = wordIndex + 1; nextLineStart < wordCount; nextLineStart++) {
51 // Calculate length of current line (words + spaces between them)
52 const currentLineLength: number = prefixSum[nextLineStart] - prefixSum[wordIndex] +
53 (nextLineStart - wordIndex - 1);
54
55 // Break if current line exceeds maximum width
56 if (currentLineLength > k) {
57 break;
58 }
59
60 // Calculate cost for current line + minimum cost for remaining words
61 const trailingSpaces: number = k - currentLineLength;
62 const currentLineCost: number = trailingSpaces * trailingSpaces;
63 const totalCost: number = calculateMinCost(nextLineStart) + currentLineCost;
64
65 minCost = Math.min(minCost, totalCost);
66 }
67
68 // Store and return the minimum cost
69 memo[wordIndex] = minCost;
70 return minCost;
71 };
72
73 return calculateMinCost(0);
74}
75
Time and Space Complexity
The time complexity is O(n^2)
, where n
is the number of words in the sentence.
Time Complexity Analysis:
- The function
dfs(i)
uses memoization with@cache
, so each statei
is computed at most once - There are
n
possible states (one for each starting position from0
ton-1
) - For each state
i
, the while loop iterates through positionsj
fromi+1
until eitherj >= n
or the conditions[j] - s[i] + j - i - 1 > k
is met - In the worst case, this loop can iterate up to
n - i - 1
times - The total work across all states is bounded by
O(n^2)
since we haven
states and each state doesO(n)
work in the worst case - The preprocessing step (splitting the sentence and computing prefix sums) takes
O(n)
time - Therefore, the overall time complexity is
O(n^2)
The space complexity is O(n)
.
Space Complexity Analysis:
- The
nums
array stores the length of each word, requiringO(n)
space - The prefix sum array
s
has lengthn + 1
, requiringO(n)
space - The memoization cache stores at most
n
different states, each takingO(1)
space, for a total ofO(n)
- The recursion depth is at most
n
in the worst case (when each word forms its own line), contributingO(n)
to the call stack - Therefore, the overall space complexity is
O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Length Calculation for Lines
One of the most common mistakes is incorrectly calculating the length of a line containing multiple words. Developers often forget to account for spaces between words or miscalculate the number of spaces needed.
Incorrect approach:
# Wrong: Forgetting spaces or using wrong space count line_length = prefix_sums[j] - prefix_sums[i] # Missing spaces # or line_length = prefix_sums[j] - prefix_sums[i] + j - i # One space too many
Correct approach:
# For words from index i to j-1 (inclusive): words_length = prefix_sums[j] - prefix_sums[i] num_spaces = j - i - 1 # Number of spaces between words line_length = words_length + num_spaces
2. Off-by-One Errors in Index Handling
The relationship between word indices and prefix sum indices can be confusing. Remember that prefix_sums[i]
represents the sum of the first i
words (0-indexed), so prefix_sums[j] - prefix_sums[i]
gives the sum of words from index i
to j-1
.
Incorrect approach:
# Wrong: Thinking this includes word at index j words_sum = prefix_sums[j] - prefix_sums[i] # Actually excludes word j
Correct understanding:
prefix_sums[0] = 0
(no words)prefix_sums[1] = length of word 0
prefix_sums[j] - prefix_sums[i]
= sum of words from indexi
toj-1
3. Forgetting the Last Line Has No Cost
A critical aspect of this problem is that the last line doesn't contribute to the total cost. Failing to handle this special case will result in incorrect answers.
Incorrect approach:
def dfs(i):
# Wrong: Always adding cost for every line
min_cost = inf
for j in range(i + 1, n + 1):
line_length = calculate_length(i, j)
if line_length <= k:
cost = (k - line_length) ** 2 + dfs(j)
min_cost = min(min_cost, cost)
return min_cost
Correct approach:
def dfs(i):
# Check if remaining words can fit in one line (last line)
if all_remaining_words_fit_in_one_line:
return 0 # No cost for last line
# Otherwise, try different splits
min_cost = inf
# ... rest of the logic
4. Not Validating Single Word Length
While the problem typically assumes each individual word fits within k
characters, it's good practice to validate this assumption or handle the edge case where a single word exceeds k
.
Potential issue:
# If any word has length > k, the problem becomes unsolvable
words = sentence.split()
for word in words:
if len(word) > k:
return -1 # or handle appropriately
5. Inefficient Recalculation Without Memoization
Without memoization, the recursive solution would have exponential time complexity due to recalculating the same subproblems multiple times.
Inefficient approach:
def dfs(i): # Without @cache decorator
# This will recalculate dfs(i) multiple times
...
Efficient approach:
@cache # Essential for performance
def dfs(i):
...
Solution Summary
To avoid these pitfalls:
- Always calculate line length as:
(sum of word lengths) + (number of spaces between words)
- Be precise with index ranges when using prefix sums
- Remember the last line has zero cost - implement the base case correctly
- Consider edge cases like words longer than
k
- Always use memoization for overlapping subproblems
Which of the following array represent a max heap?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!