Facebook Pixel

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
  • 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
  • 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.

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

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 word i+1
  • Put words i and i+1 on this row and continue from word i+2
  • Put words i, i+1, and i+2 on this row and continue from word i+3
  • And so on...

Each choice has two components:

  1. The cost of the current row (which we can calculate immediately)
  2. 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 where nums[i] is the length of the i-th word
  • Build prefix sum array s where s[i] represents the total length of the first i 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 to n-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 from i to j-1 inclusive:
    • s[j] - s[i]: sum of word lengths
    • j - 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 Evaluator

Example 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)
  • 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)
  • 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)

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 state i is computed at most once
  • There are n possible states (one for each starting position from 0 to n-1)
  • For each state i, the while loop iterates through positions j from i+1 until either j >= n or the condition s[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 have n states and each state does O(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, requiring O(n) space
  • The prefix sum array s has length n + 1, requiring O(n) space
  • The memoization cache stores at most n different states, each taking O(1) space, for a total of O(n)
  • The recursion depth is at most n in the worst case (when each word forms its own line), contributing O(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 index i to j-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:

  1. Always calculate line length as: (sum of word lengths) + (number of spaces between words)
  2. Be precise with index ranges when using prefix sums
  3. Remember the last line has zero cost - implement the base case correctly
  4. Consider edge cases like words longer than k
  5. Always use memoization for overlapping subproblems
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following array represent a max heap?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More