title: Card Flipping Game
slug: card-flipping-game
difficulty: medium
leetcode_id: 822
leetcode_url: https://leetcode.com/problems/card-flipping-game/
categories:
  - arrays
  - hash-tables
patterns:
  - slug: greedy
    is_optimal: true

function_signature: "def flipgame(fronts: list[int], backs: list[int]) -> int:"

test_cases:
  visible:
    - input: { fronts: [1, 2, 4, 4, 7], backs: [1, 3, 4, 1, 3] }
      expected: 2
    - input: { fronts: [1], backs: [1] }
      expected: 0
  hidden:
    - input: { fronts: [1, 2], backs: [2, 1] }
      expected: 1
    - input: { fronts: [2, 2], backs: [2, 2] }
      expected: 0
    - input: { fronts: [1, 1], backs: [1, 2] }
      expected: 2
    - input: { fronts: [5, 3, 7], backs: [5, 3, 8] }
      expected: 7
    - input: { fronts: [1, 2, 3], backs: [4, 5, 6] }
      expected: 1

description: |
  You are given two **0-indexed** integer arrays `fronts` and `backs` of length `n`, where the i<sup>th</sup> card has the positive integer `fronts[i]` printed on the front and `backs[i]` printed on the back. Initially, each card is placed on a table such that the front number is facing up and the other is facing down.

  You may flip over any number of cards (possibly zero).

  After flipping the cards, an integer is considered **good** if it is facing down on some card and **not** facing up on any card.

  Return *the minimum possible good integer after flipping the cards*. If there are no good integers, return `0`.

constraints: |
  - `n == fronts.length == backs.length`
  - `1 <= n <= 1000`
  - `1 <= fronts[i], backs[i] <= 2000`

examples:
  - input: "fronts = [1,2,4,4,7], backs = [1,3,4,1,3]"
    output: "2"
    explanation: "If we flip the second card, the face up numbers are [1,3,4,4,7] and the face down are [1,2,4,1,3]. 2 is the minimum good integer as it appears facing down but not facing up."
  - input: "fronts = [1], backs = [1]"
    output: "0"
    explanation: "There are no good integers no matter how we flip the cards, so we return 0."

explanation:
  intuition: |
    The key insight is understanding what makes a number **impossible** to be "good".

    Think about it this way: a number is "good" if we can arrange the cards such that it appears on the back of at least one card but never on the front of any card. We have full control over which side of each card faces up — we can flip any card we want.

    So when is a number **impossible** to hide from the front? Only when a card has the **same number on both sides**. If `fronts[i] == backs[i]`, that number will always be visible face-up, no matter how we flip that card. These numbers are "blocked" — they can never be good.

    For any other number that appears in our arrays, we can always arrange the flips so that it appears only on the back. For example, if number `x` appears on the front of card `i`, we can flip card `i` to hide it. As long as `x` isn't blocked by appearing on a same-number card, we can make it good.

    The strategy becomes clear: find all blocked numbers, then find the minimum number from all fronts and backs that isn't blocked.

  approach: |
    We solve this using a **Set-based Elimination** approach:

    **Step 1: Identify blocked numbers**

    - Create a set called `blocked` to store numbers that can never be good
    - Iterate through all cards: if `fronts[i] == backs[i]`, add that number to `blocked`
    - These are the only numbers that will always be visible face-up regardless of flipping

    &nbsp;

    **Step 2: Find the minimum valid candidate**

    - Initialise `result` to infinity (or a large value like `2001` given constraints)
    - Iterate through all numbers in both `fronts` and `backs` arrays
    - For each number, if it's not in `blocked`, consider it as a candidate
    - Track the minimum such candidate

    &nbsp;

    **Step 3: Return the result**

    - If we found a valid candidate (result is not infinity), return it
    - Otherwise, return `0` indicating no good integer exists

    &nbsp;

    The greedy choice of taking the minimum works because any non-blocked number can be made good, so we simply pick the smallest one.

  common_pitfalls:
    - title: Overcomplicating the Flip Logic
      description: |
        A common mistake is trying to simulate all possible flip combinations or track which cards are flipped.

        The insight is that we don't need to track flips at all. We only need to identify which numbers are "blocked" (same on both sides of a card). Any non-blocked number can be made good through some sequence of flips.

        For example, if `7` appears on front of card A and back of card B, we can flip card A (hiding the 7) and keep card B unflipped (7 is on the back, not visible). The specific flip sequence doesn't matter — what matters is whether the number is blocked or not.
      wrong_approach: "Simulating all 2^n flip combinations"
      correct_approach: "Identify blocked numbers with a set, find minimum non-blocked"

    - title: Only Checking Fronts or Only Backs
      description: |
        Some solutions only search for the minimum among the `fronts` array or only among the `backs` array.

        The minimum good integer could come from either array. A number on the back of a card can be exposed (made face-down) simply by not flipping that card. A number on the front can be exposed by flipping that card.

        You must search through all values in both arrays to find the global minimum candidate.
      wrong_approach: "Only iterating through fronts array"
      correct_approach: "Checking all values in both fronts and backs"

    - title: Misunderstanding "Good" Definition
      description: |
        The definition states a good integer must be facing **down** on some card and **not** facing up on **any** card.

        This means if a number appears face-up on even one card in the final configuration, it cannot be good — even if it's also face-down on another card. The blocked set correctly captures this: if a card has the same number on both sides, that number will always be face-up somewhere.

  key_takeaways:
    - "**Constraint analysis**: When a value appears on both sides of the same card, it's impossible to hide — this is the key insight"
    - "**Set-based elimination**: Use a set to track blocked/invalid options, then search remaining candidates"
    - "**Greedy works when all valid options are achievable**: Since any non-blocked number can be made good, we simply pick the minimum"
    - "**Don't simulate when you can reason**: Instead of exploring `2^n` flip states, identify the constraint that matters"

  time_complexity: "O(n). We iterate through the arrays twice — once to build the blocked set and once to find the minimum candidate."
  space_complexity: "O(n). The blocked set can contain at most `n` elements (one per card with matching front and back)."

