Skip to content

Commit 0c133c1

Browse files
committed
LeetCode 299. Bulls and Cows
1 parent 46cb81f commit 0c133c1

File tree

2 files changed

+120
-0
lines changed

2 files changed

+120
-0
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ Proposed solutions to some LeetCode problems. The first column links to the prob
7171
| [240. Search a 2D Matrix II][lc240] | 🟠 Medium | [![python](res/py.png)][lc240py] |
7272
| [242. Valid Anagram][lc242] | 🟢 Easy | [![python](res/py.png)](leetcode/valid-anagram.py) |
7373
| [278. First Bad Version][lc278] | 🟢 Easy | [![python](res/py.png)](leetcode/first-bad-version.py) |
74+
| [299. Bulls and Cows][lc299] | 🟠 Medium | [![python](res/py.png)][lc299py] |
7475
| [303. Range Sum Query - Immutable][lc303] | 🟢 Easy | [![python](res/py.png)][lc303py] |
7576
| [304. Range Sum Query 2D - Immutable][lc304] | 🟠 Medium | [![python](res/py.png)][lc304py] |
7677
| [315. Count of Smaller Numbers After Self][lc315] | 🔴 Hard | [![python](res/py.png)][lc315py] |
@@ -227,6 +228,8 @@ First column is the problem difficulty, in descending order, second links to the
227228
[lc238py]: leetcode/search-a-2d-matrix-ii.py
228229
[lc242]: https://leetcode.com/problems/valid-anagram/
229230
[lc278]: https://leetcode.com/problems/first-bad-version/
231+
[lc290]: https://leetcode.com/problems/bulls-and-cows/
232+
[lc290]: leetcode/bulls-and-cows.py
230233
[lc303]: https://leetcode.com/problems/range-sum-query-immutable/
231234
[lc303py]: leetcode/range-sum-query-immutable.py
232235
[lc304]: https://leetcode.com/problems/range-sum-query-2d-immutable/

leetcode/bulls-and-cows.py

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# 299. Bulls and Cows
2+
# 🟠 Medium
3+
#
4+
# https://leetcode.com/problems/bulls-and-cows/
5+
#
6+
# Tags: Hash Table - String - Counting
7+
8+
import timeit
9+
from collections import Counter, defaultdict
10+
11+
12+
# Iterate over the length of the input strings checking if the characters match. If they match, add to the "bulls"
13+
# if they don't match, store them in one of two dictionaries, seen in the guess or seen in the secret.
14+
# Loop through one of the dictionaries checking which characters, and in which frequency, are also in the other
15+
# dictionary. For each match in character, add 1 to the cow count.
16+
#
17+
# Time complexity: O(n) - We iterate over the input once, then once over the dictionary of non-matched digits.
18+
# Space complexity: O(n) - The dictionary could grow to size n.
19+
#
20+
# Runtime: 71 ms, faster than 39.09% of Python3 online submissions for Bulls and Cows.
21+
# Memory Usage: 13.8 MB, less than 98.91% of Python3 online submissions for Bulls and Cows.
22+
class LoopCheck:
23+
def getHint(self, secret: str, guess: str) -> str:
24+
# Store the count of both types of matches
25+
bulls, cows, ds, dg = 0, 0, defaultdict(int), defaultdict(int)
26+
# Iterate over the characters in guess and secret checking if they match.
27+
for i, c in enumerate(secret):
28+
if c == guess[i]:
29+
bulls += 1
30+
else:
31+
# Update the dictionaries with the characters at this position.
32+
ds[c] += 1
33+
dg[guess[i]] += 1
34+
35+
# Compare the dictionaries.
36+
for c in dg:
37+
if c in ds:
38+
while dg[c] and ds[c]:
39+
cows += 1
40+
dg[c] -= 1
41+
ds[c] -= 1
42+
43+
return f"{bulls}A{cows}B"
44+
45+
46+
# Similar to the above solution but, instead of checking matches with a nested loop, we use the fact that the
47+
# default dictionary will return 0 for non-existing indexes and check the matches using the min() function.
48+
#
49+
# Time complexity: O(n) - We iterate over the input once, then once over the dictionary of non-matched digits.
50+
# Space complexity: O(n) - The dictionary could grow to size n.
51+
#
52+
# Runtime: 50 ms, faster than 78.05% of Python3 online submissions for Bulls and Cows.
53+
# Memory Usage: 13.9 MB, less than 31.00% of Python3 online submissions for Bulls and Cows.
54+
class MinCheck:
55+
def getHint(self, secret: str, guess: str) -> str:
56+
# Store the count of both types of matches
57+
bulls, cows, ds, dg = 0, 0, defaultdict(int), defaultdict(int)
58+
# Iterate over the characters in guess and secret checking if they match.
59+
for i, c in enumerate(secret):
60+
if c == guess[i]:
61+
bulls += 1
62+
else:
63+
# Update the dictionaries with the characters at this position.
64+
ds[c] += 1
65+
dg[guess[i]] += 1
66+
67+
# Compare the dictionaries.
68+
for c in dg:
69+
cows += min(dg[c], ds[c])
70+
71+
return f"{bulls}A{cows}B"
72+
73+
74+
# When some tasks can be performed by built-in functions, they tend to be more performant, even though the theoretical
75+
# work load is bigger. For example, using counter and sum, we iterate more times over the inputs but, the fact that
76+
# the code being called is C, makes the solution faster.
77+
#
78+
# Time complexity: O(n) - We iterate over the input once, then once over the dictionary of non-matched digits.
79+
# Space complexity: O(n) - The dictionary could grow to size n.
80+
#
81+
# Runtime: 33 ms, faster than 99.13% of Python3 online submissions for Bulls and Cows.
82+
# Memory Usage: 13.8 MB, less than 78.40% of Python3 online submissions for Bulls and Cows.
83+
class BuiltInFn:
84+
def getHint(self, secret: str, guess: str) -> str:
85+
# Use Counter to create two dictionaries, like in the previous solutions.
86+
dict_secret, dict_guess = Counter(secret), Counter(guess)
87+
# Use zip to find bulls, positions where the digit in secret and guess are the same.
88+
bulls = sum(i == j for i, j in zip(secret, guess))
89+
# We have bulls, cows are matches in the dictionaries minus bulls.
90+
return "%sA%sB" % (bulls, sum((dict_secret & dict_guess).values()) - bulls)
91+
92+
93+
def test():
94+
executors = [LoopCheck, MinCheck, BuiltInFn]
95+
tests = [
96+
["1807", "7810", "1A3B"],
97+
["1123", "0111", "1A1B"],
98+
["112233445566778899123456789", "223344556677889912345678911", "0A27B"],
99+
["112233445566778899123456789", "122334455667788991234567891", "9A18B"],
100+
]
101+
for executor in executors:
102+
start = timeit.default_timer()
103+
for _ in range(int(float("1e4"))):
104+
for col, t in enumerate(tests):
105+
sol = executor()
106+
result = sol.getHint(t[0], t[1])
107+
exp = t[2]
108+
assert (
109+
result == exp
110+
), f"\033[93m» {result} <> {exp}\033[91m for test {col} using \033[1m{executor.__name__}"
111+
stop = timeit.default_timer()
112+
used = str(round(stop - start, 5))
113+
res = "{0:20}{1:10}{2:10}".format(executor.__name__, used, "seconds")
114+
print(f"\033[92m» {res}\033[0m")
115+
116+
117+
test()

0 commit comments

Comments
 (0)