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 · 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).

operationmeaningPython (list as a stack)cost
pushput on topstack.append(x)O(1)
popremove the topstack.pop()O(1)
peeklook at the topstack[-1]O(1)
is empty?anything there?if stack:O(1)
sizehow many itemslen(stack)O(1)
Check for empty firstIf the stack is empty, there's no previous character, so nothing can be removed. Just push. Peeking an empty list crashes (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)


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.

index01234567
sABFCACDBanswer 2 ("FC" is left)

"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.

Doubt 1: it says "minimum". Do I have to try different orders of deletion to find the smallest result?
→ 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.
Doubt 2: does "BA" or "DC" count?
→ 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

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

  1. At A (index 0): nothing before it, nothing to do.
  2. At B: the previous is A → "AB" → delete both. String: "FCACDB".
  3. At F, then C, A, C: no pair ("FC", "CA", "AC" aren't removable).
  4. 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.
  5. 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

  1. Copy s into a list arr. Set i = 1 (the "current" character; i − 1 is the previous one).
  2. 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).
  3. Else i += 1.
  4. Return len(arr).

6Code (Python)

Brute force: delete in place, step back, O(n²)
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

linewhat it means
arr = list(s)Strings can't be changed in place, lists can.
i = 1Start 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 -= 1The 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".

stepipair (i−1, i)removable?arr afternext i
11A ByesF C A C D B1 (can't go below 1)
21F Cnosame2
32C Anosame3
43A Cnosame4
54C DyesF C A B3 (step back)
63A ByesF C2 (step back)
722 < len 2 is false → loop endsreturn 2

Answer 2 ✓

9Complexity & remember

Remember the brute forceLook at (previous, current). "AB" or "CD" → delete both and look back one step. Fine for n = 100, but every delete shifts the string.

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:

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.

Doubt 1: the question wants a number. Do I still need to build the string?
→ 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

  1. Empty stack.
  2. For each ch: if the stack is non-empty and (top, ch) is ('A','B') or ('C','D') → pop. Else push ch.
  3. Return len(stack).

6Code (Python)

Stack, O(n) time, O(n) space
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 survived

7Code line by line

linewhat 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.

icurrentwhat we pop (and why)what we pushstack AFTERlength so far
0Anothing (stack empty)A[A]1
1BA: top A + current B = "AB"nothing[ ]0
2Fnothing (stack empty)F[F]1
3Cnothing ("FC" isn't removable)C[F, C]2
4Anothing ("CA")A[F, C, A]3
5Cnothing ("AC")C[F, C, A, C]4
6DC: top C + current D = "CD"nothing[F, C, A]3
7BA: top A + current B = "AB" (the chain reaction)nothing[F, C]2
after i = 5 (tallest)
FCAC
after i = 6 (C popped by D)
FCA
after i = 7 (final)
FC

Answer len(stack) = 2 ✓

9Complexity & remember

Remember the stack versionTop 'A' + ch 'B', or top 'C' + ch 'D' → pop; else push. Answer = 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:

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.

Doubt 1: what about Python?
→ 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)

Builder used as the stack, O(n) time, O(n) space
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)
linewhat it means
if length > 0:Only look at the last character if there is one.
del sb[length - 1] continuePop, 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

RememberBuilder = stack without locks. 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:

problemnew ch cancels the top when…return
Backspace String Comparech is '#' (it removes the top, any letter)compare two results
Remove Adjacent Duplicatestop == chthe string
Make The String Greattop.lower() == ch.lower() and top != chthe string
Min Length After Removing Substrings(top, ch) is ('A','B') or ('C','D')the length
A · delete in placeB · stackC · builder
timeO(n²) (accepted, n ≤ 100)O(n)O(n), fastest in Java
spaceO(n)O(n)O(n)
If you remember only 5 lines 1. Removing "AB"/"CD" can create a new one → look back at the previous survivor → stack.
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.
Mistakes to avoid ✗ treating "BA" or "DC" as removable
✗ 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 stack
test it yourself (paste under any of the solutions above)
s = 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"))          # 1

Based on this video: Minimum String Length After Removing Substrings | Stack simulation & undo pattern