solutions:
  - approach_name: Set-based Elimination
    is_optimal: true
    code: |
      def flipgame(fronts: list[int], backs: list[int]) -> int:
          # Numbers that appear on both sides of same card are blocked
          # They will always be visible face-up no matter how we flip
          blocked = {f for f, b in zip(fronts, backs) if f == b}

          # Find minimum number that isn't blocked
          result = float('inf')

          # Check all numbers from fronts
          for num in fronts:
              if num not in blocked:
                  result = min(result, num)

          # Check all numbers from backs
          for num in backs:
              if num not in blocked:
                  result = min(result, num)

          # Return 0 if no valid number found
          return result if result != float('inf') else 0
    explanation: |
      **Time Complexity:** O(n) — Three linear passes through the arrays.

      **Space Complexity:** O(n) — The blocked set stores at most n elements.

      We first identify all "blocked" numbers (those appearing on both sides of the same card). Then we find the minimum number from either array that isn't blocked. This works because any non-blocked number can be arranged to appear only on the back through appropriate flipping.

  - approach_name: Single Pass with Set
    is_optimal: true
    code: |
      def flipgame(fronts: list[int], backs: list[int]) -> int:
          # Build blocked set first
          blocked = {f for f, b in zip(fronts, backs) if f == b}

          # Find minimum non-blocked value across both arrays
          result = float('inf')
          for f, b in zip(fronts, backs):
              if f not in blocked:
                  result = min(result, f)
              if b not in blocked:
                  result = min(result, b)

          return result if result != float('inf') else 0
    explanation: |
      **Time Complexity:** O(n) — Two passes: one to build the set, one to find minimum.

      **Space Complexity:** O(n) — The blocked set stores at most n elements.

      This is a slightly more compact version that checks both front and back values in a single iteration. The logic is identical — we exclude blocked numbers and find the minimum valid candidate.

  - approach_name: Brute Force Simulation
    is_optimal: false
    code: |
      def flipgame(fronts: list[int], backs: list[int]) -> int:
          n = len(fronts)
          result = float('inf')

          # Try all 2^n possible flip combinations
          for mask in range(1 << n):
              face_up = set()
              face_down = []

              for i in range(n):
                  if mask & (1 << i):  # Card i is flipped
                      face_up.add(backs[i])
                      face_down.append(fronts[i])
                  else:  # Card i is not flipped
                      face_up.add(fronts[i])
                      face_down.append(backs[i])

              # Find minimum good number for this configuration
              for num in face_down:
                  if num not in face_up:
                      result = min(result, num)

          return result if result != float('inf') else 0
    explanation: |
      **Time Complexity:** O(n * 2^n) — Exponential in the number of cards.

      **Space Complexity:** O(n) — Sets and lists for each configuration.

      This approach tries every possible way to flip the cards and checks for good numbers in each configuration. While correct, it's far too slow for the given constraints (`n <= 1000`). With 1000 cards, `2^1000` is astronomically large. This solution would only work for very small inputs and is included to illustrate why the set-based approach is necessary.
