Facebook Pixel

2825. Make String a Subsequence Using Cyclic Increments

Problem Description

You are given two strings str1 and str2, both 0-indexed.

In one operation, you can select any set of indices in str1 and increment each character at those indices to the next character in the alphabet cyclically. This means:

  • 'a' becomes 'b'
  • 'b' becomes 'c'
  • ... and so on
  • 'z' becomes 'a' (wraps around)

Your task is to determine if you can make str2 a subsequence of str1 by performing at most one such operation.

A subsequence is a sequence that can be derived from another sequence by deleting some or no elements without changing the order of the remaining elements. For example, "ace" is a subsequence of "abcde".

The key insight is that you can only perform the increment operation once, but when you do, you can choose to increment any subset of characters in str1. After the operation (or without using it at all), str2 must be a subsequence of the resulting str1.

For example:

  • If str1 = "abc" and str2 = "ad", you can increment 'c' to 'd', making str1 = "abd". Now "ad" is a subsequence of "abd", so return true.
  • If str1 = "zc" and str2 = "ad", you can increment 'z' to 'a' and 'c' to 'd', making str1 = "ad". Now "ad" equals "ad", so return true.

The solution uses a two-pointer approach where for each character in str1, we check if it can match the current character we need from str2 - either directly or after incrementing it by one position cyclically.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key realization is that we need to check if str2 can be a subsequence of str1 after potentially modifying some characters in str1. Since we can only perform the operation once, each character in str1 has only two possible states: its original form or its incremented form.

Think about how we normally check if one string is a subsequence of another - we use two pointers, one for each string, and try to match characters. When we find a match, we advance the pointer in the subsequence. If we can match all characters in the subsequence, it's valid.

This problem is similar, but with a twist: when trying to match a character from str2 with a character from str1, we have two options:

  1. The character matches directly (no operation needed)
  2. The character would match if we increment the str1 character by one position cyclically

The beauty of this problem is that we don't need to explicitly decide which characters to increment beforehand. Instead, we can make this decision greedily as we traverse str1. For each character in str1, we check if it can help us match the current character we need from str2 - either in its current form or after incrementing.

Why does this greedy approach work? Because:

  • We're looking for a subsequence, so we can skip characters in str1 that don't help
  • Each character position is independent - incrementing one character doesn't affect our ability to increment another
  • We want to match characters in order, so as soon as we find a potential match (either direct or with increment), we should take it

This leads us to a simple two-pointer solution: iterate through str1, and for each character, check if it matches the current needed character from str2 either directly or after a cyclic increment. If yes, move forward in str2. If we match all of str2, return true.

Learn more about Two Pointers patterns.

Solution Approach

The solution uses a two-pointer technique to efficiently check if str2 can be a subsequence of str1 after the allowed operation.

Algorithm Steps:

  1. Initialize a pointer i = 0 to track our current position in str2.

  2. Iterate through each character c in str1:

    • Calculate what c would become if incremented:
      • If c == 'z', it becomes 'a' (cyclic wrap)
      • Otherwise, it becomes chr(ord(c) + 1) (next character in alphabet)
    • Store this next character as d
  3. Check for matching:

    • If we haven't finished matching str2 yet (i < len(str2)), check if the current character we need from str2[i] matches either:
      • The original character c from str1, OR
      • The incremented version d
    • If either matches, increment i to move to the next character in str2
  4. Return the result:

    • After processing all characters in str1, check if i == len(str2)
    • If true, we successfully matched all characters of str2, making it a subsequence
    • If false, we couldn't match all characters

Why this works:

  • The algorithm greedily matches characters whenever possible
  • For each position in str1, we implicitly decide whether to use the operation (increment) or not based on what helps us match str2[i]
  • We don't need to track which characters we've incremented because each decision is independent
  • The in (c, d) check elegantly handles both possibilities in one comparison

Time Complexity: O(n) where n is the length of str1, as we traverse it once.

Space Complexity: O(1) as we only use a constant amount of extra space for variables.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through an example with str1 = "abc" and str2 = "ad".

Initial Setup:

  • str1 = "abc", str2 = "ad"
  • Pointer i = 0 (pointing to 'a' in str2)

Step 1: Process str1[0] = 'a'

  • Current character: c = 'a'
  • If incremented: d = 'b' (since 'a' + 1 = 'b')
  • Need to match: str2[0] = 'a'
  • Check: Is 'a' in ('a', 'b')? Yes! (matches original 'a')
  • Action: Increment i to 1
  • Status: Matched first character of str2 without using operation

