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 function_signature: "def get_hint(secret: str, guess: str) -> str:" test_cases: visible: - input: { secret: "1807", guess: "7810" } expected: "1A3B" - input: { secret: "1123", guess: "0111" } expected: "1A1B" hidden: - input: { secret: "1", guess: "0" } expected: "0A0B" - input: { secret: "1", guess: "1" } expected: "1A0B" - input: { secret: "1111", guess: "1111" } expected: "4A0B" - input: { secret: "1234", guess: "4321" } expected: "0A4B" - input: { secret: "1122", guess: "2211" } expected: "0A4B" - input: { secret: "0000", guess: "1111" } expected: "0A0B" - input: { secret: "1234", guess: "0000" } expected: "0A0B" 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   **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`   **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   **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.