Files
codetutor/backend/data/questions/bulls-and-cows.yaml

215 lines
10 KiB
YAML

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.