title: Bulls and Cows
slug: bulls-and-cows
difficulty: medium
leetcode_id: 299
leetcode_url: https://leetcode.com/problems/bulls-and-cows/
categories:
  - strings
  - hash-tables
patterns:
  - two-pointers

description: |
  You are playing the **Bulls and Cows** game with your friend.

  You write down a secret number and ask your friend to guess what the number is. When your friend makes a guess, you provide a hint with the following info:

  - The number of "bulls", which are digits in the guess that are in the correct position.
  - The number of "cows", which are digits in the guess that are in your secret number but are located in the wrong position. Specifically, the non-bull digits in the guess that could be rearranged such that they become bulls.

  Given the secret number `secret` and your friend's guess `guess`, return *the hint for your friend's guess*.

  The hint should be formatted as `"xAyB"`, where `x` is the number of bulls and `y` is the number of cows. Note that both `secret` and `guess` may contain duplicate digits.

constraints: |
  - `1 <= secret.length, guess.length <= 1000`
  - `secret.length == guess.length`
  - `secret` and `guess` consist of digits only

examples:
  - input: 'secret = "1807", guess = "7810"'
    output: '"1A3B"'
    explanation: "The digit '8' is in the correct position (bull). The digits '1', '0', and '7' appear in both strings but in wrong positions (cows)."
  - input: 'secret = "1123", guess = "0111"'
    output: '"1A1B"'
    explanation: "The second '1' in the guess matches the second position in secret (bull). Only one of the remaining '1's can be a cow since secret only has one other '1' available."

explanation:
  intuition: |
    Think of this as a two-phase matching problem. First, we need to identify **exact matches** (bulls) — digits that are in the right position. Then, for the remaining unmatched digits, we count how many digits from the guess appear *somewhere* in the secret (cows).

    The tricky part is handling duplicates correctly. Consider `secret = "1123"` and `guess = "0111"`. The guess has three `1`s, but secret only has two. After matching one `1` as a bull (at position 1), only one more `1` can be a cow — not two.

    The key insight is to use **frequency counting**. After removing bulls, we count how many of each digit remains in both strings. For each digit, the number of cows is the *minimum* of its remaining count in secret and guess — you can only match as many as the lesser string has.

  approach: |
    We solve this using a **Single Pass with Frequency Arrays**:

    **Step 1: Initialise counters**

    - `bulls`: Counter for exact position matches
    - `cows`: Counter for correct digits in wrong positions
    - `secret_count`: Array of size 10 to track unmatched digit frequencies in secret
    - `guess_count`: Array of size 10 to track unmatched digit frequencies in guess

    &nbsp;

    **Step 2: Iterate through both strings simultaneously**

    - For each position `i`, compare `secret[i]` with `guess[i]`
    - If they match, increment `bulls` — this is an exact match
    - If they don't match:
      - Increment the count for `secret[i]` in `secret_count`
      - Increment the count for `guess[i]` in `guess_count`

    &nbsp;

    **Step 3: Calculate cows from frequency arrays**

    - For each digit `0` through `9`:
      - Add `min(secret_count[digit], guess_count[digit])` to `cows`
    - This ensures we only count as many cows as can actually be matched

    &nbsp;

    **Step 4: Format and return the result**

    - Return the string `"{bulls}A{cows}B"`

  common_pitfalls:
    - title: Counting Cows Before Removing Bulls
      description: |
        A common mistake is to count all matching digits as potential cows, then subtract bulls. This fails with duplicates.

        For `secret = "1122"` and `guess = "2211"`: if you count all matching digits first (two `1`s and two `2`s = 4), then subtract bulls (0), you get 4 cows. But the correct answer is `"0A4B"` — the logic happens to work here, but the approach is fragile.

        By separating bulls from the frequency counting, we avoid double-counting entirely.
      wrong_approach: "Count all matching digits, then subtract bulls"
      correct_approach: "Only add to frequency arrays when positions don't match"

    - title: Not Handling Duplicate Digits Correctly
      description: |
        With `secret = "1123"` and `guess = "0111"`, the guess has three `1`s but secret only has two. After one `1` matches as a bull, only *one* more `1` can be a cow.

        Using `min(secret_count[d], guess_count[d])` handles this automatically — if guess has 2 remaining `1`s but secret has only 1, we count just 1 cow.
      wrong_approach: "Count all occurrences of matching digits as cows"
      correct_approach: "Use minimum of frequencies from both strings"

    - title: Using String Contains Instead of Frequency
      description: |
        Checking `if digit in secret` for each guess digit overcounts when duplicates exist. Each digit in secret can only match one digit in guess as a cow.

        For `secret = "1000"` and `guess = "0111"`, using `in` would count all three `1`s as present in secret, but secret only has one `1` to match.
      wrong_approach: "Check if each guess digit exists in secret string"
      correct_approach: "Use frequency arrays to track available matches"

  key_takeaways:
    - "**Frequency counting** is essential when matching with duplicates — use `min(count_a, count_b)` to avoid overcounting"
    - "**Separate exact matches first**: Process bulls before cows to avoid double-counting positions"
    - "**Arrays vs Hash Maps**: For digit-only problems (0-9), a fixed-size array of 10 is cleaner and faster than a hash map"
    - "This pattern extends to problems like **word matching** and **anagram detection** where position and frequency both matter"

  time_complexity: "O(n). We iterate through both strings once to count bulls and populate frequency arrays, then iterate through 10 digits to sum cows."
  space_complexity: "O(1). We use two arrays of fixed size 10 (for digits 0-9), which doesn't grow with input size."

