Thursday, January 21, 2021

[LeetCode] Strong Password Checker

Problem: A password is considered strong if the below conditions are all met:

  • It has at least 6 characters and at most 20 characters.
  • 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., "...aaa..." is weak, but "...aa...a..." is strong, assuming other conditions are met).

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 password with another character.

Example:

Input: password = "a"
Output: 5
Explanation: We can insert any 5 characters including at least one uppercase character and at least one digit. We also need to make sure that there are no three repeating characters.


Approach: This problem is very hard to solve. Please have pen and paper with you to run through examples wherever you don't understand the approach. Let's start with the most basic approach to solve this question which is recursive approach and it is the easiest solution to understand. Let's go in depth of this basic solution:

For a recursive solution, First we need to define the method: 

int GetMinSteps(int currIndex, int numOfChartecters, bool hasLowerCase, bool hasUpperCase, bool hasDigit, char lastCharacter, char secondLastCharacter)

  • currIndex: The character of password at index which we are processing 
  • numOfChartecters: Number of characters we have added into the password till now
  • hasLowerCase: Did we get any lowercase character in password[0...currIndex - 1]
  • hasUpperCase: Did we get any uppercase character in password[0...currIndex - 1]
  • hasDigit: Did we get any digit in password[0...currIndex - 1]
  • lastCharacter: Last character processed
  • secondLastCharacter: Second last character processed. 
  • Both lastCharacter and secondLastCharacter are need to check if there is any three repeating characters are there in the password.
  • Return value: Minimum steps required to make a given password a valid password.

Okay. So now we have defined our method, we need to define the base condition here. If you see there are two base conditions

  1. numOfChartecters > 20, number of characters added in the password is more than 20. We need to return from here as we don't want to add more character.
  2. currIndex == length of password: Reached the end of string, nothing to process any more.

Let's see what we want to return in these base conditions:

If ( numOfChartecters > 20) // Invalid password, return some large value

    return int.Max;


if (currIndex == n)

{

    if (numOfChartecters < 6 || !hasLowerCase || !hasUpperCase || !hasDigit) // Invalid password

    {

        return int.Max;

    }

    // Valid password, no changes required

    return 0;

}

So now we are done with our base condition, we can move to our operations:

Let's say minSteps = int.Max;

1. We just add the current character to password if it is not same as last character and second last character. Right!

IF NOT( password[currIndex] == lastCharacter AND password[currIndex] == secondLastCharacter)

    minSteps = Min(minSteps,

                                GetMinSteps(

                                currIndex + 1,  // Next index

                                numOfChartecters + 1, // 1 more character is added

                                hasLowerCase || IsLower(password[currIndex]), // setting lower case

                                hasUpperCase || IsUpper(password[currIndex]), // setting upper case

                                hasDigit || IsDigit(password[currIndex]), // setting digit

                                password[currIndex], // current character becomes last character

                                lastCharacter)) // last character becomes second last character

2. Insert a lowercase character:

        minSteps = Min(minSteps,

                                GetMinSteps(

                                currIndex,  // Added a character, index remains same

                                numOfChartecters + 1, // 1 more character is added

                                True), // added a lower case

                                hasUpperCase, // no change here

                                hasDigit, // no change

                                lastCharacter == 'z' ? 'x' : 'z', // add any char, newly added char becomes last char

                                lastCharacter) + 1) // last character becomes second last character, adding one as we made one change

3. Insert a uppercase character:

        minSteps = Min(minSteps,

                                GetMinSteps(

                                currIndex,  // Added a character, index remains same

                                numOfChartecters + 1, // 1 more character is added

                                hasLowerCase, // no change here

                                True, // added a upper case

                                hasDigit, // no change

                                lastCharacter == 'Z' ? 'X' : 'X', // add any char, newly added char becomes last char

                                lastCharacter) + 1) // last character becomes second last character, adding one as we made one change

4. Insert a digit:

        minSteps = Min(minSteps,

                                GetMinSteps(

                                currIndex,  // Added a character, index remains same

                                numOfChartecters + 1, // 1 more character is added

                                hasLowerCase, // no change here

                                hasUpperCase , // no change here

                                True, // added a digit

                                lastCharacter == '9' ? '8' : '9', // add any digit, newly added char becomes last char

                                lastCharacter) + 1) // last character becomes second last character, adding one as we made one change

