title: Check If Word Is Valid After Substitutions
slug: check-if-word-is-valid-after-substitutions
difficulty: medium
leetcode_id: 1003
leetcode_url: https://leetcode.com/problems/check-if-word-is-valid-after-substitutions/
categories:
  - strings
  - stack
patterns:
  - monotonic-stack

description: |
  Given a string `s`, determine if it is **valid**.

  A string `s` is **valid** if, starting with an empty string `t = ""`, you can **transform** `t` **into** `s` after performing the following operation **any number of times**:

  - Insert the string `"abc"` into any position in `t`. More formally, `t` becomes `t_left + "abc" + t_right`, where `t == t_left + t_right`. Note that `t_left` and `t_right` may be **empty**.

  Return `true` *if* `s` *is a valid string, otherwise, return* `false`.

constraints: |
  - `1 <= s.length <= 2 * 10^4`
  - `s` consists of letters `'a'`, `'b'`, and `'c'` only

examples:
  - input: 's = "aabcbc"'
    output: "true"
    explanation: '"" -> "abc" -> "aabcbc". We can insert "abc" at position 1 (after the first "a"), producing the valid string.'
  - input: 's = "abcabcababcc"'
    output: "true"
    explanation: '"" -> "abc" -> "abcabc" -> "abcabcabc" -> "abcabcababcc". Multiple insertions at different positions produce this valid string.'
  - input: 's = "abccba"'
    output: "false"
    explanation: "It is impossible to produce \"abccba\" using only insertions of \"abc\". The sequence \"cba\" cannot be formed by any valid insertion."

explanation:
  intuition: |
    Think of this problem like **matching parentheses**, but instead of `(` and `)`, we're matching the sequence `a`, `b`, `c`.

    Imagine you're reading the string character by character. When you see an `a`, it's like opening a new "bracket". When you see a `b`, it must follow an `a`. When you see a `c`, it must complete a valid `abc` triplet.

    The key insight is that whenever we encounter `c`, the two characters immediately before it (in our "pending" sequence) must be `a` and `b` in that order. If they are, we've found a complete `abc` and can "remove" it, just like matching parentheses.

    This is the **reverse operation** of the problem: instead of inserting `abc`, we *remove* every `abc` we find. If the string was built from valid insertions, we should be able to remove all characters and end with an empty string.

  approach: |
    We solve this using a **Stack-Based Approach**:

    **Step 1: Initialise an empty stack**

    - `stack`: An empty list to track characters we've seen but haven't matched yet

    &nbsp;

    **Step 2: Process each character**

    - For each character `c` in the string:
      - If `c == 'c'`: Check if the stack ends with `['a', 'b']`
        - If yes, pop both `a` and `b` — we've matched a complete `abc`
        - If no, return `False` — invalid sequence
      - Otherwise: Push the character onto the stack

    &nbsp;

    **Step 3: Check final state**

    - After processing all characters, return `True` if the stack is empty
    - An empty stack means all characters formed complete `abc` triplets

    &nbsp;

    **Why this works:** Every valid string is built by inserting `abc` into previous valid strings. By "unwinding" these insertions (removing `abc` whenever we complete one), we reverse the construction process. A valid string reduces to empty; an invalid one leaves residue.

  common_pitfalls:
    - title: Using String Replacement
      description: |
        A tempting approach is to repeatedly use `s.replace("abc", "")` until no more replacements occur, then check if the string is empty.

        While this is correct, it's **inefficient**. Each replacement creates a new string and re-scans, leading to **O(n^2)** time complexity in the worst case. For `n = 2 * 10^4`, this risks TLE.

        The stack approach processes each character exactly once for **O(n)** time.
      wrong_approach: "Repeated string replacement"
      correct_approach: "Stack-based single pass"

    - title: Not Checking Stack Size Before Popping
      description: |
        When you see a `c`, you need to verify the stack has at least two elements (`a` and `b`) before popping.

        For example, with input `"c"` or `"bc"`, the stack doesn't have enough characters. Attempting to pop from an insufficient stack causes an error or incorrect result.

        Always check `len(stack) >= 2` before accessing `stack[-1]` and `stack[-2]`.
      wrong_approach: "Pop without checking stack size"
      correct_approach: "Verify stack has at least 2 elements before checking for 'ab'"

    - title: Forgetting Order Matters
      description: |
        The sequence must be exactly `a`, then `b`, then `c`. Checking only that the stack contains `a` and `b` somewhere isn't enough.

        For input `"bac"`, a naive check might pass, but `bac` cannot be formed by inserting `abc`. The stack must end with `a` at position `-2` and `b` at position `-1` specifically.
      wrong_approach: "Check if 'a' and 'b' exist anywhere in stack"
      correct_approach: "Check stack[-2] == 'a' and stack[-1] == 'b'"

  key_takeaways:
    - "**Stack for sequence matching**: When validating nested or sequential patterns, stacks let you track 'pending' elements and match them when completed"
    - "**Reverse the operation**: Instead of simulating insertion, simulate *removal* — valid constructions can be fully deconstructed"
    - "**Character-by-character processing**: Building or unwinding strings incrementally often leads to linear-time solutions"
    - "**Similar problems**: This pattern applies to valid parentheses, HTML tag matching, and other nested structure validation"

  time_complexity: "O(n). Each character is pushed onto the stack at most once and popped at most once, giving us linear time."
  space_complexity: "O(n). In the worst case (e.g., `\"aaa...\"` with only `a`s), the stack stores all characters."

solutions:
  - approach_name: Stack
    is_optimal: true
    code: |
      def is_valid(s: str) -> bool:
          stack = []

          for char in s:
              if char == 'c':
                  # Check if we can complete an "abc" triplet
                  if len(stack) >= 2 and stack[-2] == 'a' and stack[-1] == 'b':
                      # Remove the 'a' and 'b' that preceded this 'c'
                      stack.pop()
                      stack.pop()
                  else:
                      # Invalid: 'c' without proper 'ab' prefix
                      return False
              else:
                  # Push 'a' or 'b' onto the stack
                  stack.append(char)

          # Valid only if all characters formed complete "abc" triplets
          return len(stack) == 0
    explanation: |
      **Time Complexity:** O(n) — Single pass through the string, with O(1) stack operations per character.

      **Space Complexity:** O(n) — Stack may hold up to n characters in the worst case.

      We process characters left to right. When we see `c`, we check if the top of the stack is `ab`. If so, we pop both and effectively "remove" a complete `abc`. If not, the string is invalid. At the end, an empty stack means every character was part of a valid `abc` triplet.

  - approach_name: String Replacement
    is_optimal: false
    code: |
      def is_valid(s: str) -> bool:
          # Keep removing "abc" until no more can be removed
          prev_len = -1

          while len(s) != prev_len:
              prev_len = len(s)
              s = s.replace("abc", "")

          # Valid if everything was removed
          return len(s) == 0
    explanation: |
      **Time Complexity:** O(n^2) — Each replacement takes O(n), and we may need O(n) replacements.

      **Space Complexity:** O(n) — Each replacement creates a new string.

      This approach repeatedly removes all occurrences of `"abc"` until the string stops changing. While correct and intuitive, it's slower than the stack approach due to repeated string creation and scanning. Useful for understanding the problem but not optimal for large inputs.
