Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

1531. String Compression II - Leetcode Solution

Problem Description

You are given a string s and an integer k. Your task is to delete at most k characters from s so that the length of the run-length encoded version of s is as short as possible.

The run-length encoding of a string compresses repeated consecutive characters into a single character followed by the count (if the count is more than 1). For example, aaabcccd is encoded as a3bc3d, which has length 6.

  • You may delete up to k characters anywhere in s.
  • You must minimize the length of the run-length encoded string after these deletions.
  • Return the minimum possible length of the run-length encoded string after at most k deletions.

Constraints:

  • 1 <= s.length <= 100
  • 0 <= k <= s.length
  • s consists of lowercase English letters only.

Thought Process

At first glance, the problem seems to invite a brute-force approach: try all possible ways to delete up to k characters, compress each resulting string, and find the minimum length. However, with up to 100 characters and possibly 100 deletions, the number of combinations is enormous and not feasible to check individually.

To solve this efficiently, we need to avoid recalculating the same subproblems repeatedly. Since the result depends on the current position in the string, the number of deletions left, and the current run of characters, it is a classic candidate for dynamic programming (DP).

The key insight is to use memoization to cache results for subproblems defined by the current index in the string and the remaining number of deletions. This way, we avoid redundant calculations and ensure that our solution is efficient even for the largest allowed inputs.

Solution Approach

We use a recursive dynamic programming approach with memoization. At each position in the string, we decide:

  • To delete the current character (if deletions are available) and move forward.
  • To keep the current character and try to extend a run of the same character as far as possible, possibly deleting intervening characters to maintain the run.

For each subproblem, we record:

  • i: The current position in the string.
  • k: The number of deletions remaining.
The DP function returns the minimum possible compressed length starting from position i with k deletions left.

  1. Base Case: If k < 0, return infinity (invalid). If i >= len(s), return 0 (no characters left).
  2. Option 1: Delete s[i] and solve the subproblem at i+1 with k-1 deletions left.
  3. Option 2: Keep s[i] and extend the run as far as possible:
    • For each possible end of a run starting at i, count how many deletions are needed to make all characters in the run the same as s[i].
    • For each possible run, compute the compressed length: 1 for the character + digits for the count (if count > 1).
    • Recursively solve for the next position after the run, with remaining deletions.
    • Keep the minimum result among all possibilities.
  4. Memoize the results for each (i, k) pair to avoid redundant work.

To compute the compressed length, remember:

  • For a run of length 1, length = 1 (just the character).
  • For length 2-9, length = 2 (character + 1 digit).
  • For length 10-99, length = 3 (character + 2 digits), etc.

Example Walkthrough

Example Input: s = "aaabcccd", k = 2

  1. We can delete up to 2 characters. Let's consider deleting both 'b' and 'd'. The string becomes "aaaccc".
    • Compressed: "a3c3" (length 4)
  2. Alternatively, delete one 'a' and one 'c': "aabcccd"
    • Compressed: "a2bc3d" (length 6)
  3. The DP will try all such combinations, but will find that deleting 'b' and 'd' gives the shortest compressed result.
  4. Final Answer: 4

At each step, the DP explores both deleting and keeping the current character, and for "keeping", it tries to extend the run by possibly deleting intervening characters, always choosing the minimum compressed length.

Time and Space Complexity

Brute-force approach: Would try all possible subsets of deletions (up to 2n), and for each, compress the string, which is infeasible for n up to 100.

Optimized DP approach:

  • There are at most O(n * k) subproblems, since i can range from 0 to n and k from 0 to k.
  • For each subproblem, we may consider up to O(n) possible runs (from i up to n), but since n is small (≤100), this is acceptable.
  • Total time complexity: O(n^2 * k)
  • Space complexity: O(n * k) for memoization.

Summary

This problem is a classic example of using dynamic programming to avoid redundant calculations in a problem with overlapping subproblems and optimal substructure. By carefully defining our state and memoizing results, we can efficiently explore all possible ways to delete up to k characters and minimize the compressed length. The solution elegantly balances recursion, memoization, and run-length encoding logic to handle all edge cases within the given constraints.