5. Delete current character:

         minSteps = Min(minSteps,

                                GetMinSteps(

                                currIndex + 1,  // Remove a character, go to next index

                                numOfChartecters , // no character has been added

                                hasLowerCase, // no change here

                                hasUpperCase , // no change here

                                hasDigit, // no change here

                                lastCharacter, // Remains same

                                secondLastCharacter) + 1) // Second last character remains same, adding one as we made one change

6. Change to a lowercase character:

        minSteps = Min(minSteps,

                                GetMinSteps(

                                currIndex + 1,  // Changed a character, move to next 

                                numOfChartecters + 1, // 1 more character is added

                                True), // changed to a lower case

                                hasUpperCase, // no change here

                                hasDigit, // no change

                                lastCharacter == 'z' ? 'x' : 'z', // change to any char, newly changed char becomes last char

                                lastCharacter) + 1) // last character becomes second last character, adding one as we made one change

7. Change to a uppercase character:

        minSteps = Min(minSteps,

                                GetMinSteps(

                                currIndex + 1,  // Changed a character, move to next index

                                numOfChartecters + 1, // 1 more character is added

                                hasLowerCase, // no change here

                                True, // changed to a upper case

                                hasDigit, // no change

                                lastCharacter == 'Z' ? 'X' : 'X', // changed to any char, newly changed char becomes last char

                                lastCharacter) + 1) // last character becomes second last character, adding one as we made one change

8. Change to a digit:

        minSteps = Min(minSteps,

                                GetMinSteps(

                                currIndex,  // change a character, move to next index

                                numOfChartecters + 1, // 1 more character is added

                                hasLowerCase, // no change here

                                hasUpperCase , // no change here

                                True, // added a digit

                                lastCharacter == '9' ? '8' : '9', // add any digit, newly added char becomes last char

                                lastCharacter) + 1) // last character becomes second last character, adding one as we made one change

After applying all these operations, we get the minimum steps required to make a valid password. We can just return minSteps now.

If you see this will work and solve our problem but the time complexity will be very very high. We can reduce it by using DP. If you see we are getting recursive calls for same parameters multiple times. We can store the result in the table and return from the table if we see the function call with the same parameters. You can see the implementation to understand how to apply the DP here.

After applying the DP, definitely we see improvements significantly but still we are checking every case here and still it is very expensive. Let's approach to our most optimal solution which is even  harder to understand. To be honest I had to run through many examples to reach to this solution and still its a greedy intuition, not a proper formula.

Let's define some variables:

numMissingLetterTypes: Count of missing types: Uppercase, Lowercase, Digit 

Let's see condition by condition:

1. length < 6:
    
    Max (numMissingLetterTypes, 6 - n)
    
    We need to add at least 6 - n characters right, but say if a uppercase character is missing and a digit is missing then we need to add uppercase and digit. Say if the length is 4, then we can just add these 2 missing types characters but if length is 5 then we need to say add one uppercase and change one lowercase to digit. You can run more examples and you will found that the changes required will be max of these two.

2. 6 <= length <= 20:
        
     Look at "aaa", we know that this is invalid. Here we need to make one character change. Right, in the same way in case of "aaaaa", we just need one change of character that is the middle character to make it valid right. For example "aabaa" is valid. With this intuition, if you see the number of changes required are floor of (Length of repeating characters / 3) in case of repeating characters. You can run more examples to see it. So in that way lets say we calculated the  "numCharChangesReqd". 

    You can also observe that we don't need to remove any character as the length of password is within the limits so in this case our answer will be:

    Max(numCharChangesReqd, numMissingLetterTypes)

    Hopefully you are with me till now, as condition 3 is the most complex one.

3. length > 20:

Here let's look at "aaaaaa", here the numCharChangesReqd are (6/3 =) 2 but if you see we remove one character then the string will become "aaaaa" and numCharChangesReqd is reduced to 1. 

Similarly in case of "aaaaaaa",  the numCharChangesReqd are (7/3 =) 2 but if we remove 2 characters then the string will become "aaaaa" and numCharChangesReqd is reduced to 1.

Obviously in case of "aaaaaaaa", if we remove3 characters, numCharChangesReqd will become 1 instead of 2. 

So in this case (length > 20), let's see how to calculate the minimum steps:

numOfDeletes = length - 20

For sure we need to delete these many characters, but our task is not done yet as even after deleting these many characters, may be we need to make more changes. Right! Say digit is not there or there are repeating characters of length more than or equal to 3 so our answer is:

numOfDeletes + {number_of_changes} 

