822. Card Flipping Game
Problem Description
You have n
cards, each with two sides - a front and a back. The i-th
card has a positive integer fronts[i]
on the front side and backs[i]
on the back side.
Initially, all cards are placed on a table with their front sides facing up (visible) and back sides facing down (hidden). You can flip any number of cards (including zero cards) to swap which side is facing up.
After flipping some cards, an integer is considered good if:
- It appears on at least one card's face-down side (hidden side)
- It does NOT appear on any card's face-up side (visible side)
Your task is to find the minimum possible good integer after optimally flipping the cards. If no good integer exists, return 0
.
Example walkthrough:
- If a card has the same number on both front and back (like
fronts[i] = backs[i] = 5
), then5
can never be a good integer. No matter how you flip this card,5
will always be visible on one side. - For cards with different numbers on front and back, you can choose which number to hide by flipping the card appropriately.
The solution identifies all numbers that appear on both sides of the same card (these can never be good), then finds the minimum number from all card values that isn't in this "impossible" set.
Intuition
Let's think about when a number can be "good". For a number to be good, we need to be able to hide it from all face-up positions while keeping it on at least one face-down position.
The key insight comes from considering individual cards:
-
Cards with different numbers on front and back: These cards give us flexibility. If we have a card with
fronts[i] = 3
andbacks[i] = 5
, we can choose to show either 3 or 5 by flipping the card. This means we can potentially hide one of these numbers while showing the other. -
Cards with the same number on both sides: This is the critical case. If
fronts[i] = backs[i] = 7
, then no matter how we flip this card, the number 7 will always be visible. There's no way to hide it from the face-up side. Therefore, 7 can never be a good number.
This observation leads us to the solution strategy:
- First, identify all "impossible" numbers - those that appear on both sides of at least one card. These numbers will always be visible no matter how we arrange the cards.
- Then, from all the numbers that appear on any card (front or back), find the smallest one that isn't "impossible".
Why does this work? Because for any number that isn't in the "impossible" set, we know it only appears on cards where the front and back are different. For each such card, we can flip it to hide this number on the face-down side while showing a different number on the face-up side.
The elegance of this solution is that we don't need to track which specific cards to flip - we just need to know which numbers are possible candidates for being "good", and then pick the minimum among them.
Solution Approach
The implementation follows directly from our intuition using a hash table approach:
Step 1: Identify impossible numbers
We create a hash set s
to store all numbers that can never be "good". We iterate through all positions and check if fronts[i] == backs[i]
. If they're equal, we add that number to our set:
s = {a for a, b in zip(fronts, backs) if a == b}
This set comprehension zips the fronts
and backs
arrays together and adds any number a
where the front and back values are identical.
Step 2: Find the minimum valid number
We need to check all numbers that appear on any card (both fronts and backs). Using chain(fronts, backs)
, we create an iterator that goes through all values from both arrays.
For each number x
in this combined list:
- If
x
is not in sets
, it's a candidate for being "good" - We want the minimum among all such candidates
return min((x for x in chain(fronts, backs) if x not in s), default=0)
The generator expression (x for x in chain(fronts, backs) if x not in s)
produces all numbers that aren't in the "impossible" set. The min()
function finds the smallest such number.
Edge case handling:
If no valid number exists (all numbers appear on both sides of at least one card), the generator will be empty. The default=0
parameter handles this case by returning 0
when there are no good integers.
Time Complexity: O(n)
where n
is the number of cards, as we iterate through the arrays a constant number of times.
Space Complexity: O(n)
in the worst case for the hash set if all cards have the same number on both sides.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with 3 cards:
- Card 1: front = 1, back = 2
- Card 2: front = 3, back = 3
- Card 3: front = 2, back = 4
Step 1: Identify impossible numbers
We check each card to find numbers that appear on both sides:
- Card 1: front (1) ≠ back (2) → No impossible numbers
- Card 2: front (3) = back (3) → 3 is impossible (can never be hidden)
- Card 3: front (2) ≠ back (4) → No impossible numbers
Impossible set = {3}
Step 2: Find minimum valid number
All numbers appearing on cards: [1, 2, 3, 3, 2, 4] = {1, 2, 3, 4}
Check each number:
- 1: Not in impossible set → Valid candidate ✓
- 2: Not in impossible set → Valid candidate ✓
- 3: In impossible set → Cannot be good ✗
- 4: Not in impossible set → Valid candidate ✓
Minimum valid number = 1
Verification: We can achieve this by:
- Keep Card 1 as is (showing front = 1, hiding back = 2)
- Keep Card 2 as is (doesn't matter since 3 is always visible)
- Flip Card 3 (showing back = 4, hiding front = 2)
Result: Face-up shows [1, 3, 4], Face-down has [2, 3, 2]
- Number 2 is hidden on face-down sides but not visible on any face-up side → 2 is good
- Number 1 is visible on face-up side → 1 is not good
- Actually, we need to flip Card 1 to hide 1!
Let me reconsider:
- Flip Card 1 (showing back = 2, hiding front = 1)
- Keep Card 2 (showing 3)
- Keep Card 3 (showing front = 2, hiding back = 4)
Result: Face-up shows [2, 3, 2], Face-down has [1, 3, 4]
- Number 1 appears on face-down side only → 1 is good ✓
- Number 4 appears on face-down side only → 4 is good ✓
The minimum good integer is 1.
Solution Implementation
1from typing import List
2from itertools import chain
3
4class Solution:
5 def flipgame(self, fronts: List[int], backs: List[int]) -> int:
6 # Find all numbers that appear on both sides of the same card
7 # These numbers cannot be the answer since flipping won't help
8 same_on_both_sides = {front for front, back in zip(fronts, backs) if front == back}
9
10 # Find the minimum number from all cards (both fronts and backs)
11 # that is not in the set of numbers appearing on both sides
12 # If no valid number exists, return 0 as default
13 return min((number for number in chain(fronts, backs)
14 if number not in same_on_both_sides), default=0)
15
1class Solution {
2 public int flipgame(int[] fronts, int[] backs) {
3 // Set to store numbers that appear on both sides of the same card
4 // These numbers are "bad" because no matter how we flip, they'll always be visible
5 Set<Integer> sameOnBothSides = new HashSet<>();
6
7 // Get the number of cards
8 int numberOfCards = fronts.length;
9
10 // Find all numbers that appear on both front and back of the same card
11 // These cannot be our answer since they'll always be face-up
12 for (int i = 0; i < numberOfCards; i++) {
13 if (fronts[i] == backs[i]) {
14 sameOnBothSides.add(fronts[i]);
15 }
16 }
17
18 // Initialize minimum value to a large number (max possible value based on constraints)
19 int minGoodNumber = 9999;
20
21 // Check all numbers on the front of cards
22 // If a number doesn't appear on both sides of any card, it's a candidate
23 for (int value : fronts) {
24 if (!sameOnBothSides.contains(value)) {
25 minGoodNumber = Math.min(minGoodNumber, value);
26 }
27 }
28
29 // Check all numbers on the back of cards
30 // If a number doesn't appear on both sides of any card, it's a candidate
31 for (int value : backs) {
32 if (!sameOnBothSides.contains(value)) {
33 minGoodNumber = Math.min(minGoodNumber, value);
34 }
35 }
36
37 // Return the minimum good number found, or 0 if no good number exists
38 // (when minGoodNumber remains 9999, modulo operation returns 0)
39 return minGoodNumber % 9999;
40 }
41}
42
1class Solution {
2public:
3 int flipgame(vector<int>& fronts, vector<int>& backs) {
4 // Store numbers that appear on both sides of the same card
5 // These numbers cannot be the answer since they'll always be visible
6 unordered_set<int> sameNumbers;
7 int n = fronts.size();
8
9 // Find all cards where front and back have the same number
10 for (int i = 0; i < n; ++i) {
11 if (fronts[i] == backs[i]) {
12 sameNumbers.insert(fronts[i]);
13 }
14 }
15
16 // Initialize answer with a large value (representing infinity)
17 int minValue = 9999;
18
19 // Check all front values for potential minimum
20 for (const int& value : fronts) {
21 if (!sameNumbers.count(value)) {
22 minValue = min(minValue, value);
23 }
24 }
25
26 // Check all back values for potential minimum
27 for (const int& value : backs) {
28 if (!sameNumbers.count(value)) {
29 minValue = min(minValue, value);
30 }
31 }
32
33 // Return the minimum value found, or 0 if no valid number exists
34 // (when minValue remains 9999, modulo operation returns 0)
35 return minValue % 9999;
36 }
37};
38
1/**
2 * Finds the minimum number that can be shown after flipping cards.
3 * Cards with the same number on front and back cannot be used as the answer.
4 *
5 * @param fronts - Array of numbers on the front of each card
6 * @param backs - Array of numbers on the back of each card
7 * @returns The minimum valid number, or 0 if no valid number exists
8 */
9function flipgame(fronts: number[], backs: number[]): number {
10 // Set to store numbers that appear on both sides of the same card
11 // These numbers are invalid as they'll always be visible
12 const invalidNumbers: Set<number> = new Set<number>();
13
14 const cardCount: number = fronts.length;
15
16 // Find all numbers that appear on both sides of the same card
17 for (let i = 0; i < cardCount; i++) {
18 if (fronts[i] === backs[i]) {
19 invalidNumbers.add(fronts[i]);
20 }
21 }
22
23 // Initialize minimum answer with a large value
24 let minAnswer: number = 9999;
25
26 // Check all front numbers for the minimum valid number
27 for (const frontNumber of fronts) {
28 if (!invalidNumbers.has(frontNumber)) {
29 minAnswer = Math.min(minAnswer, frontNumber);
30 }
31 }
32
33 // Check all back numbers for the minimum valid number
34 for (const backNumber of backs) {
35 if (!invalidNumbers.has(backNumber)) {
36 minAnswer = Math.min(minAnswer, backNumber);
37 }
38 }
39
40 // Return 0 if no valid number found (minAnswer still 9999), otherwise return minAnswer
41 return minAnswer % 9999;
42}
43
Time and Space Complexity
Time Complexity: O(n)
The algorithm performs the following operations:
- Creating set
s
by iterating throughfronts
andbacks
withzip()
:O(n)
time - The set comprehension checks condition
a == b
for each pair:O(1)
per element chain(fronts, backs)
creates an iterator over2n
elements- The generator expression iterates through at most
2n
elements - For each element, checking membership in set
s
takesO(1)
average time - Finding the minimum of the filtered elements takes
O(n)
in total
Overall time complexity: O(n) + O(2n) = O(n)
Space Complexity: O(n)
The space usage includes:
- Set
s
stores at mostn
unique values (worst case when all pairs have matching front and back):O(n)
space - The generator expression and
chain()
useO(1)
additional space as they create iterators - The
min()
function processes elements one at a time without storing them all
Overall space complexity: O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Misunderstanding What Makes a Number "Good"
The Mistake: Many people initially think that a number is good if it appears on ANY back side and doesn't appear on ANY front side. This leads to incorrect solutions like:
# WRONG approach
def flipgame(self, fronts, backs):
front_set = set(fronts)
back_set = set(backs)
# Find numbers only in backs but not in fronts
good_numbers = back_set - front_set
return min(good_numbers) if good_numbers else 0
Why It's Wrong: The problem allows you to flip cards! After flipping, what was on the back becomes visible (face-up), and what was on the front becomes hidden (face-down). A number can be good even if it initially appears on the front, as long as you can flip cards to hide all occurrences of it.
The Fix: Understand that you have control over which side is up for each card, except when both sides have the same number.
Pitfall 2: Forgetting About Cards with Identical Sides
The Mistake: Trying to track which specific cards to flip without recognizing the fundamental constraint:
# WRONG approach - overly complex and misses the key insight
def flipgame(self, fronts, backs):
min_good = float('inf')
for target in set(fronts + backs):
can_hide_all = True
for i in range(len(fronts)):
if fronts[i] == target and backs[i] == target:
can_hide_all = False
break
# Trying to figure out complex flipping logic...
if can_hide_all:
min_good = min(min_good, target)
return min_good if min_good != float('inf') else 0
Why It's Wrong: This approach overcomplicates the problem. The key insight is that if a number appears on both sides of the same card, it's impossible to make it good. For all other numbers, you can always arrange the cards to make them good.
The Fix: Simply identify numbers that appear on both sides of the same card - these are the only numbers that cannot be good. All other numbers that appear on any card are valid candidates.
Pitfall 3: Not Considering All Numbers on Cards
The Mistake: Only checking numbers from the fronts array or only from the backs array:
# WRONG approach
def flipgame(self, fronts, backs):
same = {a for a, b in zip(fronts, backs) if a == b}
# Only checking fronts array - missing potential answers from backs
return min((x for x in fronts if x not in same), default=0)
Why It's Wrong: A good number could be any number that appears on any card (either front or back), as long as it doesn't appear on both sides of the same card.
The Fix: Check all numbers from both fronts
and backs
arrays when finding the minimum valid number, which is why the correct solution uses chain(fronts, backs)
.
Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!