DSA sheet · Stack · Simulation & undo pattern
Minimum String Length After Removing Substrings
The last problem of the simulation and undo pattern. The teacher says that if you've done Remove All Adjacent Duplicates and Make The String Great, you can solve this one alone, and she's right. The loop is identical. Only the cancel rule changes: "A" followed by "B", or "C" followed by "D", vanish. And we return a length, not a string. She covers the "go back and delete" brute force, the stack, and the faster string-builder version, plus some useful tips on when code really hits TLE.
Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember
- Part 0 · What you must know before starting (stack, the undo pattern, TLE limits)
- Part A · Brute force: delete in place and step back
- Part B · Stack: compare with the top
- Part C · Same idea, builder instead of a stack
- Part D · Revision page
Part 0 · Before starting
What is a stack?
A stack is a pile where you only touch the top. The last item in is the first item out: LIFO (Last In, First Out).
| operation | meaning | Python (list as a stack) | cost |
|---|---|---|---|
| push | put on top | stack.append(x) | O(1) |
| pop | remove the top | stack.pop() | O(1) |
| peek | look at the top | stack[-1] | O(1) |
| is empty? | anything there? | if stack: | O(1) |
| size | how many items | len(stack) | O(1) |
IndexError).The undo pattern
Read left to right. The top of the stack is the latest surviving character. If the new character and the top together form something removable, pop the top and don't push the new one. Otherwise push. After a pop, the next character below becomes the top, which is exactly the character that now sits next to whatever comes after. So chain reactions ("removing AB makes a new CD appear") are handled for free.
The teacher's TLE tips (said in this video)
- On LeetCode, roughly 10⁸ operations is safe. Even 5·10⁸ may pass, slowly, if the code is simple.
- If the code is heavy or complex, you can get TLE even around 10⁷.
- Recursion has extra overhead too. She'll cover that later in the playlist.
Part A · Brute force: delete in place and step back
LeetCode 2696
1The question in simple words
You get a string s of uppercase letters. In one move you may delete any occurrence of the substring "AB" or "CD" (a substring = characters sitting next to each other, in that order). After a deletion, the two sides join and can form new "AB" or "CD". Return the minimum possible length of the string at the end.
Put simply: when you stand on a B and the previous character is an A, both can go. When you stand on a D and the previous one is a C, both can go. Same as the last two problems, just a different pair rule.
"ABFCACDB" → delete "AB" → "FCACDB" → delete "CD" → "FCAB" → now "AB" has formed → delete → "FC". Length 2.
Another: "ACBBD" → there's no "AB" or "CD" anywhere ("AC", "CB", "BB", "BD") → nothing can be removed → 5.
→ No. (My note, the video takes this for granted.) "AB" and "CD" share no letters, and a pair can't overlap with itself (that would need B = A). So two removable pairs never fight over the same character, and any order of deletions ends at the same final string. Removing greedily, as soon as a pair appears, already gives the minimum. The test file checks this against a search over every possible order.
→ No. The order matters: A must come before B, and C before D. So the check is "top is A and current is B", not "they are A and B in any order".
2What the constraints tell us
- 1 ≤ s.length ≤ 100. Very small: n² = 10⁴ steps. The brute force will be accepted easily (see the TLE tips in Part 0).
- Only uppercase English letters. No case conversion is needed this time, unlike Make The String Great.
- The answer can be 0 (everything removed, e.g. "CABD").
3Intuition: check the previous character, delete, look back again
Walk through the string with a simple loop. At each character, look at the previous one. If together they're "AB" or "CD", delete both. But now the character before them touches the character after them, so you have to go back and check again. That's the same back-and-forth we saw in the last two problems.
4Building the logic from the example
- At A (index 0): nothing before it, nothing to do.
- At B: the previous is A → "AB" → delete both. String: "FCACDB".
- At F, then C, A, C: no pair ("FC", "CA", "AC" aren't removable).
- At D: the previous is C → "CD" → delete. String: "FCAB". We must look back: the A before the hole now touches the B after it.
- At B: the previous is A → "AB" → delete. String: "FC". Done → length 2.
Every deletion in the middle of a string shifts the rest left (O(n)), and we keep stepping back. The teacher's question: "where have we seen go back and check the previous one before?" → the stack.
5Approach steps
- Copy s into a list
arr. Seti = 1(the "current" character; i − 1 is the previous one). - While
i < len(arr): if arr[i−1] + arr[i] is "AB" or "CD" → delete both, and step back (i -= 1, but not below 1). - Else
i += 1. - Return
len(arr).
6Code (Python)
class Solution:
def minLength(self, s):
arr = list(s)
i = 1 # current char; i-1 = previous
while i < len(arr):
pair = arr[i - 1] + arr[i]
if pair == "AB" or pair == "CD":
del arr[i - 1:i + 1] # O(n) shift
if i > 1:
i -= 1 # new neighbours, look back
else:
i += 1
return len(arr)7Code line by line
| line | what it means |
|---|---|
| arr = list(s) | Strings can't be changed in place, lists can. |
| i = 1 | Start at the second character so it has a previous one. |
| pair = arr[i - 1] + arr[i] | The previous character and the current one, as a 2-letter string. |
| if pair == "AB" or pair == "CD": | One of the two removable substrings, in the right order. |
| del arr[i - 1:i + 1] | Remove both. Everything on the right slides left by 2. |
| if i > 1: i -= 1 | The character now at i − 1 and the one now at i are new neighbours. Step back so they get checked. Never go below 1. |
| return len(arr) | The question wants the length, not the string. |
8Dry run
s = "ABFCACDB".
| step | i | pair (i−1, i) | removable? | arr after | next i |
|---|---|---|---|---|---|
| 1 | 1 | A B | yes | F C A C D B | 1 (can't go below 1) |
| 2 | 1 | F C | no | same | 2 |
| 3 | 2 | C A | no | same | 3 |
| 4 | 3 | A C | no | same | 4 |
| 5 | 4 | C D | yes | F C A B | 3 (step back) |
| 6 | 3 | A B | yes | F C | 2 (step back) |
| 7 | 2 | 2 < len 2 is false → loop ends | return 2 | ||
Answer 2 ✓
9Complexity & remember
- Time O(n²): each middle deletion costs O(n), and there can be up to n/2 of them. With n ≤ 100 that's tiny, so it's accepted.
- Space O(n) for the list copy.
Part B · Stack: compare with the top
1The question (same as Part A)
Same question, one pass, O(1) work per character.
2What the constraints tell us
n ≤ 100, so we don't strictly need it. But "look back at the previous survivor" is the stack signal, and this is the version interviewers expect.
3Intuition
The stack holds the characters that survive so far, and its top is the "previous character" of the brute force. For each new ch:
- stack empty → nothing to pair with → push;
- top is 'A' and ch is 'B', or top is 'C' and ch is 'D' → pop the top, don't push ch (both excluded);
- otherwise → push ch.
At the end, the number of characters left on the stack is the answer: len(stack).
4Building the logic from the example
"ABFCACDB": A → push. B with top A → pop (A and B both excluded). F, C, A, C → no pop condition → push all four. D with top C → pop. B with top A → pop. Stack = [F, C] → 2. The teacher points out that the F-C-A-C stretch has no popping condition, so they just pile up until D and B arrive and unwind them.
→ No. In the previous problems we converted the stack to a string because the answer was a string. Here we only need how many characters are left, so
len(stack) is enough. No conversion step.5Approach steps
- Empty stack.
- For each ch: if the stack is non-empty and (top, ch) is ('A','B') or ('C','D') → pop. Else push ch.
- Return
len(stack).
6Code (Python)
class Solution:
def minLength(self, s):
stack = []
for ch in s:
if stack and ((stack[-1] == "A" and ch == "B") or
(stack[-1] == "C" and ch == "D")):
stack.pop() # "AB" or "CD" vanishes
else:
stack.append(ch)
return len(stack) # how many survived7Code line by line
| line | what it means |
|---|---|
| if stack and (…) | Only compare when there's a previous survivor. |
| stack[-1] == "A" and ch == "B" | The previous is A, the current is B → "AB". The order matters. |
| stack[-1] == "C" and ch == "D" | Same idea for "CD". |
| stack.pop() | Remove the A (or C). The B (or D) is never pushed, so both are gone. |
| stack.append(ch) | No match: ch survives, for now. |
| return len(stack) | The minimum length. |
8Dry run (stack after every step)
s = "ABFCACDB". Stack drawn left = bottom, right = top.
| i | current | what we pop (and why) | what we push | stack AFTER | length so far |
|---|---|---|---|---|---|
| 0 | A | nothing (stack empty) | A | [A] | 1 |
| 1 | B | A: top A + current B = "AB" | nothing | [ ] | 0 |
| 2 | F | nothing (stack empty) | F | [F] | 1 |
| 3 | C | nothing ("FC" isn't removable) | C | [F, C] | 2 |
| 4 | A | nothing ("CA") | A | [F, C, A] | 3 |
| 5 | C | nothing ("AC") | C | [F, C, A, C] | 4 |
| 6 | D | C: top C + current D = "CD" | nothing | [F, C, A] | 3 |
| 7 | B | A: top A + current B = "AB" (the chain reaction) | nothing | [F, C] | 2 |
Answer len(stack) = 2 ✓
9Complexity & remember
- Time O(n): one pass; each character is pushed once and popped at most once.
- Space O(n): the stack. Worst case, nothing is removable ("ACBBD") and everything is pushed.
len(stack).Part C · Same idea, builder instead of a stack
1What changes
Exactly as in the last two problems: use a string builder as the stack. Its last character is the top, deleting the last character is the pop, and appending is the push. The teacher writes it slightly differently this time, using continue:
- If the builder isn't empty, read the last character.
- If (last, ch) is "AB" or "CD" → delete the last character and
continue, which skips the rest of the loop body, so ch is never appended. - The append line runs only when that
continuedid not happen.
At the end, return the builder's length. No toString is needed, since the answer is a number.
2Why it's faster, and what to say in an interview
Same Big-O, but faster in Java: Stack is synchronized (it locks on every push and pop), while StringBuilder is a plain array with no locks. The teacher's advice: a stack solution is perfectly fine in an interview. If the interviewer pushes you to optimise further, the builder version is what they're hoping to hear.
→ Python has no StringBuilder, so the teacher says to just use the stack (a list). A Python list already is a lock-free array, so Parts B and C cost the same in Python. Part C is written below in the builder/continue style so you can see her structure.
3Code (Python, builder style with continue)
class Solution:
def minLength(self, s):
sb = []
for ch in s:
length = len(sb)
if length > 0:
last = sb[length - 1]
if (last == "A" and ch == "B") or (last == "C" and ch == "D"):
del sb[length - 1] # remove the A / C
continue # and never add the B / D
sb.append(ch) # reached only if no removal
return len(sb)| line | what it means |
|---|---|
| if length > 0: | Only look at the last character if there is one. |
| del sb[length - 1] continue | Pop, then jump straight to the next character. The append below is skipped. |
| sb.append(ch) | Runs for every character that didn't cause a removal (including when the builder was empty). |
| return len(sb) | The length is the answer. |
Dry run: the same as Part B. The builder goes [A] → [ ] → [F] → [F,C] → [F,C,A] → [F,C,A,C] → [F,C,A] → [F,C] → length 2.
4Complexity & remember
- Time O(n), space O(n).
continue after a removal is a neat way to say "don't append this one".Part D · Revision page
The whole undo family side by side. Same loop, different cancel rule:
| problem | new ch cancels the top when… | return |
|---|---|---|
| Backspace String Compare | ch is '#' (it removes the top, any letter) | compare two results |
| Remove Adjacent Duplicates | top == ch | the string |
| Make The String Great | top.lower() == ch.lower() and top != ch | the string |
| Min Length After Removing Substrings | (top, ch) is ('A','B') or ('C','D') | the length |
| A · delete in place | B · stack | C · builder | |
|---|---|---|---|
| time | O(n²) (accepted, n ≤ 100) | O(n) | O(n), fastest in Java |
| space | O(n) | O(n) | O(n) |
2. Pop when top 'A' meets 'B', or top 'C' meets 'D'. The order matters ("BA" stays).
3. Otherwise push. Check for empty before reading the top.
4. Return
len(stack); no string building needed.5. Removal order doesn't change the result, so the greedy stack gives the minimum.
✗ pushing ch after popping (B or D would stay behind)
✗ only scanning once without looking back (misses "FCAB" → "FC")
✗ returning the string instead of its length
✗ reading
stack[-1] on an empty stacks = Solution()
print(s.minLength("ABFCACDB")) # 2
print(s.minLength("ACBBD")) # 5
print(s.minLength("CABD")) # 0
print(s.minLength("BA")) # 2
print(s.minLength("A")) # 1Based on this video: Minimum String Length After Removing Substrings | Stack simulation & undo pattern