Strong Password Checker
HardPrompt
A password is considered strong if the below conditions are all met:
- It has at least
6characters and at most20characters. - It contains at least one lowercase letter, at least one uppercase letter, and at least one digit.
- It does not contain three repeating characters in a row (i.e.,
"Baaabb0"is weak, but"Baaba0"is strong).
Given a string password, return the minimum number of steps required to make password strong. if password is already strong, return 0.
In one step, you can:
- Insert one character to
password, - Delete one character from
password, or - Replace one character of
passwordwith another character.
Example 1:
Input: password = "a"
Output: 5Example 2:
Input: password = "aA1"
Output: 3Example 3:
Input: password = "1337C0d3"
Output: 0
Constraints:
1 <= password.length <= 50passwordconsists of letters, digits, dot'.'or exclamation mark'!'.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach treats the problem as a shortest path search in a vast state space of possible passwords. Starting from the given password, it explores all possible modifications (insert, delete, replace) layer by layer using a Breadth-First Search (BFS). The first time a strong password is found, the algorithm terminates, guaranteeing that the number of steps taken is the minimum possible. However, the number of possible passwords to check grows exponentially with each step, making this method computationally impractical.
Algorithm
- Model the problem as a shortest path problem on a graph where nodes are password strings and edges represent one operation (insert, delete, replace).
- Use Breadth-First Search (BFS) to find the shortest path from the initial password to any strong password.
- Initialize a queue with the starting password and a
visitedset to avoid cycles. - In each level of the BFS, explore all possible passwords that can be generated by one operation from the passwords in the current level.
- For each password dequeued, check if it meets the strong password criteria.
- If a strong password is found, the current level number (number of steps) is the minimum required, so return it.
- If the password is not strong, generate all valid next states (by inserting, deleting, or replacing a character) and add them to the queue if they haven't been visited.
Walkthrough
The core idea is to build a state graph where each node is a unique password string. An edge connects two nodes if one password can be transformed into the other via a single operation (insertion, deletion, or replacement). The problem then becomes finding the shortest path from the initial password node to any node representing a strong password.
BFS is the natural algorithm for this, as it explores the graph level by level, guaranteeing that the first time we reach a goal state, it will be via a shortest path.
We start with a queue containing the initial password. We also use a Set to keep track of visited passwords to prevent redundant computations and infinite loops. The search proceeds in levels, where each level corresponds to one additional operation. For every password processed, we check if it's strong. If not, we generate all possible new passwords by applying one of the three operations at every possible position. These new, unvisited passwords are then added to the queue for the next level of the search.
// Conceptual code for BFS approach// Note: This is not a practical solution due to performance issues.class Solution { public int strongPasswordChecker(String password) { Queue<String> queue = new LinkedList<>(); Set<String> visited = new HashSet<>(); queue.offer(password); visited.add(password); int steps = 0; while (!queue.isEmpty()) { int levelSize = queue.size(); for (int i = 0; i < levelSize; i++) { String current = queue.poll(); if (isStrong(current)) { return steps; } // Generate all next possible passwords via one operation // (insert, delete, replace) and add to queue if not visited. } steps++; } return -1; // Should not be reached } private boolean isStrong(String s) { if (s.length() < 6 || s.length() > 20) return false; boolean hasLower = false, hasUpper = false, hasDigit = false; for (char c : s.toCharArray()) { if (Character.isLowerCase(c)) hasLower = true; if (Character.isUpperCase(c)) hasUpper = true; if (Character.isDigit(c)) hasDigit = true; } if (!hasLower || !hasUpper || !hasDigit) return false; for (int i = 0; i <= s.length() - 3; i++) { if (s.charAt(i) == s.charAt(i + 1) && s.charAt(i + 1) == s.charAt(i + 2)) { return false; } } return true; }}Complexity
Time
Exponential, O(B^D). The branching factor (number of possible next states) is very large, making the search explode in complexity very quickly.
Space
Exponential, O(B^D), where B is the branching factor and D is the solution depth. The `visited` set and queue can grow to an unmanageable size.
Trade-offs
Pros
Guaranteed to find the absolute minimum number of steps.
Conceptually simple and straightforward to understand.
Cons
Extremely high time complexity, making it infeasible for the given constraints.
Requires a massive amount of memory to store the
visitedset and the queue, leading to potential memory overflow.
This is a highly efficient approach that solves the problem in a single pass by applying a greedy strategy. It first analyzes the password to determine all its deficiencies: length issues, missing character types, and consecutive repeating characters. Based on the password's length, it enters one of three logical paths (n < 6, 6 <= n <= 20, or n > 20) and calculates the minimum changes by cleverly resolving the conflicts and overlaps between different types of required fixes.
Algorithm
- First, perform a single pass over the password to gather key metrics:
n: the length of the password.missing_types: the number of missing character types (lowercase, uppercase, digit), from 0 to 3.repeating_changes: the number of replacements needed to fix all repeating sequences. A sequence of lengthLneedsL/3replacements.- For the
n > 20case, also count how many repeating sequences have lengthsLsuch thatL % 3 == 0,L % 3 == 1, andL % 3 == 2.
- Handle the problem in three distinct cases based on the password length
n: - Case 1:
n < 6- The password is too short. We need at least
6 - ninsertions. These insertions can also fix missing character types. The total steps will be the maximum of the changes needed for length and for character types. - Return
max(missing_types, 6 - n).
- The password is too short. We need at least
- Case 2:
6 <= n <= 20- The password length is acceptable. We only need to fix missing types and repeating characters using replacements. A single replacement can address both issues simultaneously.
- Return
max(missing_types, repeating_changes).
- Case 3:
n > 20- The password is too long. We must perform
deletions_needed = n - 20deletions. - Greedily use these deletions to reduce
repeating_changesas efficiently as possible:- First, use 1 deletion on sequences where
len % 3 == 0(saves 1 replacement). - Second, use 2 deletions on sequences where
len % 3 == 1(saves 1 replacement). - Finally, use any remaining 3 deletions on any repeating sequence (saves 1 replacement).
- First, use 1 deletion on sequences where
- After deletions, the remaining changes are
max(missing_types, remaining_repeating_changes). - Return
deletions_needed + max(missing_types, remaining_repeating_changes).
- The password is too long. We must perform
Walkthrough
The algorithm's strength lies in its ability to correctly determine the minimum number of operations by understanding how they interact. Instead of exploring a vast search space, it directly calculates the result.
Initial Analysis: First, we iterate through the password to find:
missing_types: The number of required character types (lowercase, uppercase, digit) that are not present.repeating_changes: The number of replacements needed to break up sequences of three or more identical characters. For each such sequence of lengthL, we needL/3replacements. We also track the counts of these sequences based on their length modulo 3, which is crucial for then > 20case.
Case-based Logic:
-
n < 6: The primary issue is length. We need6 - ninsertions. These insertions can simultaneously satisfy themissing_typesrequirement. Thus, the total operations needed is the maximum of these two values, as one operation can serve two purposes. Repeating sequences are not a concern as they will be broken by the necessary insertions/replacements. -
6 <= n <= 20: Length is not an issue. We only need to use replacements to fixmissing_typesandrepeating_changes. Since one replacement can fix a repeating character and introduce a missing type, the total operations are the maximum of the two counts. -
n > 20: This is the most complex case. We must performn - 20deletions. The greedy strategy is to use these deletions to fix the most 'expensive' repeating sequences first. A deletion is most valuable when it reduces the number of required replacements. The priority is:- Delete 1 character from a sequence of length
LwhereL % 3 == 0. This saves one replacement. - Delete 2 characters from a sequence of length
LwhereL % 3 == 1. This also saves one replacement. - Any remaining deletions are used in groups of 3 to reduce the total length, which effectively saves one replacement per 3 deletions.
After applying deletions, the final number of steps is the
n - 20deletions plus the number of replacements needed for any remaining issues, which ismax(missing_types, remaining_repeating_changes).
- Delete 1 character from a sequence of length
class Solution { public int strongPasswordChecker(String password) { int n = password.length(); int missing_types = 3; if (password.chars().anyMatch(Character::isLowerCase)) missing_types--; if (password.chars().anyMatch(Character::isUpperCase)) missing_types--; if (password.chars().anyMatch(Character::isDigit)) missing_types--; int repeating_changes = 0; // repeats_mod[i] will store the count of repeating sequences with length % 3 == i int[] repeats_mod = new int[3]; for (int i = 0; i < n; ) { char c = password.charAt(i); int j = i; while (j < n && password.charAt(j) == c) { j++; } int len = j - i; if (len >= 3) { repeating_changes += len / 3; repeats_mod[len % 3]++; } i = j; } if (n < 6) { return Math.max(missing_types, 6 - n); } else if (n <= 20) { return Math.max(missing_types, repeating_changes); } else { // n > 20 int deletions_needed = n - 20; int total_deletions = deletions_needed; // Use 1 deletion to fix a L%3==0 sequence (saves 1 replacement) int use_del = Math.min(deletions_needed, repeats_mod[0]); deletions_needed -= use_del; repeating_changes -= use_del; // Use 2 deletions to fix a L%3==1 sequence (saves 1 replacement) use_del = Math.min(deletions_needed / 2, repeats_mod[1]); deletions_needed -= use_del * 2; repeating_changes -= use_del; // Use 3 deletions to fix any sequence (saves 1 replacement) use_del = Math.min(deletions_needed / 3, repeating_changes); repeating_changes -= use_del; return total_deletions + Math.max(missing_types, repeating_changes); } }}Complexity
Time
O(N), where N is the length of the password. The algorithm involves a single pass to gather statistics, followed by constant-time calculations.
Space
O(1), as we only use a few variables and a constant-size array (`repeats_mod`) to store the state, regardless of the input password's length.
Trade-offs
Pros
Extremely efficient with linear time complexity.
Requires constant extra space, making it suitable for large inputs.
Provides the optimal solution by correctly applying a greedy strategy.
Cons
The logic is complex, particularly for the
n > 20case, and requires careful handling of edge cases.It can be difficult to prove the greedy choices are optimal without careful case analysis.
Solutions
Solution
class Solution {public int strongPasswordChecker(String password) { int types = countTypes(password); int n = password.length(); if (n < 6) { return Math.max(6 - n, 3 - types); } char[] chars = password.toCharArray(); if (n <= 20) { int replace = 0; int cnt = 0; char prev = '~'; for (char curr : chars) { if (curr == prev) { ++cnt; } else { replace += cnt / 3; cnt = 1; prev = curr; } } replace += cnt / 3; return Math.max(replace, 3 - types); } int replace = 0, remove = n - 20; int remove2 = 0; int cnt = 0; char prev = '~'; for (char curr : chars) { if (curr == prev) { ++cnt; } else { if (remove > 0 && cnt >= 3) { if (cnt % 3 == 0) { --remove; --replace; } else if (cnt % 3 == 1) { ++remove2; } } replace += cnt / 3; cnt = 1; prev = curr; } } if (remove > 0 && cnt >= 3) { if (cnt % 3 == 0) { --remove; --replace; } else if (cnt % 3 == 1) { ++remove2; } } replace += cnt / 3; int use2 = Math.min(Math.min(replace, remove2), remove / 2); replace -= use2; remove -= use2 * 2; int use3 = Math.min(replace, remove / 3); replace -= use3; remove -= use3 * 3; return (n - 20) + Math.max(replace, 3 - types); }private int countTypes(String s) { int a = 0, b = 0, c = 0; for (char ch : s.toCharArray()) { if (Character.isLowerCase(ch)) { a = 1; } else if (Character.isUpperCase(ch)) { b = 1; } else if (Character.isDigit(ch)) { c = 1; } } return a + b + c; }}class Solution {public: int strongPasswordChecker(string password) { int types = countTypes(password); int n = password.size(); if (n < 6) return max(6 - n, 3 - types); if (n <= 20) { int replace = 0, cnt = 0; char prev = '~'; for (char &curr : password) { if (curr == prev) ++cnt; else { replace += cnt / 3; cnt = 1; prev = curr; } } replace += cnt / 3; return max(replace, 3 - types); } int replace = 0, remove = n - 20; int remove2 = 0; int cnt = 0; char prev = '~'; for (char &curr : password) { if (curr == prev) ++cnt; else { if (remove > 0 && cnt >= 3) { if (cnt % 3 == 0) { --remove; --replace; } else if (cnt % 3 == 1) ++remove2; } replace += cnt / 3; cnt = 1; prev = curr; } } if (remove > 0 && cnt >= 3) { if (cnt % 3 == 0) { --remove; --replace; } else if (cnt % 3 == 1) ++remove2; } replace += cnt / 3; int use2 = min(min(replace, remove2), remove / 2); replace -= use2; remove -= use2 * 2; int use3 = min(replace, remove / 3); replace -= use3; remove -= use3 * 3; return (n - 20) + max(replace, 3 - types); } int countTypes(string &s) { int a = 0, b = 0, c = 0; for (char &ch : s) { if (islower(ch)) a = 1; else if (isupper(ch)) b = 1; else if (isdigit(ch)) c = 1; } return a + b + c; }};class Solution: def strongPasswordChecker(self, password: str) -> int: def countTypes(s): a = b = c = 0 for ch in s: if ch . islower(): a = 1 elif ch . isupper(): b = 1 elif ch . isdigit(): c = 1 return a + b + c types = countTypes(password) n = len(password) if n < 6: return max(6 - n, 3 - types) if n <= 20: replace = cnt = 0 prev = '~' for curr in password: if curr == prev: cnt += 1 else: replace += cnt // 3 cnt = 1 prev = curr replace += cnt // 3 return max(replace, 3 - types) replace = cnt = 0 remove, remove2 = n - 20, 0 prev = '~' for curr in password: if curr == prev: cnt += 1 else: if remove > 0 and cnt >= 3: if cnt % 3 == 0: remove -= 1 replace -= 1 elif cnt % 3 == 1: remove2 += 1 replace += cnt // 3 cnt = 1 prev = curr if remove > 0 and cnt >= 3: if cnt % 3 == 0: remove -= 1 replace -= 1 elif cnt % 3 == 1: remove2 += 1 replace += cnt // 3 use2 = min(replace, remove2, remove // 2) replace -= use2 remove -= use2 * 2 use3 = min(replace, remove // 3) replace -= use3 remove -= use3 * 3 return n - 20 + max(replace, 3 - types)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.