Step 2: Process str1[1] = 'b'

  • Current character: c = 'b'
  • If incremented: d = 'c' (since 'b' + 1 = 'c')
  • Need to match: str2[1] = 'd'
  • Check: Is 'd' in ('b', 'c')? No
  • Action: Don't increment i, stays at 1
  • Status: Skip this character, still looking for 'd'

Step 3: Process str1[2] = 'c'

  • Current character: c = 'c'
  • If incremented: d = 'd' (since 'c' + 1 = 'd')
  • Need to match: str2[1] = 'd'
  • Check: Is 'd' in ('c', 'd')? Yes! (matches incremented 'd')
  • Action: Increment i to 2
  • Status: Matched second character of str2 using the operation

Final Check:

  • i = 2 which equals len(str2) = 2
  • All characters matched! Return true

The algorithm found that by incrementing 'c' to 'd' in str1, we can make str2 = "ad" a subsequence of the modified str1 = "abd".

Solution Implementation

1class Solution:
2    def canMakeSubsequence(self, str1: str, str2: str) -> bool:
3        # Pointer to track current position in str2
4        str2_index = 0
5      
6        # Iterate through each character in str1
7        for char in str1:
8            # Calculate the next character in cyclic order (z -> a)
9            next_char = "a" if char == "z" else chr(ord(char) + 1)
10          
11            # Check if we haven't exhausted str2 and current position in str2
12            # matches either the current character or its cyclic successor
13            if str2_index < len(str2) and str2[str2_index] in (char, next_char):
14                # Move to next character in str2
15                str2_index += 1
16      
17        # Return True if we matched all characters in str2
18        return str2_index == len(str2)
19
1class Solution {
2    /**
3     * Determines if str2 can be made as a subsequence of str1 where each character
4     * in str1 can optionally be incremented by 1 (cyclically, 'z' becomes 'a').
5     * 
6     * @param str1 The source string to check against
7     * @param str2 The target subsequence to find
8     * @return true if str2 can be formed as a subsequence, false otherwise
9     */
10    public boolean canMakeSubsequence(String str1, String str2) {
11        // Index pointer for str2
12        int str2Index = 0;
13        // Length of the target subsequence
14        int str2Length = str2.length();
15      
16        // Iterate through each character in str1
17        for (char currentChar : str1.toCharArray()) {
18            // Calculate the next character cyclically ('z' wraps to 'a')
19            char nextChar = (currentChar == 'z') ? 'a' : (char)(currentChar + 1);
20          
21            // Check if we haven't matched all of str2 yet AND
22            // the current position in str2 matches either:
23            // 1. The current character from str1, OR
24            // 2. The incremented version of the current character
25            if (str2Index < str2Length && 
26                (str2.charAt(str2Index) == currentChar || str2.charAt(str2Index) == nextChar)) {
27                // Move to the next character in str2
28                str2Index++;
29            }
30        }
31      
32        // Return true if we've matched all characters in str2
33        return str2Index == str2Length;
34    }
35}
36
1class Solution {
2public:
3    bool canMakeSubsequence(string str1, string str2) {
4        // Initialize pointer for str2 and get its length
5        int str2Index = 0;
6        int str2Length = str2.size();
7      
8        // Iterate through each character in str1
9        for (char currentChar : str1) {
10            // Calculate the next character in cyclic order (z -> a)
11            char nextChar = (currentChar == 'z') ? 'a' : (currentChar + 1);
12          
13            // Check if current position in str2 matches either:
14            // 1. The current character from str1, or
15            // 2. The next character in cyclic order
16            if (str2Index < str2Length && 
17                (str2[str2Index] == currentChar || str2[str2Index] == nextChar)) {
18                // Move to next character in str2 if match found
19                ++str2Index;
20            }
21        }
22      
23        // Return true if all characters in str2 have been matched
24        return str2Index == str2Length;
25    }
26};
27
1/**
2 * Determines if str2 can be made a subsequence of str1 by incrementing 
3 * at most one character in str1 (where 'z' wraps to 'a')
4 * @param str1 - The source string to check against
5 * @param str2 - The target string to verify as a potential subsequence
6 * @returns true if str2 can be a subsequence of str1 with the increment rule, false otherwise
7 */
8function canMakeSubsequence(str1: string, str2: string): boolean {
9    // Pointer to track current position in str2
10    let str2Index: number = 0;
11  
12    // Length of the target string
13    const str2Length: number = str2.length;
14  
15    // Iterate through each character in str1
16    for (const currentChar of str1) {
17        // Calculate the next character in the alphabet (with wraparound from 'z' to 'a')
18        const nextChar: string = currentChar === 'z' 
19            ? 'a' 
20            : String.fromCharCode(currentChar.charCodeAt(0) + 1);
21      
22        // Check if current position in str2 matches either:
23        // 1. The current character from str1
24        // 2. The next character (incremented version) from str1
25        if (str2Index < str2Length && 
26            (str2[str2Index] === currentChar || str2[str2Index] === nextChar)) {
27            // Move to the next character in str2
28            str2Index++;
29        }
30    }
31  
32    // Return true if all characters in str2 have been matched
33    return str2Index === str2Length;
34}
35