numOfDeletes, we know but we need to figure out number_of_changes. Let's say we got numCharChangesReqd  after preprocessing the string. 

Let's define some new variables:

numRemoveOneCharReqd = number of cases like "aaaaaa" where removing one character can reduce numCharChangesReqd by 1.

numRemoveTwoCharsReqd = number of cases like "aaaaaaa" where removing two character can reduce numCharChangesReqd by 1.

numCharChangesReqd  = numCharChangesReqd  - Min(numOfDeletes, numRemoveOneCharReqd)

If you see numRemoveOneCharReqd is the number of cases of removing one character and because of it numCharChangesReqd is getting reduce by one (we have already seen) but we also don't want to remove characters more than numOfDeletes as it will just add more operations so we reduce the numCharChangesReqd  by the minimum of theses two value.

numCharChangesReqd  = numCharChangesReqd  - Min(Max(numOfDeletes - numRemoveOneCharReqd, 0), numRemoveTwoCharsReqd) / 2

So after the first step we have numOfDeletes - numRemoveOneCharReqd characters to remove (limiting by 0), if we remove 2 characters numRemoveTwoCharsReqd times we are reducing numCharChangesReqd by 1 every time.

numCharChangesReqd  = numCharChangesReqd  - Min(Max(numOfDeletes - numRemoveOneCharReqd - numRemoveTwoCharsReqd * 2, 0), numRemoveTwoCharsReqd) / 3

After second step we got numOfDeletes - numRemoveOneCharReqd - numRemoveTwoCharsReqd * 2 characters to delete. The above case is when we are removing 3 characters to reduce numCharChangesReqd  by 1.

With the above knowledge, we can say  number_of_changes = Max(numCharChangesReqd, numMissingLetterTypes)

So our final answer in this case is:

numOfDeletes + Max(numCharChangesReqd, numMissingLetterTypes)


Implementation in C#:

DP solution:

        public static int StrongPasswordCheckerDP(string password)

        {

            if (password?.Length == 0)

            {

                return 6;

            }

            Dictionary<string, long> dp = new Dictionary<string, long>();

            return (int)GetMinSteps(password, 0, 0, false, false, false, Char.MinValue, Char.MinValue, dp);

        }


        private static long GetMinSteps(

            string password, 

            int currIndex, 

            int numCharacters,

            bool hasLowerCase,

            bool hasUpperCase,

            bool hasDigit,

            char lastCharacter,

            char secondLastCharacter,

            Dictionary<string, long> dp)

        {

            string key = $"{currIndex}-{numCharacters}-{hasLowerCase}-{hasUpperCase}-{hasDigit}-{lastCharacter}-{secondLastCharacter}";

            if (dp.ContainsKey(key))

            {

                return dp[key];

            }

            if (numCharacters > 20)

            {

                dp.Add(key, int.MaxValue);

                return int.MaxValue;

            }

            if (currIndex == password.Length)

            {

                if (numCharacters < 6 || !hasLowerCase || !hasUpperCase || !hasDigit)

                {

                    dp.Add(key, int.MaxValue);

                    return int.MaxValue;

                }

                dp.Add(key, 0);

                return 0;

            }

            long minSteps = int.MaxValue;

            // Add current character

            if (!(password[currIndex] == lastCharacter && password[currIndex] == secondLastCharacter))

            {

                minSteps = Math.Min(

                    minSteps,

                    GetMinSteps(

                        password,

                        currIndex + 1,

                        numCharacters + 1,

                        hasLowerCase || char.IsLower(password[currIndex]),

                        hasUpperCase || char.IsUpper(password[currIndex]),

                        hasDigit || char.IsDigit(password[currIndex]),

                        password[currIndex],

                        lastCharacter,

                        dp));

            }

            // Insert a lower case character

            minSteps = Math.Min(

                    minSteps,

                    GetMinSteps(

                        password,

                        currIndex,

                        numCharacters + 1,

                        true,

                        hasUpperCase,

                        hasDigit,

                        lastCharacter == 'z' ? 'x' : 'z',

                        lastCharacter,

                        dp) + 1);

            // Insert a upper case character

            minSteps = Math.Min(

                    minSteps,

                    GetMinSteps(

                        password,

                        currIndex,

                        numCharacters + 1,

                        hasLowerCase,

                        true,

                        hasDigit,

                        lastCharacter == 'Z' ? 'X' : 'Z',

                        lastCharacter,

                        dp) + 1);

            // Insert a digit

            minSteps = Math.Min(

                    minSteps,

                    GetMinSteps(

                        password,

                        currIndex,

                        numCharacters + 1,

                        hasLowerCase,

                        hasUpperCase,

                        true,

                        lastCharacter == '9' ? '2' : '9',

                        lastCharacter, 

                        dp) + 1);

            // delete this character

            minSteps = Math.Min(

                    minSteps,

                    GetMinSteps(

                        password,

                        currIndex + 1,

                        numCharacters,

                        hasLowerCase,

                        hasUpperCase,

                        hasDigit,

                        lastCharacter,

                        secondLastCharacter,

                        dp) + 1);

            // Change a lower case character

            minSteps = Math.Min(

                    minSteps,

                    GetMinSteps(

                        password,

                        currIndex + 1,

                        numCharacters + 1,

                        true,

                        hasUpperCase,

                        hasDigit,

                        lastCharacter == 'z' ? 'x' : 'z',

                        lastCharacter,

                        dp) + 1);

            // change a upper case character

            minSteps = Math.Min(

                    minSteps,

                    GetMinSteps(

                        password,

                        currIndex + 1,

                        numCharacters + 1,

                        hasLowerCase,

                        true,

                        hasDigit,

                        lastCharacter == 'Z' ? 'X' : 'Z',

                        lastCharacter,

                        dp) + 1);

            // Change a digit

            minSteps = Math.Min(

                    minSteps,

                    GetMinSteps(

                        password,

                        currIndex + 1,

                        numCharacters + 1,

                        hasLowerCase,

                        hasUpperCase,

                        true,

                        lastCharacter == '9' ? '2' : '9',

                        lastCharacter,

                        dp) + 1);

            dp.Add(key, minSteps);

            return minSteps;

        }


