Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

2052. Minimum Cost to Separate Sentence Into Rows - Leetcode Solution

Problem Description

You are given a string sentence and an integer k. The goal is to split the sentence into multiple rows such that each row contains a contiguous sequence of words from the original sentence, and the total length of each row does not exceed k (including spaces between words).

For each row except the last, you must pay a cost equal to (k - length of the row)^2. The last row incurs no cost regardless of its length. Your task is to determine the minimum total cost required to split the sentence into rows under these conditions.

Constraints:

  • Each word is separated by a single space
  • You cannot split a word between rows
  • 1 ≤ length of sentence ≤ 1000
  • 1 ≤ k ≤ 1000

Thought Process

At first glance, this problem seems similar to text justification or word wrapping. A brute-force approach would be to try every possible way to split the sentence and compute the cost for each configuration, but this quickly becomes impractical as the number of words grows.

To optimize, we notice that the problem has overlapping subproblems: the minimum cost to arrange the rest of the sentence after a certain word can be reused if we encounter the same sub-sentence again. This hints at a dynamic programming (DP) approach, where we store the minimum cost for each possible starting word index.

The key is to try every possible way to break the sentence at each word, calculate the cost for the current row, and recursively solve for the rest. We keep track of the minimum cost encountered.

Solution Approach

We use a dynamic programming strategy to efficiently compute the minimum cost:

  1. Tokenize the sentence: Split the sentence into a list of words.
  2. Define DP State: Let dp[i] represent the minimum cost to arrange the words starting from index i.
  3. Base Case: If i is at the end of the words list, the cost is 0 (no words left).
  4. Transition: For each possible end index j (starting from i), check if the length of words from i to j fits in a row (including spaces). If it does, calculate the cost for this row:
    • If j is the last word, cost is 0.
    • Otherwise, cost is (k - row_length)^2 plus dp[j+1].
  5. Memoization: Use a cache to avoid recomputing the minimum cost for the same starting index.
  6. Result: The answer is dp[0], the minimum cost to arrange all words.

This approach ensures we only compute each subproblem once, making it efficient even for large sentences.

Example Walkthrough

Example:
sentence = "hello world leetcode", k = 12

  1. Words: ["hello", "world", "leetcode"]
  2. Try to fit as many words as possible in the first row:
    • "hello" (5 chars) → fits (cost = (12-5)^2 = 49 + dp[1])
    • "hello world" (11 chars) → fits (cost = (12-11)^2 = 1 + dp[2])
    • "hello world leetcode" (19 chars) → does not fit (exceeds k)
  3. Recursively compute dp[1] and dp[2]:
    • dp[1]: Try "world" (5 chars), "world leetcode" (13 chars, doesn't fit)
    • So, "world" (cost = (12-5)^2 = 49 + dp[2])
    • dp[2]: "leetcode" is last word, so cost = 0
  4. Backtrack and choose the split with minimum total cost.

The minimum cost is achieved by splitting as ["hello world", "leetcode"], with total cost = 1.

Time and Space Complexity

Brute-force approach: For each word, we can split at every possible position, leading to exponential combinations (O(2^n)). This is not feasible for large n.

Optimized DP approach:

  • There are O(n) subproblems (one for each word index).
  • For each subproblem, we try up to O(n) possible splits (from current index to end).
  • Total time complexity: O(n^2), where n is the number of words.
  • Space complexity: O(n) for the DP array or memoization table.

Summary

The problem is a variation of the classic word wrap or text justification problem, where we seek to minimize the cost of splitting a sentence into rows of at most k characters. By using dynamic programming and memoization, we efficiently compute the minimum cost for each sub-sentence, avoiding redundant calculations. The solution demonstrates the power of DP in optimizing problems with overlapping subproblems and optimal substructure.

Code Implementation

from functools import lru_cache

class Solution:
    def minimumCost(self, sentence: str, k: int) -> int:
        words = sentence.split()
        n = len(words)

        @lru_cache(maxsize=None)
        def dp(i):
            if i == n:
                return 0
            min_cost = float('inf')
            length = 0
            for j in range(i, n):
                length += len(words[j])
                if j > i:
                    length += 1  # space
                if length > k:
                    break
                if j == n - 1:
                    cost = 0
                else:
                    cost = (k - length) ** 2
                min_cost = min(min_cost, cost + dp(j + 1))
            return min_cost

        return dp(0)
      
#include <vector>
#include <string>
#include <climits>
using namespace std;

class Solution {
public:
    int minimumCost(string sentence, int k) {
        vector<string> words;
        int n = 0;
        size_t pos = 0, found;
        while ((found = sentence.find(' ', pos)) != string::npos) {
            words.push_back(sentence.substr(pos, found - pos));
            pos = found + 1;
        }
        words.push_back(sentence.substr(pos));
        n = words.size();

        vector<int> dp(n + 1, -1);

        function<int(int)> solve = [&](int i) {
            if (i == n) return 0;
            if (dp[i] != -1) return dp[i];
            int min_cost = INT_MAX;
            int length = 0;
            for (int j = i; j < n; ++j) {
                length += words[j].length();
                if (j > i) length += 1;
                if (length > k) break;
                int cost = (j == n - 1) ? 0 : (k - length) * (k - length);
                int total = cost + solve(j + 1);
                if (total < min_cost) min_cost = total;
            }
            dp[i] = min_cost;
            return min_cost;
        };

        return solve(0);
    }
};
      
import java.util.*;

class Solution {
    public int minimumCost(String sentence, int k) {
        String[] words = sentence.split(" ");
        int n = words.length;
        Integer[] memo = new Integer[n + 1];

        return dp(0, words, k, memo);
    }

    private int dp(int i, String[] words, int k, Integer[] memo) {
        int n = words.length;
        if (i == n) return 0;
        if (memo[i] != null) return memo[i];
        int minCost = Integer.MAX_VALUE;
        int length = 0;
        for (int j = i; j < n; ++j) {
            length += words[j].length();
            if (j > i) length += 1;
            if (length > k) break;
            int cost = (j == n - 1) ? 0 : (k - length) * (k - length);
            int total = cost + dp(j + 1, words, k, memo);
            if (total < minCost) minCost = total;
        }
        memo[i] = minCost;
        return minCost;
    }
}
      
/**
 * @param {string} sentence
 * @param {number} k
 * @return {number}
 */
var minimumCost = function(sentence, k) {
    const words = sentence.split(' ');
    const n = words.length;
    const memo = new Array(n + 1).fill(undefined);

    function dp(i) {
        if (i === n) return 0;
        if (memo[i] !== undefined) return memo[i];
        let minCost = Infinity;
        let length = 0;
        for (let j = i; j < n; ++j) {
            length += words[j].length;
            if (j > i) length += 1;
            if (length > k) break;
            let cost = (j === n - 1) ? 0 : Math.pow(k - length, 2);
            minCost = Math.min(minCost, cost + dp(j + 1));
        }
        memo[i] = minCost;
        return minCost;
    }

    return dp(0);
};