|
| 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