Delete Columns to Make Sorted III
HardPrompt
You are given an array of n strings strs, all of the same length.
We may choose any deletion indices, and we delete all the characters in those indices for each string.
For example, if we have strs = ["abcdef","uvwxyz"] and deletion indices {0, 2, 3}, then the final array after deletions is ["bef", "vyz"].
Suppose we chose a set of deletion indices answer such that after deletions, the final array has every string (row) in lexicographic order. (i.e., (strs[0][0] <= strs[0][1] <= ... <= strs[0][strs[0].length - 1]), and (strs[1][0] <= strs[1][1] <= ... <= strs[1][strs[1].length - 1]), and so on). Return the minimum possible value of answer.length.
Example 1:
Input: strs = ["babca","bbazb"]
Output: 3
Explanation: After deleting columns 0, 1, and 4, the final array is strs = ["bc", "az"].
Both these rows are individually in lexicographic order (ie. strs[0][0] <= strs[0][1] and strs[1][0] <= strs[1][1]).
Note that strs[0] > strs[1] - the array strs is not necessarily in lexicographic order.Example 2:
Input: strs = ["edcba"]
Output: 4
Explanation: If we delete less than 4 columns, the only row will not be lexicographically sorted.Example 3:
Input: strs = ["ghi","def","abc"]
Output: 0
Explanation: All rows are already lexicographically sorted.
Constraints:
n == strs.length1 <= n <= 1001 <= strs[i].length <= 100strs[i]consists of lowercase English letters.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach attempts to solve the problem by exploring all possible subsequences of columns. It uses a recursive function to generate and check these subsequences. At each column, it decides whether to include it in the subsequence or not, leading to an exponential number of possibilities.
Algorithm
- Define a recursive function
findMaxLen(prevCol, currCol)that returns the length of the longest valid subsequence of columns starting fromcurrCol, given the last included column wasprevCol. - The base case for the recursion is when
currColequals the total number of columns (L), in which case it returns 0. - In the recursive step, consider two choices for
currCol:- Exclude
currCol: Recursively callfindMaxLen(prevCol, currCol + 1). - Include
currCol: This is only possible ifcurrColis compatible withprevCol. IfprevColis -1 (start of sequence) or for all rowsr,strs[r][currCol] >= strs[r][prevCol], then it's compatible. The length would be1 + findMaxLen(currCol, currCol + 1).
- Exclude
- The function returns the maximum length from the valid choices.
- The initial call is
findMaxLen(-1, 0). - The final result is
L - findMaxLen(-1, 0).
Walkthrough
The problem asks for the minimum number of columns to delete, which is equivalent to finding the maximum number of columns to keep. This approach uses a brute-force recursive method to find the length of the longest subsequence of columns that satisfies the non-decreasing condition for every row.
A recursive function, let's call it findMaxLen(prevCol, currCol), is defined. prevCol is the index of the last column added to our subsequence, and currCol is the column we are currently considering.
For each currCol, we explore two possibilities:
- Exclude the column: We simply skip this column and move to the next one. The length of the subsequence in this case is determined by
findMaxLen(prevCol, currCol + 1). - Include the column: We can only include
currColif it maintains the sorted property with respect toprevCol. This means for every string instrs, the character atcurrColmust be greater than or equal to the character atprevCol. If this condition holds, we include the column, and the length becomes1 + findMaxLen(currCol, currCol + 1).
The function returns the maximum of the lengths obtained from these two choices. The recursion stops when currCol goes beyond the last column index, returning 0. The main function calls findMaxLen(-1, 0) to start the process and then subtracts the result from the total number of columns to get the minimum deletions.
class Solution { int n; int l; String[] strs; public int minDeletionSize(String[] strs) { this.n = strs.length; this.l = strs[0].length(); this.strs = strs; int maxLen = findMaxLen(-1, 0); return l - maxLen; } private int findMaxLen(int prevCol, int currCol) { if (currCol == l) { return 0; } // Option 1: Exclude currCol int len1 = findMaxLen(prevCol, currCol + 1); // Option 2: Include currCol boolean isCompatible = true; if (prevCol != -1) { for (int i = 0; i < n; i++) { if (strs[i].charAt(currCol) < strs[i].charAt(prevCol)) { isCompatible = false; break; } } } int len2 = 0; if (isCompatible) { len2 = 1 + findMaxLen(currCol, currCol + 1); } return Math.max(len1, len2); }}Complexity
Time
O(2^L * N), where L is the length of the strings and N is the number of strings. For each of the `L` columns, we have two choices (include or exclude), leading to `2^L` possibilities. The compatibility check takes O(N).
Space
O(L), where L is the length of the strings. This is for the recursion call stack depth.
Trade-offs
Pros
Conceptually simple and follows a straightforward divide-and-conquer logic.
Cons
Extremely inefficient due to re-computation of overlapping subproblems.
Will result in a 'Time Limit Exceeded' error on most platforms for the given constraints.
This approach optimizes the brute-force recursion by using memoization, a technique also known as top-down dynamic programming. It avoids re-calculating results for the same subproblems by storing them in a cache (a 2D array). This turns the exponential complexity into a polynomial one.
Algorithm
- Use the same recursive structure as the brute-force approach:
findMaxLen(prevCol, currCol). - Introduce a 2D memoization table,
memo[L+1][L], initialized with a value indicating 'not computed' (e.g., null). - Before any computation in
findMaxLen, check ifmemo[prevCol+1][currCol]has a stored value. If yes, return it immediately. - If the value is not in the memo table, perform the recursive calculations as in the brute-force approach.
- Store the computed result in
memo[prevCol+1][currCol]before returning it. - The rest of the logic remains the same.
Walkthrough
The brute-force recursive solution suffers from solving the same subproblems multiple times. A subproblem is uniquely identified by the state (prevCol, currCol). We can optimize this by storing the result of each subproblem in a memoization table.
We create a 2D array, memo, of size (L+1) x L. memo[prevCol + 1][currCol] will store the result of findMaxLen(prevCol, currCol). We add 1 to prevCol to handle the initial case where prevCol is -1.
Inside the recursive function, before any computation, we check if the result for the current state (prevCol, currCol) is already stored in our memo table. If it is, we return the stored value directly. Otherwise, we proceed with the computation as in the brute-force method. Once the result is computed, we store it in the memo table before returning. This ensures that each of the O(L^2) possible states is computed only once.
class Solution { int n; int l; String[] strs; Integer[][] memo; public int minDeletionSize(String[] strs) { this.n = strs.length; this.l = strs[0].length(); this.strs = strs; this.memo = new Integer[l + 1][l]; int maxLen = findMaxLen(-1, 0); return l - maxLen; } private int findMaxLen(int prevCol, int currCol) { if (currCol == l) { return 0; } if (memo[prevCol + 1][currCol] != null) { return memo[prevCol + 1][currCol]; } // Option 1: Exclude currCol int len1 = findMaxLen(prevCol, currCol + 1); // Option 2: Include currCol boolean isCompatible = true; if (prevCol != -1) { for (int i = 0; i < n; i++) { if (strs[i].charAt(currCol) < strs[i].charAt(prevCol)) { isCompatible = false; break; } } } int len2 = 0; if (isCompatible) { len2 = 1 + findMaxLen(currCol, currCol + 1); } int result = Math.max(len1, len2); memo[prevCol + 1][currCol] = result; return result; }}Complexity
Time
O(L^2 * N), where L is the length of the strings and N is the number of strings. There are `O(L^2)` states, and each state computation involves a compatibility check taking O(N) time.
Space
O(L^2), where L is the length of the strings. This is for the `(L+1) x L` memoization table. The recursion stack also adds O(L).
Trade-offs
Pros
Significantly more efficient than brute-force.
Guaranteed to solve the problem within the time limits.
The logic closely follows the recursive thought process, which can be intuitive.
Cons
Requires more space than the bottom-up DP approach due to the 2D memoization table.
This is the most efficient approach, utilizing bottom-up dynamic programming. The problem is modeled as finding the Longest Increasing Subsequence (LIS), where the 'elements' are the columns and the 'increasing' condition is that a column can follow another if it maintains the lexicographical order for all rows. This approach iteratively builds the solution using a 1D DP array, offering the best space complexity.
Algorithm
- Rephrase the problem as finding the Longest Increasing Subsequence (LIS) of columns.
- Create a DP array
dpof sizeL, wheredp[i]stores the length of the longest valid subsequence of columns ending at indexi. - Initialize all elements of
dpto 1, as any single column is a valid subsequence of length 1. - Iterate with
ifrom 0 toL-1(for each column).- Inside, iterate with
jfrom 0 toi-1(for each previous column). - Check if column
ican follow columnj. This is true if for all rowsr,strs[r][j] <= strs[r][i]. - If they are compatible, update
dp[i] = max(dp[i], 1 + dp[j]).
- Inside, iterate with
- Keep track of the maximum value found in the
dparray. This will be the maximum number of columns we can keep (maxLen). - The final answer is
L - maxLen.
Walkthrough
The problem of finding the maximum number of columns to keep can be perfectly mapped to the classic Longest Increasing Subsequence (LIS) problem. Here, the sequence is the columns from index 0 to L-1.
We define dp[i] as the length of the longest valid subsequence of columns that ends with column i.
- Initialize a 1D array
dpof sizeL(the number of columns) and fill it with 1s. This is because every column by itself forms a valid subsequence of length 1. - We iterate through the columns from
i = 0toL-1. - For each column
i, we look back at all previous columnsj(where0 <= j < i). - We check if column
ican extend a subsequence that ends atj. The condition for this is that columnimust be 'greater than or equal to' columnj. In this problem's context, this means for every rowk,strs[k].charAt(i) >= strs[k].charAt(j). - If this compatibility condition is met, it means we can append column
ito the subsequence ending atj. The new length would bedp[j] + 1. We want the longest possible subsequence ending ati, so we updatedp[i]using the formula:dp[i] = Math.max(dp[i], 1 + dp[j]). - After the loops complete, the
dparray is filled. The maximum value in this array represents the length of the longest valid subsequence of columns we can keep. - The minimum number of deletions is the total number of columns
Lminus this maximum length.
import java.util.Arrays; class Solution { public int minDeletionSize(String[] strs) { int n = strs.length; int l = strs[0].length(); int[] dp = new int[l]; Arrays.fill(dp, 1); int maxLen = 1; for (int i = 0; i < l; i++) { for (int j = 0; j < i; j++) { boolean isCompatible = true; for (int k = 0; k < n; k++) { if (strs[k].charAt(i) < strs[k].charAt(j)) { isCompatible = false; break; } } if (isCompatible) { dp[i] = Math.max(dp[i], 1 + dp[j]); } } maxLen = Math.max(maxLen, dp[i]); } return l - maxLen; }}Complexity
Time
O(L^2 * N), where L is the length of the strings and N is the number of strings. The nested loops run in `O(L^2)`, and the compatibility check inside takes O(N).
Space
O(L), where L is the length of the strings. This is for the 1D `dp` array.
Trade-offs
Pros
Most space-efficient solution.
Efficient time complexity suitable for the given constraints.
It's an iterative solution, avoiding potential recursion depth issues.
Cons
The LIS-based formulation might be less intuitive to derive compared to a direct recursive approach.
Solutions
Solution
class Solution { public int minDeletionSize ( String [] strs ) { int n = strs [ 0 ]. length (); int [] dp = new int [ n ]; Arrays . fill ( dp , 1 ); int mx = 1 ; for ( int i = 1 ; i < n ; ++ i ) { for ( int j = 0 ; j < i ; ++ j ) { if ( check ( i , j , strs )) { dp [ i ] = Math . max ( dp [ i ], dp [ j ] + 1 ); } } mx = Math . max ( mx , dp [ i ]); } return n - mx ; } private boolean check ( int i , int j , String [] strs ) { for ( String s : strs ) { if ( s . charAt ( i ) < s . charAt ( j )) { return false ; } } return true ; } }class Solution : def minDeletionSize ( self , strs : List [ str ]) -> int : n = len ( strs [ 0 ]) dp = [ 1 ] * n for i in range ( 1 , n ): for j in range ( i ): if all ( s [ j ] <= s [ i ] for s in strs ): dp [ i ] = max ( dp [ i ], dp [ j ] + 1 ) return n - max ( dp )class Solution { public: int minDeletionSize ( vector < string >& strs ) { int n = strs [ 0 ]. size (); vector < int > dp ( n , 1 ); int mx = 1 ; for ( int i = 1 ; i < n ; ++ i ) { for ( int j = 0 ; j < i ; ++ j ) { if ( check ( i , j , strs )) { dp [ i ] = max ( dp [ i ], dp [ j ] + 1 ); } } mx = max ( mx , dp [ i ]); } return n - mx ; } bool check ( int i , int j , vector < string >& strs ) { for ( string & s : strs ) if ( s [ i ] < s [ j ]) return false ; return true ; } };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.