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"
andstr2 = "ad"
, you can increment'c'
to'd'
, makingstr1 = "abd"
. Now"ad"
is a subsequence of"abd"
, so returntrue
. - If
str1 = "zc"
andstr2 = "ad"
, you can increment'z'
to'a'
and'c'
to'd'
, makingstr1 = "ad"
. Now"ad"
equals"ad"
, so returntrue
.
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.
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:
- The character matches directly (no operation needed)
- 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:
-
Initialize a pointer
i = 0
to track our current position instr2
. -
Iterate through each character
c
instr1
:- 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)
- If
- Store this next character as
d
- Calculate what
-
Check for matching:
- If we haven't finished matching
str2
yet (i < len(str2)
), check if the current character we need fromstr2[i]
matches either:- The original character
c
fromstr1
, OR - The incremented version
d
- The original character
- If either matches, increment
i
to move to the next character instr2
- If we haven't finished matching
-
Return the result:
- After processing all characters in
str1
, check ifi == len(str2)
- If true, we successfully matched all characters of
str2
, making it a subsequence - If false, we couldn't match all characters
- After processing all characters in
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 matchstr2[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 EvaluatorExample 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'
instr2
)
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 equalslen(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:
- Check if
str2
is a subsequence ofstr1
without any changes - Check if
str2
is a subsequence ofstr1
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
.
Which of the following array represent a max heap?
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!