Code Implementation

from functools import lru_cache

class Solution:
    def getLengthOfOptimalCompression(self, s: str, k: int) -> int:
        n = len(s)
        
        @lru_cache(maxsize=None)
        def dp(i, k):
            if k < 0:
                return float('inf')
            if i >= n or n - i <= k:
                return 0
            res = float('inf')
            cnt = [0] * 26
            most = 0
            for j in range(i, n):
                idx = ord(s[j]) - ord('a')
                cnt[idx] += 1
                most = max(most, cnt[idx])
                del_needed = (j - i + 1) - most
                if k - del_needed < 0:
                    continue
                length = 1
                if most > 1:
                    length += len(str(most))
                res = min(res, length + dp(j + 1, k - del_needed))
            return res
        
        return dp(0, k)
      
#include <vector>
#include <string>
#include <algorithm>
#include <cstring>

using namespace std;

class Solution {
public:
    int dp[101][101];
    int getLength(int cnt) {
        if (cnt == 1) return 1;
        if (cnt < 10) return 2;
        if (cnt < 100) return 3;
        return 4;
    }
    int helper(const string& s, int i, int k) {
        int n = s.size();
        if (k < 0) return 1e9;
        if (i >= n || n - i <= k) return 0;
        if (dp[i][k] != -1) return dp[i][k];
        int res = 1e9;
        int cnt[26] = {0};
        int most = 0;
        for (int j = i; j < n; ++j) {
            cnt[s[j] - 'a']++;
            most = max(most, cnt[s[j] - 'a']);
            int del_needed = (j - i + 1) - most;
            if (k - del_needed < 0) continue;
            res = min(res, getLength(most) + helper(s, j + 1, k - del_needed));
        }
        return dp[i][k] = res;
    }
    int getLengthOfOptimalCompression(string s, int k) {
        memset(dp, -1, sizeof(dp));
        return helper(s, 0, k);
    }
};
      
import java.util.Arrays;

class Solution {
    private int[][] dp;
    private int getLength(int cnt) {
        if (cnt == 1) return 1;
        if (cnt < 10) return 2;
        if (cnt < 100) return 3;
        return 4;
    }
    private int helper(String s, int i, int k) {
        int n = s.length();
        if (k < 0) return 100000;
        if (i >= n || n - i <= k) return 0;
        if (dp[i][k] != -1) return dp[i][k];
        int[] cnt = new int[26];
        int most = 0, res = 100000;
        for (int j = i; j < n; ++j) {
            cnt[s.charAt(j) - 'a']++;
            most = Math.max(most, cnt[s.charAt(j) - 'a']);
            int del_needed = (j - i + 1) - most;
            if (k - del_needed < 0) continue;
            res = Math.min(res, getLength(most) + helper(s, j + 1, k - del_needed));
        }
        dp[i][k] = res;
        return res;
    }
    public int getLengthOfOptimalCompression(String s, int k) {
        dp = new int[s.length() + 1][k + 2];
        for (int[] row : dp) Arrays.fill(row, -1);
        return helper(s, 0, k);
    }
}
      
var getLengthOfOptimalCompression = function(s, k) {
    const n = s.length;
    const memo = new Map();

    function getLen(cnt) {
        if (cnt === 1) return 1;
        if (cnt < 10) return 2;
        if (cnt < 100) return 3;
        return 4;
    }

    function dp(i, k) {
        if (k < 0) return Infinity;
        if (i >= n || n - i <= k) return 0;
        const key = i + ',' + k;
        if (memo.has(key)) return memo.get(key);

        let res = Infinity;
        const cnt = Array(26).fill(0);
        let most = 0;
        for (let j = i; j < n; ++j) {
            cnt[s.charCodeAt(j) - 97]++;
            most = Math.max(most, cnt[s.charCodeAt(j) - 97]);
            const del_needed = (j - i + 1) - most;
            if (k - del_needed < 0) continue;
            res = Math.min(res, getLen(most) + dp(j + 1, k - del_needed));
        }
        memo.set(key, res);
        return res;
    }
    return dp(0, k);
};