240 lines
11 KiB
YAML
240 lines
11 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:
|
|
- slug: two-pointers
|
|
is_optimal: true
|
|
|
|
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.
|