Time and Space Complexity

The time complexity is O(m), where m is the length of string str1. The algorithm iterates through each character in str1 exactly once using a single for loop. For each character, it performs constant time operations: calculating the next character (either wrapping from 'z' to 'a' or incrementing), comparing with str2[i], and potentially incrementing the index i. Even though we check characters from str2, we never iterate through str2 separately - we only access at most n characters from str2 (where n is the length of str2) during our single pass through str1.

The space complexity is O(1). The algorithm only uses a constant amount of extra space: the index variable i, the loop variable c, and the temporary variable d for storing the next character. No additional data structures that grow with input size are created.

Note: The reference answer states O(m + n) for time complexity, but this appears to be imprecise. Since we iterate through str1 once and only access elements of str2 during that iteration (without a separate traversal of str2), the time complexity is more accurately O(m) where m β‰₯ n for a valid subsequence to exist.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Misunderstanding the Operation Constraint

The Mistake: A common misinterpretation is thinking you need to either increment ALL characters or NONE of them. Some might try to solve this by checking two separate cases:

  1. Check if str2 is a subsequence of str1 without any changes
  2. Check if str2 is a subsequence of str1 after incrementing ALL characters

Why It's Wrong: The problem allows you to select ANY subset of indices to increment. You can increment some characters while leaving others unchanged.

Incorrect Approach:

def canMakeSubsequence(self, str1: str, str2: str) -> bool:
    # Wrong: Only checking all-or-nothing scenarios
    def isSubsequence(s1, s2):
        j = 0
        for char in s1:
            if j < len(s2) and char == s2[j]:
                j += 1
        return j == len(s2)
  
    # Check without any increments
    if isSubsequence(str1, str2):
        return True
  
    # Check with ALL characters incremented
    incremented = ''.join('a' if c == 'z' else chr(ord(c) + 1) for c in str1)
    return isSubsequence(incremented, str2)

The Fix: The correct solution checks both possibilities (original and incremented) for EACH character independently during the traversal.

Pitfall 2: Forgetting Cyclic Wrap for 'z'

The Mistake: Incrementing 'z' using simple ASCII arithmetic without handling the wrap-around case.

Incorrect Code:

# Wrong: This would produce '{' instead of 'a' for 'z'
next_char = chr(ord(char) + 1)

The Fix: Always handle the 'z' to 'a' wrap explicitly:

next_char = "a" if char == "z" else chr(ord(char) + 1)

Pitfall 3: Trying to Track Which Characters Were Incremented

The Mistake: Attempting to maintain state about which characters have been incremented, thinking you can only increment each character once across the entire string.

Why It's Wrong: The operation happens all at once - you select a set of indices and increment them simultaneously. Each character position makes an independent choice.

Incorrect Approach:

def canMakeSubsequence(self, str1: str, str2: str) -> bool:
    used_increment = set()  # Wrong: Unnecessary tracking
    j = 0
  
    for i, char in enumerate(str1):
        if j >= len(str2):
            break
          
        if char == str2[j]:
            j += 1
        elif i not in used_increment:  # Wrong logic
            next_char = "a" if char == "z" else chr(ord(char) + 1)
            if next_char == str2[j]:
                used_increment.add(i)
                j += 1
  
    return j == len(str2)

The Fix: The greedy approach doesn't need to track state. For each position, we simply check if either the original or incremented character matches what we need from str2.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following array represent a max heap?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More