Maximum Deletions on a String
HardPrompt
You are given a string s consisting of only lowercase English letters. In one operation, you can:
- Delete the entire string
s, or - Delete the first
iletters ofsif the firstiletters ofsare equal to the followingiletters ins, for anyiin the range1 <= i <= s.length / 2.
For example, if s = "ababc", then in one operation, you could delete the first two letters of s to get "abc", since the first two letters of s and the following two letters of s are both equal to "ab".
Return the maximum number of operations needed to delete all of s.
Example 1:
Input: s = "abcabcdabc"
Output: 2
Explanation:
- Delete the first 3 letters ("abc") since the next 3 letters are equal. Now, s = "abcdabc".
- Delete all the letters.
We used 2 operations so return 2. It can be proven that 2 is the maximum number of operations needed.
Note that in the second operation we cannot delete "abc" again because the next occurrence of "abc" does not happen in the next 3 letters.Example 2:
Input: s = "aaabaab"
Output: 4
Explanation:
- Delete the first letter ("a") since the next letter is equal. Now, s = "aabaab".
- Delete the first 3 letters ("aab") since the next 3 letters are equal. Now, s = "aab".
- Delete the first letter ("a") since the next letter is equal. Now, s = "ab".
- Delete all the letters.
We used 4 operations so return 4. It can be proven that 4 is the maximum number of operations needed.Example 3:
Input: s = "aaaaa"
Output: 5
Explanation: In each operation, we can delete the first letter of s.
Constraints:
1 <= s.length <= 4000sconsists only of lowercase English letters.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach uses dynamic programming to solve the problem. We define dp[i] as the maximum number of operations to delete the substring starting at index i. We compute dp[i] for i from n-1 down to 0. For each i, we consider all possible first moves. The base case is that we can always delete the entire remaining substring in one operation, so dp[i] is at least 1. Then, we check for all possible lengths j if the prefix of length j can be deleted. If s.substring(i, i+j) equals s.substring(i+j, i+2*j), we can perform an operation and transition to the state i+j. The number of operations would be 1 + dp[i+j]. We take the maximum over all such possibilities.
Algorithm
- Let
nbe the length ofs. - Initialize
dparray of sizen+1.dp[n] = 0. - Loop
ifromn-1down to0:dp[i] = 1.- Loop
jfrom1to(n-i)/2:- If
s.substring(i, i+j).equals(s.substring(i+j, i+2*j)):dp[i] = max(dp[i], 1 + dp[i+j]).
- If
- Return
dp[0].
Walkthrough
The core idea is to build up a solution from smaller subproblems. A subproblem is defined by the starting index i of the string we need to delete, i.e., s.substring(i). Let dp[i] be the maximum number of operations for this subproblem.
We can compute the dp array from right to left:
- Create a DP array
dpof sizen+1, wherenis the length of the strings.dp[i]will store the maximum operations for the suffixs.substring(i). Initializedp[n] = 0(empty string needs 0 operations). - Iterate
ifromn-1down to0. - For each
i, initializedp[i] = 1, representing the single operation of deleting the entire remaining substrings.substring(i). - Then, iterate through all possible prefix lengths
jfrom1to(n-i)/2. - In the inner loop, check if the prefix of length
jis equal to the nextjcharacters. This is done by comparings.substring(i, i+j)withs.substring(i+j, i+2*j). - If they are equal, it means we can perform a prefix deletion operation. This move leads to the subproblem of deleting
s.substring(i+j), which takesdp[i+j]operations. So, the total operations for this move would be1 + dp[i+j]. - Update
dp[i]with the maximum value found:dp[i] = max(dp[i], 1 + dp[i+j]). - After the loops complete,
dp[0]will hold the result for the entire strings.
The string comparison at each step takes O(j) time, leading to an overall cubic time complexity.
class Solution { public int deleteString(String s) { int n = s.length(); if (n == 0) { return 0; } int[] dp = new int[n + 1]; // dp[i] = max operations for s.substring(i) // dp[n] = 0 for empty string for (int i = n - 1; i >= 0; i--) { dp[i] = 1; // Operation to delete the entire remaining string for (int j = 1; i + 2 * j <= n; j++) { if (s.substring(i, i + j).equals(s.substring(i + j, i + 2 * j))) { dp[i] = Math.max(dp[i], 1 + dp[i + j]); } } } return dp[0]; }}Complexity
Time
O(n^3), where n is the length of the string. The outer loop runs `n` times. The inner loop runs up to `n/2` times. Inside the inner loop, `substring` creation and `equals` comparison take `O(j)` time, where `j` can be up to `n/2`. This gives a complexity of `Σ(i=0 to n-1) Σ(j=1 to (n-i)/2) O(j)`, which is `O(n^3)`.
Space
O(n) for the DP array. The substrings created in the loop also take space, but they can be considered temporary.
Trade-offs
Pros
Simple to understand and implement.
Follows a standard dynamic programming pattern.
Cons
Too slow for the given constraints (n <= 4000), will result in Time Limit Exceeded.
The brute-force DP approach is slow because of the repeated substring comparisons inside the nested loops. We can optimize this by pre-calculating the Longest Common Prefix (LCP) for all pairs of suffixes of the string. We can build an LCP table, lcp[i][j], which stores the length of the longest common prefix between s.substring(i) and s.substring(j). This table can be computed in O(n^2) time. With this table, checking if s.substring(i, i+j) equals s.substring(i+j, i+2*j) becomes an O(1) lookup: we just need to check if lcp[i][i+j] >= j. This optimization reduces the overall time complexity to O(n^2).
Algorithm
- Let
nbe the length ofs. - Create an LCP table
lcpof size(n+1) x (n+1). - Fill the
lcptable:- Loop
ifromn-1down to0:- Loop
jfromn-1down to0:- If
s.charAt(i) == s.charAt(j):lcp[i][j] = 1 + lcp[i+1][j+1].
- If
- Loop
- Loop
- Initialize
dparray of sizen+1.dp[n] = 0. - Loop
ifromn-1down to0:dp[i] = 1.- Loop
jfrom1to(n-i)/2:- If
lcp[i][i+j] >= j:dp[i] = max(dp[i], 1 + dp[i+j]).
- If
- Return
dp[0].
Walkthrough
The core DP logic remains the same: dp[i] is the max operations for s.substring(i). The optimization comes from precomputation.
- First, we precompute an LCP table. Let
lcp[i][j]be the length of the longest common prefix of suffixes starting atiandj. - The
lcptable is of size(n+1) x (n+1). We can fill it using dynamic programming. We iterateiandjfromn-1down to0. - If
s.charAt(i) == s.charAt(j), thenlcp[i][j] = 1 + lcp[i+1][j+1]. Otherwise,lcp[i][j] = 0. The base caseslcp[i][n]andlcp[n][j]are 0. - After building the
lcptable inO(n^2)time, we proceed with the main DP calculation. - Iterate
ifromn-1down to0. - Initialize
dp[i] = 1. - Iterate
jfrom1to(n-i)/2. - Instead of string comparison, check if
lcp[i][i+j] >= j. This is anO(1)operation. - If the condition is true, update
dp[i] = max(dp[i], 1 + dp[i+j]). - The final answer is
dp[0].
class Solution { public int deleteString(String s) { int n = s.length(); if (n == 0) { return 0; } // lcp[i][j]: length of longest common prefix of s.substring(i) and s.substring(j) int[][] lcp = new int[n + 1][n + 1]; for (int i = n - 1; i >= 0; i--) { for (int j = n - 1; j >= 0; j--) { if (s.charAt(i) == s.charAt(j)) { lcp[i][j] = 1 + lcp[i + 1][j + 1]; } } } int[] dp = new int[n + 1]; // dp[n] = 0 by default for (int i = n - 1; i >= 0; i--) { dp[i] = 1; for (int j = 1; i + 2 * j <= n; j++) { // Check if s.substring(i, i+j) == s.substring(i+j, i+2*j) if (lcp[i][i + j] >= j) { dp[i] = Math.max(dp[i], 1 + dp[i + j]); } } } return dp[0]; }}Complexity
Time
O(n^2). It takes `O(n^2)` to build the LCP table and `O(n^2)` for the main DP calculation.
Space
O(n^2). The LCP table requires `O(n^2)` space. The `dp` array takes `O(n)` space.
Trade-offs
Pros
Significantly faster than the brute-force approach.
Fast enough to pass the given constraints.
Cons
High space complexity due to the O(n^2) LCP table, which might be an issue for memory-constrained environments, although it's feasible for n=4000.
This approach achieves the same O(n^2) time complexity as the LCP table method but improves the space complexity to O(n). Instead of a large precomputed table, for each state i, we compute the necessary LCP information on-the-fly. The Z-algorithm is perfect for this. For each i, we compute the Z-array for the suffix s[i:]. The Z-value z[j] gives the LCP of s[i:] and s[i+j:]. The check s[i:i+j] == s[i+j:i+2*j] is equivalent to z[j] >= j. Computing the Z-array for s[i:] takes O(n-i) time. Summing over all i gives a total time of O(n^2).
Algorithm
- Convert
stochar[] sChars. - Initialize
dparray of sizen+1.dp[n] = 0. - Loop
ifromn-1down to0:dp[i] = 1.- Compute Z-array for
sCharsstarting from indexi. Let it bez. - Loop
jfrom1to(n-i)/2:- If
z[j] >= j:dp[i] = max(dp[i], 1 + dp[i+j]).
- If
- Return
dp[0].
Walkthrough
The overall DP structure is the same. We optimize the check for s.substring(i, i+j) == s.substring(i+j, i+2*j).
- We iterate
ifromn-1down to0. For eachi, we need to find for which lengthsjthe prefix ofs.substring(i)of lengthjis repeated. - This is a classic use case for the Z-algorithm. The Z-array
Zfor a stringTis defined such thatZ[k]is the length of the longest common prefix betweenTand the suffix ofTstarting atk. - For each
i, we can run the Z-algorithm on the suffixs.substring(i). Let the resulting Z-array bez_i. The conditions.substring(i, i+j) == s.substring(i+j, i+2*j)is then equivalent toz_i[j] >= j. - The Z-algorithm for a string of length
mruns inO(m)time. So for eachi, we spendO(n-i)time to compute the Z-array. The total time for alliisΣ(n-i)which isO(n^2). - The space required is
O(n)for thedparray andO(n)for the Z-array at each step, for a total ofO(n)space. - To implement this efficiently, we can convert the input string
sto a character array once and pass an offset to the Z-algorithm function to avoid creating new substrings.
class Solution { public int deleteString(String s) { int n = s.length(); int[] dp = new int[n + 1]; // dp[n] = 0 char[] sChars = s.toCharArray(); for (int i = n - 1; i >= 0; i--) { dp[i] = 1; int[] z = calculateZ(sChars, i); for (int j = 1; j <= (n - i) / 2; j++) { if (z[j] >= j) { dp[i] = Math.max(dp[i], 1 + dp[i + j]); } } } return dp[0]; } // Z-algorithm on a slice of a char array private int[] calculateZ(char[] t, int start) { int n = t.length - start; if (n == 0) return new int[0]; int[] z = new int[n]; int l = 0, r = 0; for (int i = 1; i < n; i++) { if (i <= r) { z[i] = Math.min(r - i + 1, z[i - l]); } while (i + z[i] < n && t[start + z[i]] == t[start + i + z[i]]) { z[i]++; } if (i + z[i] - 1 > r) { l = i; r = i + z[i] - 1; } } return z; }}Complexity
Time
O(n^2). For each `i` from `n-1` down to `0`, we run the Z-algorithm on a suffix of length `n-i`, which takes `O(n-i)` time. The total time is the sum `Σ O(n-i)` for `i` from `0` to `n-1`, which is `O(n^2)`.
Space
O(n). `O(n)` for the `dp` array and `O(n)` for the temporary Z-array used in each iteration of the outer loop.
Trade-offs
Pros
Optimal time complexity for this problem.
Best space complexity, making it the most efficient overall solution.
Cons
More complex to implement due to the Z-algorithm.
Solutions
Solution
class Solution {private int n;private Integer[] f;private int[][] g;public int deleteString(String s) { n = s.length(); f = new Integer[n]; g = new int[n + 1][n + 1]; for (int i = n - 1; i >= 0; --i) { for (int j = i + 1; j < n; ++j) { if (s.charAt(i) == s.charAt(j)) { g[i][j] = g[i + 1][j + 1] + 1; } } } return dfs(0); }private int dfs(int i) { if (i == n) { return 0; } if (f[i] != null) { return f[i]; } f[i] = 1; for (int j = 1; j <= (n - i) / 2; ++j) { if (g[i][i + j] >= j) { f[i] = Math.max(f[i], 1 + dfs(i + j)); } } return f[i]; }}class Solution {public: int deleteString(string s) { int n = s.size(); int g[n + 1][n + 1]; memset(g, 0, sizeof(g)); for (int i = n - 1; ~i; --i) { for (int j = i + 1; j < n; ++j) { if (s[i] == s[j]) { g[i][j] = g[i + 1][j + 1] + 1; } } } int f[n]; memset(f, 0, sizeof(f)); function<int(int)> dfs = [&](int i) -> int { if (i == n) { return 0; } if (f[i]) { return f[i]; } f[i] = 1; for (int j = 1; j <= (n - i) / 2; ++j) { if (g[i][i + j] >= j) { f[i] = max(f[i], 1 + dfs(i + j)); } } return f[i]; }; return dfs(0); }};class Solution: def deleteString(self, s: str) -> int: @ cache def dfs(i: int) -> int: if i == n: return 0 ans = 1 for j in range(1, (n - i) // 2 + 1): if s[i: i + j] == s[i + j: i + j + j]: ans = max(ans, 1 + dfs(i + j)) return ans n = len(s) return dfs(0)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.