LeetCode 3664 - Two-Letter Card Game
Each card consists of exactly two lowercase letters. A card is only relevant if it contains the given letter x somewhere in its two positions. During the game, we repeatedly select two cards that satisfy two conditions: 1. Both cards contain the letter x. 2.
Difficulty: 🟡 Medium
Topics: Array, Hash Table, String, Counting, Enumeration
Solution
Problem Understanding
Each card consists of exactly two lowercase letters. A card is only relevant if it contains the given letter x somewhere in its two positions.
During the game, we repeatedly select two cards that satisfy two conditions:
- Both cards contain the letter
x. - The two strings differ in exactly one position.
After selecting such a pair, both cards are removed and we gain one point.
The goal is to maximize the total number of points.
Since every card has length exactly two, compatibility is very restrictive. Two cards are compatible if they share one position and differ in the other. For example:
"ab"and"ac"are compatible because only the second character differs."aa"and"ba"are compatible because only the first character differs."ab"and"ba"are not compatible because both positions differ.
The input size can be as large as 100000 cards, so any solution that tries to explicitly search for pairs among cards is far too slow.
An important observation comes from the constraint that every character is between 'a' and 'j'. Since there are only 10 possible letters and every card has length 2, there are only:
$$10 \times 10 = 100$$
possible card types.
This means that although there may be up to 100000 cards, the number of distinct card patterns is tiny.
Important Edge Cases
A naive implementation can easily mishandle several situations.
If no card contains x, the answer is obviously 0.
If only one card contains x, no pair can be formed.
If all relevant cards are identical, no pair can be formed because identical cards differ in zero positions.
If many copies of "xx" exist, they may be paired with cards from either side of the compatibility graph, and choosing those pairings optimally is the key challenge.
Approaches
Brute Force
A straightforward idea is to repeatedly search for two compatible cards containing x, remove them, and continue.
One implementation would examine every pair of remaining cards, find a compatible pair, remove it, and repeat until no more pairs exist.
This approach is correct because it explicitly simulates the game. However, it is completely infeasible.
With up to 100000 cards, even checking all pairs once requires:
$$O(n^2)$$
operations, which is far beyond the allowed limits.
Key Insight
Only cards containing x matter.
Because cards have length 2, every relevant card must be one of the following forms:
(x, a)(a, x)
for some letter a.
There are only 19 such card types:
"xx""xa"for the other 9 letters"ax"for the other 9 letters
Let:
A= cards of the form"x?"B= cards of the form"?x""xx"belongs to both groups
Inside group A, every two different card types are compatible.
Inside group B, every two different card types are compatible.
The only compatibility between the two groups occurs through "xx".
This dramatically reduces the problem. Instead of dealing with up to 100000 cards individually, we only need to count how many copies of each of the 19 relevant card types exist.
The remaining optimization becomes a one-dimensional problem involving how many "xx" cards remain unused by cross-group pairings.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) or worse | O(n) | Explicitly searches for removable pairs |
| Optimal | O(n) | O(1) | Counts card types and optimizes over a constant-sized state space |
Algorithm Walkthrough
Step 1
Count every card type that contains the character x.
For cards of the form "x?" (excluding "xx"), store their frequencies in group A.
For cards of the form "?x" (excluding "xx"), store their frequencies in group B.
Store the frequency of "xx" separately.
Step 2
For each group, compute:
S= total number of cards in the groupM= largest frequency among its card types
For a complete compatibility clique, the maximum number of pairs obtainable from counts with an additional r copies of "xx" is:
$$f(S,M,r) = \min\left( \left\lfloor\frac{S+r}{2}\right\rfloor, (S+r)-\max(M,r) \right)$$
The first term comes from the fact that every pair consumes two cards.
The second term comes from the fact that no pair may use two copies of the same card type.
Step 3
Suppose exactly r copies of "xx" remain after cross-group pairings.
Then:
c - rcopies of"xx"were used in cross-group pairings- each such usage directly contributes one point
The total score becomes:
$$(c-r) + f_A(r) + f_B(r)$$
where c is the total count of "xx" cards.
Step 4
The feasible range of r is:
$$\max(0, c-(S_A+S_B)) \le r \le c$$
because at most S_A + S_B copies of "xx" can participate in cross-group pairings.
Step 5
The function above is piecewise linear with only a constant number of breakpoints.
For each group, breakpoints occur at:
$$2M-S+1$$
and
$$S$$
Combining both groups yields only a constant number of intervals.
Within one interval, the score is linear on each parity of r, therefore the maximum must occur at an interval boundary.
We evaluate only a constant-sized set of candidate values near all interval boundaries and take the maximum score.
Why it works
The entire compatibility structure collapses into two cliques connected only through "xx" cards.
Once the number of remaining "xx" cards is fixed, the optimal score inside each clique is determined uniquely by the formula for maximum pairing in a complete compatibility graph.
Therefore the problem reduces to optimizing a single variable r, and the score function has only a constant number of structural changes. Evaluating all boundary candidates guarantees that the global optimum is found.
Python Solution
from typing import List
from collections import Counter
class Solution:
def score(self, cards: List[str], x: str) -> int:
cnt = Counter(cards)
xx = x + x
center = cnt[xx]
a_counts = []
b_counts = []
for ch in "abcdefghij":
if ch == x:
continue
a_counts.append(cnt[x + ch])
b_counts.append(cnt[ch + x])
sa = sum(a_counts)
sb = sum(b_counts)
ma = max(a_counts, default=0)
mb = max(b_counts, default=0)
def clique_pairs(S: int, M: int, r: int) -> int:
total = S + r
return min(total // 2, total - max(M, r))
low = max(0, center - (sa + sb))
boundaries = {
low,
center + 1,
2 * ma - sa + 1,
sa,
2 * mb - sb + 1,
sb,
}
points = set()
sorted_bounds = sorted(boundaries)
for i in range(len(sorted_bounds) - 1):
left = max(low, sorted_bounds[i])
right = min(center, sorted_bounds[i + 1] - 1)
if left > right:
continue
for r in (left, left + 1, right - 1, right):
if left <= r <= right:
points.add(r)
best = 0
for r in points:
score = (
(center - r)
+ clique_pairs(sa, ma, r)
+ clique_pairs(sb, mb, r)
)
best = max(best, score)
return best
Implementation Explanation
The first part counts all card types. Only cards containing x are relevant.
The frequencies are split into three categories:
"xx""x?""?x"
The helper function clique_pairs() computes the maximum number of compatible pairs obtainable inside one clique when r copies of "xx" belong to that clique.
The optimization variable is r, the number of "xx" cards not used in cross-group pairings.
Instead of checking every possible r, we exploit the fact that the score function changes behavior only at a constant number of breakpoints. We enumerate interval boundaries, evaluate the score near each boundary, and keep the maximum.
Since the number of card types is fixed, all computations after counting are constant time.
Go Solution
package main
func score(cards []string, x byte) int {
cnt := make(map[string]int)
for _, card := range cards {
cnt[card]++
}
xx := string([]byte{x, x})
center := cnt[xx]
aCounts := make([]int, 0, 9)
bCounts := make([]int, 0, 9)
for ch := byte('a'); ch <= 'j'; ch++ {
if ch == x {
continue
}
aCounts = append(aCounts, cnt[string([]byte{x, ch})])
bCounts = append(bCounts, cnt[string([]byte{ch, x})])
}
sa, sb := 0, 0
ma, mb := 0, 0
for _, v := range aCounts {
sa += v
if v > ma {
ma = v
}
}
for _, v := range bCounts {
sb += v
if v > mb {
mb = v
}
}
cliquePairs := func(S, M, r int) int {
total := S + r
a := total / 2
b := total - max(M, r)
if a < b {
return a
}
return b
}
low := max(0, center-(sa+sb))
boundaries := map[int]struct{}{
low: {},
center + 1: {},
2*ma - sa + 1: {},
sa: {},
2*mb - sb + 1: {},
sb: {},
}
var bounds []int
for v := range boundaries {
bounds = append(bounds, v)
}
for i := 0; i < len(bounds); i++ {
for j := i + 1; j < len(bounds); j++ {
if bounds[j] < bounds[i] {
bounds[i], bounds[j] = bounds[j], bounds[i]
}
}
}
points := make(map[int]struct{})
for i := 0; i+1 < len(bounds); i++ {
left := max(low, bounds[i])
right := min(center, bounds[i+1]-1)
if left > right {
continue
}
candidates := []int{left, left + 1, right - 1, right}
for _, r := range candidates {
if r >= left && r <= right {
points[r] = struct{}{}
}
}
}
best := 0
for r := range points {
cur := (center - r) +
cliquePairs(sa, ma, r) +
cliquePairs(sb, mb, r)
if cur > best {
best = cur
}
}
return best
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
Go-Specific Notes
The Go solution follows exactly the same logic as the Python version.
A map[string]int is used for frequency counting. Since there are only 100 possible card types, memory usage remains constant.
All arithmetic fits comfortably within Go's int type because the answer can never exceed cards.length / 2.
Worked Examples
Example 1
Input:
cards = ["aa","ab","ba","ac"]
x = "a"
Frequency summary:
| Type | Count |
|---|---|
| aa | 1 |
| ab | 1 |
| ac | 1 |
| ba | 1 |
Values:
| Variable | Value |
|---|---|
| center ("aa") | 1 |
| SA | 2 |
| SB | 1 |
| MA | 1 |
| MB | 1 |
Try r = 0:
- cross pairs = 1
- clique A contributes 1
- clique B contributes 0
Total:
1 + 1 + 0 = 2
Answer:
2
Example 2
Input:
cards = ["aa","ab","ba"]
x = "a"
Counts:
| Type | Count |
|---|---|
| aa | 1 |
| ab | 1 |
| ba | 1 |
Values:
| Variable | Value |
|---|---|
| center | 1 |
| SA | 1 |
| SB | 1 |
Using the single "aa" as a cross-group pairing yields:
1 point
No additional pairs remain.
Answer:
1
Example 3
Input:
cards = ["aa","ab","ba","ac"]
x = "b"
Relevant cards:
| Type | Count |
|---|---|
| ab | 1 |
| ba | 1 |
These two cards differ in both positions.
No compatible pair exists.
Answer:
0
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | One pass to count card frequencies |
| Space | O(1) | At most 100 distinct card types |
The counting phase dominates the runtime. After counting, only a constant number of card types and candidate states are examined, so the remaining work is constant.
Test Cases
sol = Solution()
assert sol.score(["aa", "ab", "ba", "ac"], "a") == 2 # Example 1
assert sol.score(["aa", "ab", "ba"], "a") == 1 # Example 2
assert sol.score(["aa", "ab", "ba", "ac"], "b") == 0 # Example 3
assert sol.score(["aa", "aa"], "a") == 0 # identical cards only
assert sol.score(["ab", "ac"], "a") == 1 # direct compatible pair
assert sol.score(["ab", "ba"], "a") == 0 # differ in two positions
assert sol.score(["aa", "ab", "ac"], "a") == 1 # one center card helps pairing
assert sol.score(["aa", "aa", "ab", "ba"], "a") == 2 # multiple uses of center cards
assert sol.score(["bc", "cd"], "a") == 0 # no relevant cards
assert sol.score(["ab"], "a") == 0 # single relevant card
assert sol.score(
["aa"] * 50000 + ["ab"] * 25000 + ["ba"] * 25000,
"a"
) == 50000 # large stress case
Test Summary
| Test | Why |
|---|---|
| Example 1 | Basic optimal pairing |
| Example 2 | One removable pair |
| Example 3 | Relevant cards exist but are incompatible |
["aa","aa"] |
Identical cards cannot pair |
["ab","ac"] |
Simple compatible pair |
["ab","ba"] |
Differ in both positions |
["aa","ab","ac"] |
Center card participates strategically |
Multiple "aa" cards |
Tests cross-group usage |
| No relevant cards | Answer should be zero |
| Single relevant card | Cannot form a pair |
| Large stress case | Validates scalability |
Edge Cases
All Relevant Cards Are Identical
Consider:
["aa", "aa", "aa"]
Every card is the same. Compatibility requires differing in exactly one position, but identical cards differ in zero positions. A buggy solution might incorrectly pair equal cards simply because they contain x.
The implementation avoids this because the clique formula never pairs cards of the same type together.
No Card Contains x
Consider:
cards = ["bc", "cd", "de"]
x = "a"
There are no usable cards at all. The answer must be zero.
The counting phase naturally produces zero frequencies for every relevant type, and the optimization returns zero.
Large Number of "xx" Cards
Consider:
["aa", "aa", "aa", "ab", "ba"]
The "aa" cards can interact with both sides of the compatibility structure. Greedily assigning them to one side may lose optimal pairings.
The solution explicitly models how many "xx" cards remain after cross-group usage and optimizes over all structurally distinct possibilities, guaranteeing the maximum score.
One Dominant Card Type
Consider:
["ab", "ab", "ab", "ab", "ac"]
Although there are five cards, only one pair can be formed because compatible pairs must involve different card types.
The clique formula correctly captures this limitation through the term:
$$(S+r)-\max(M,r)$$
which prevents overcounting when one type dominates the frequency distribution.