solutions:
  - approach_name: Single Pass with Frequency Arrays
    is_optimal: true
    code: |
      def get_hint(secret: str, guess: str) -> str:
          bulls = 0
          cows = 0
          # Frequency arrays for digits 0-9
          secret_count = [0] * 10
          guess_count = [0] * 10

          # Single pass: count bulls and build frequency arrays
          for s, g in zip(secret, guess):
              if s == g:
                  # Exact match - it's a bull
                  bulls += 1
              else:
                  # Not a match - add to frequency counts
                  secret_count[int(s)] += 1
                  guess_count[int(g)] += 1

          # Count cows: for each digit, take minimum of both frequencies
          for i in range(10):
              cows += min(secret_count[i], guess_count[i])

          return f"{bulls}A{cows}B"
    explanation: |
      **Time Complexity:** O(n) — One pass through the strings plus O(10) = O(1) for summing cows.

      **Space Complexity:** O(1) — Two fixed-size arrays of 10 elements each.

      We identify bulls in a single pass while simultaneously building frequency counts for non-bull digits. Then we calculate cows by taking the minimum frequency for each digit — this ensures we only count as many matches as both strings can provide.

  - approach_name: Two Pass with Hash Maps
    is_optimal: false
    code: |
      from collections import Counter

      def get_hint(secret: str, guess: str) -> str:
          bulls = 0

          # First pass: count bulls and collect non-bull characters
          secret_remaining = []
          guess_remaining = []

          for s, g in zip(secret, guess):
              if s == g:
                  bulls += 1
              else:
                  secret_remaining.append(s)
                  guess_remaining.append(g)

          # Second pass: count cows using Counter intersection
          secret_counter = Counter(secret_remaining)
          guess_counter = Counter(guess_remaining)

          # Sum of minimum counts for each digit
          cows = sum((secret_counter & guess_counter).values())

          return f"{bulls}A{cows}B"
    explanation: |
      **Time Complexity:** O(n) — Two passes through the data.

      **Space Complexity:** O(n) — Storing remaining characters in lists.

      This approach is more intuitive but less efficient. We first separate bulls from non-bulls, then use Python's Counter intersection (`&`) to find common elements with their minimum counts. The Counter intersection automatically handles the "take minimum" logic.

  - approach_name: One Pass Optimised
    is_optimal: true
    code: |
      def get_hint(secret: str, guess: str) -> str:
          bulls = 0
          cows = 0
          counts = [0] * 10  # Combined count: positive = secret, negative = guess

          for s, g in zip(secret, guess):
              if s == g:
                  bulls += 1
              else:
                  s_digit = int(s)
                  g_digit = int(g)

                  # If secret digit was previously seen in guess, it's a cow
                  if counts[s_digit] < 0:
                      cows += 1
                  # If guess digit was previously seen in secret, it's a cow
                  if counts[g_digit] > 0:
                      cows += 1

                  # Update counts: secret adds, guess subtracts
                  counts[s_digit] += 1
                  counts[g_digit] -= 1

          return f"{bulls}A{cows}B"
    explanation: |
      **Time Complexity:** O(n) — Single pass with no second loop.

      **Space Complexity:** O(1) — Single array of 10 elements.

      This clever variant uses a single array where positive values represent unmatched secret digits and negative values represent unmatched guess digits. When we see a secret digit that was previously in guess (negative count) or a guess digit that was previously in secret (positive count), we've found a cow match. This avoids the second loop entirely.