Greedy Solution:

        public static int StrongPasswordChecker(string password)

        {

            if (password?.Length == 0)

            {

                return 6;

            }

            int numMissingLetterTypes = 3;

            for (int i = 0; i < password.Length; ++i)

            {

                if (char.IsLower(password[i]))

                {

                    --numMissingLetterTypes;

                    break;

                }

            }

            for (int i = 0; i < password.Length; ++i)

            {

                if (char.IsUpper(password[i]))

                {

                    --numMissingLetterTypes;

                    break;

                }

            }

            for (int i = 0; i < password.Length; ++i)

            {

                if (char.IsDigit(password[i]))

                {

                    --numMissingLetterTypes;

                    break;

                }

            }

            int numChangesReqd = 0;

            int numRemoveOneCharReqd = 0;

            int numRemoveTwoCharsReqd = 0;

            int index = 2;

            int currLengthOfRepeatingChars;

            while (index < password.Length)

            {

                if (password[index] == password[index - 1] && password[index] == password[index - 2])

                {

                    currLengthOfRepeatingChars = 2;

                    while (index < password.Length && password[index] == password[index - 1])

                    {

                        ++index;

                        ++currLengthOfRepeatingChars;

                    }

                    numChangesReqd += currLengthOfRepeatingChars / 3;

                    numRemoveOneCharReqd = currLengthOfRepeatingChars % 3 == 0 ? numRemoveOneCharReqd + 1 : numRemoveOneCharReqd;

                    numRemoveTwoCharsReqd = currLengthOfRepeatingChars % 3 == 1 ? numRemoveTwoCharsReqd + 1 : numRemoveTwoCharsReqd;

                }

                else

                {

                    ++index;

                }

            }

            if (password.Length < 6)

            {

                return Math.Max(numMissingLetterTypes, 6 - password.Length);

            }

            else if (password.Length <= 20)

            {

                return Math.Max(numMissingLetterTypes, numChangesReqd);

            }

            else

            {

                int numOfDeletes = password.Length - 20;

                numChangesReqd -= Math.Min(numOfDeletes, numRemoveOneCharReqd);

                numChangesReqd -= Math.Min(Math.Max(numOfDeletes - numRemoveOneCharReqd, 0), 2 * numRemoveTwoCharsReqd) / 2;

                numChangesReqd -= Math.Max(numOfDeletes - numRemoveOneCharReqd - numRemoveTwoCharsReqd * 2, 0) / 3;

                return numOfDeletes + Math.Max(numChangesReqd, numMissingLetterTypes);

            }

        }


Complexity: O(n) for greedy solution

No comments:

Post a Comment