2868. The Wording Game π
Problem Description
Alice and Bob are playing a word game with two lexicographically sorted arrays of strings, a
and b
, respectively.
The game follows these rules:
- Players take turns playing words from their respective arrays
- On each turn, a player must play a word that is closely greater than the previously played word
- If a player cannot play a valid word on their turn, they lose
- Alice starts the game by playing her lexicographically smallest word (first word in array
a
)
A word w
is closely greater than word z
if:
w
is lexicographically greater thanz
(comes afterz
in dictionary order)- The first letter of
w
must either:- Be equal to the first letter of
z
, OR - Be exactly one letter after the first letter of
z
in the alphabet
- Be equal to the first letter of
For example:
"care"
is closely greater than"book"
(first letters:c
comes afterb
by 1)"care"
is closely greater than"car"
(same first letterc
, and"care"
>"car"
)"care"
is NOT closely greater than"ant"
(first letters:c
is 2 letters aftera
)"care"
is NOT closely greater than"cook"
("care"
is not lexicographically greater than"cook"
)
The function should return true
if Alice can win the game when both players play optimally, and false
otherwise.
The solution simulates the game using indices to track each player's current position in their array. Variable k
alternates between 0 (Alice's turn) and 1 (Bob's turn), starting with Bob since Alice already played the first word. The algorithm checks if the current player can play any remaining word from their array that satisfies the "closely greater" condition. If a valid word is found, it becomes the new reference word and the turn switches. If a player exhausts their array without finding a valid word while it's still their turn, they lose.
Intuition
The key insight is that both players play optimally, which means each player will always play the earliest valid word from their sorted array when it's their turn. Why? Because playing words early preserves more options for future turns - if you skip a valid word now, you might not be able to use it later when the requirements become stricter.
Since both arrays are already lexicographically sorted, we can use a greedy approach with two pointers. For each player, we maintain a pointer that moves through their array from start to end, checking each word to see if it can be played.
The "closely greater" constraint limits which words can follow the current word - the next word must start with either the same letter or the very next letter in the alphabet. This means we don't need complex backtracking or dynamic programming; we can simply scan forward through each player's remaining words until we find one that satisfies the constraint.
The simulation approach naturally emerges from this observation:
- Start with Alice playing her first word (smallest in her array)
- Alternate turns between Bob and Alice
- On each turn, scan the current player's remaining words (using their pointer) to find the first valid word
- If found, play it and switch turns; if not, keep scanning
- The game ends when one player runs out of valid words to play
The winner is determined by who runs out of valid moves first. If Bob exhausts all his words while it's his turn to play (meaning he has no valid move), Alice wins. Conversely, if Alice exhausts all her words while it's her turn, Bob wins.
This greedy simulation works because of the optimal play assumption - each player will always make the move that gives them the best chance to have future valid moves, which in this sorted scenario means playing the earliest valid word available.
Learn more about Greedy, Math and Two Pointers patterns.
Solution Approach
The implementation uses a simulation approach with pointer tracking for both players' arrays.
Variables Setup:
i = 1
: Alice's pointer, starting at index 1 (since she already playeda[0]
)j = 0
: Bob's pointer, starting at index 0 (he hasn't played yet)k = 1
: Turn indicator (0 for Alice, 1 for Bob), starting with 1 since Alice already playedw = a[0]
: The current word in play, initialized with Alice's first word
Main Simulation Loop:
The algorithm runs an infinite loop that alternates between players based on k
:
When it's Bob's turn (k = 1
):
- First check if
j == len(b)
- if Bob has exhausted all his words without finding a valid move, Alice wins (returnTrue
) - Check if
b[j]
is a valid move:- Condition 1:
b[j][0] == w[0] and b[j] > w
(same first letter AND lexicographically greater) - Condition 2:
ord(b[j][0]) - ord(w[0]) == 1
(first letter is exactly one position ahead in alphabet)
- Condition 1:
- If either condition is met:
- Update
w = b[j]
(this becomes the new current word) - Toggle turn with
k ^= 1
(XOR with 1 flips between 0 and 1)
- Update
- Always increment
j += 1
to move to the next word in Bob's array
When it's Alice's turn (k = 0
):
- First check if
i == len(a)
- if Alice has exhausted all her words without finding a valid move, Bob wins (returnFalse
) - Check if
a[i]
is a valid move using the same "closely greater" conditions:- Condition 1:
a[i][0] == w[0] and a[i] > w
- Condition 2:
ord(a[i][0]) - ord(w[0]) == 1
- Condition 1:
- If either condition is met:
- Update
w = a[i]
- Toggle turn with
k ^= 1
- Update
- Always increment
i += 1
to move to the next word in Alice's array
Key Implementation Details:
- The
ord()
function converts a character to its ASCII value, allowing us to check if letters are consecutive (e.g.,ord('c') - ord('b') == 1
) - The XOR operation
k ^= 1
efficiently toggles between 0 and 1 for turn switching - The pointer increment happens regardless of whether a valid word was found, ensuring we scan through all possibilities
- The game termination conditions are checked at the beginning of each player's turn - if they've exhausted their array, their opponent wins
This greedy approach works because both arrays are sorted, and playing the earliest valid word is always optimal since it preserves the maximum number of future options.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through a small example to understand how the solution works.
Given:
- Alice's array
a = ["apple", "berry", "cherry"]
- Bob's array
b = ["banana", "carrot", "date"]
Step 1: Initial Setup
- Alice plays her first word:
w = "apple"
- Set pointers:
i = 1
(Alice's next word),j = 0
(Bob starts at beginning) - Turn indicator:
k = 1
(Bob's turn)
Step 2: Bob's Turn (k = 1)
- Check if Bob has exhausted his array:
j = 0 < 3
, so continue - Check if
b[0] = "banana"
is closely greater than"apple"
:- First letters:
'b'
and'a'
- Is
'b' == 'a'
? No - Is
ord('b') - ord('a') == 1
? Yes! (98 - 97 = 1) - Is
"banana" > "apple"
? Yes
- First letters:
- Valid move found! Update
w = "banana"
, togglek = 0
, incrementj = 1
Step 3: Alice's Turn (k = 0)
- Check if Alice has exhausted her array:
i = 1 < 3
, so continue - Check if
a[1] = "berry"
is closely greater than"banana"
:- First letters: both
'b'
- Is
'b' == 'b'
? Yes - Is
"berry" > "banana"
? Yes
- First letters: both
- Valid move found! Update
w = "berry"
, togglek = 1
, incrementi = 2
Step 4: Bob's Turn (k = 1)
- Check if Bob has exhausted his array:
j = 1 < 3
, so continue - Check if
b[1] = "carrot"
is closely greater than"berry"
:- First letters:
'c'
and'b'
- Is
'c' == 'b'
? No - Is
ord('c') - ord('b') == 1
? Yes! (99 - 98 = 1) - Is
"carrot" > "berry"
? Yes
- First letters:
- Valid move found! Update
w = "carrot"
, togglek = 0
, incrementj = 2
Step 5: Alice's Turn (k = 0)
- Check if Alice has exhausted her array:
i = 2 < 3
, so continue - Check if
a[2] = "cherry"
is closely greater than"carrot"
:- First letters: both
'c'
- Is
'c' == 'c'
? Yes - Is
"cherry" > "carrot"
? Yes
- First letters: both
- Valid move found! Update
w = "cherry"
, togglek = 1
, incrementi = 3
Step 6: Bob's Turn (k = 1)
- Check if Bob has exhausted his array:
j = 2 < 3
, so continue - Check if
b[2] = "date"
is closely greater than"cherry"
:- First letters:
'd'
and'c'
- Is
'd' == 'c'
? No - Is
ord('d') - ord('c') == 1
? Yes! (100 - 99 = 1) - Is
"date" > "cherry"
? Yes
- First letters:
- Valid move found! Update
w = "date"
, togglek = 0
, incrementj = 3
Step 7: Alice's Turn (k = 0)
- Check if Alice has exhausted her array:
i = 3 == 3
, Alice has no more words! - Since Alice cannot play on her turn, Bob wins
- Return
False
The game flow was: "apple"
β "banana"
β "berry"
β "carrot"
β "cherry"
β "date"
, and Alice ran out of valid moves first, so Bob wins.
Solution Implementation
1class Solution:
2 def canAliceWin(self, a: List[str], b: List[str]) -> bool:
3 # Initialize pointers and game state
4 alice_index = 1 # Start from second word in Alice's list (first word already played)
5 bob_index = 0 # Start from first word in Bob's list
6 alice_turn = True # Track whose turn it is (True = Alice, False = Bob)
7 current_word = a[0] # Alice plays the first word
8
9 # Game simulation loop
10 while True:
11 if alice_turn:
12 # Bob's turn to play
13 if bob_index == len(b):
14 # Bob has no more words to play, Alice wins
15 return True
16
17 # Check if Bob can play the current word
18 # Valid move: same starting letter but lexicographically greater,
19 # or starting letter is exactly one letter after current word's starting letter
20 if (b[bob_index][0] == current_word[0] and b[bob_index] > current_word) or \
21 ord(b[bob_index][0]) - ord(current_word[0]) == 1:
22 current_word = b[bob_index]
23 alice_turn = not alice_turn # Switch turns
24
25 bob_index += 1
26
27 else:
28 # Alice's turn to play
29 if alice_index == len(a):
30 # Alice has no more words to play, Alice loses
31 return False
32
33 # Check if Alice can play the current word
34 # Valid move: same starting letter but lexicographically greater,
35 # or starting letter is exactly one letter after current word's starting letter
36 if (a[alice_index][0] == current_word[0] and a[alice_index] > current_word) or \
37 ord(a[alice_index][0]) - ord(current_word[0]) == 1:
38 current_word = a[alice_index]
39 alice_turn = not alice_turn # Switch turns
40
41 alice_index += 1
42
1class Solution {
2 public boolean canAliceWin(String[] aliceWords, String[] bobWords) {
3 // Initialize indices for Alice and Bob's word arrays
4 int aliceIndex = 1; // Start from index 1 since Alice plays first word at index 0
5 int bobIndex = 0; // Bob starts from index 0
6
7 // Track whose turn it is (true = Alice's turn, false = Bob's turn)
8 boolean isAliceTurn = true;
9
10 // Current word in play (Alice starts with her first word)
11 String currentWord = aliceWords[0];
12
13 // Game simulation loop
14 while (true) {
15 if (isAliceTurn) {
16 // Alice's turn - she needs to play a word from Bob's array
17
18 // Check if Bob has no more words (Alice wins)
19 if (bobIndex == bobWords.length) {
20 return true;
21 }
22
23 // Check if Bob's current word is valid to play
24 // Valid if: same starting letter AND lexicographically greater
25 // OR: starting letter is exactly one more than current word
26 if ((bobWords[bobIndex].charAt(0) == currentWord.charAt(0) &&
27 currentWord.compareTo(bobWords[bobIndex]) < 0) ||
28 bobWords[bobIndex].charAt(0) - currentWord.charAt(0) == 1) {
29
30 // Play Bob's word and switch turns
31 currentWord = bobWords[bobIndex];
32 isAliceTurn = !isAliceTurn;
33 }
34
35 // Move to next word in Bob's array
36 ++bobIndex;
37
38 } else {
39 // Bob's turn - he needs to play a word from Alice's array
40
41 // Check if Alice has no more words (Bob wins, so Alice loses)
42 if (aliceIndex == aliceWords.length) {
43 return false;
44 }
45
46 // Check if Alice's current word is valid to play
47 // Valid if: same starting letter AND lexicographically greater
48 // OR: starting letter is exactly one more than current word
49 if ((aliceWords[aliceIndex].charAt(0) == currentWord.charAt(0) &&
50 currentWord.compareTo(aliceWords[aliceIndex]) < 0) ||
51 aliceWords[aliceIndex].charAt(0) - currentWord.charAt(0) == 1) {
52
53 // Play Alice's word and switch turns
54 currentWord = aliceWords[aliceIndex];
55 isAliceTurn = !isAliceTurn;
56 }
57
58 // Move to next word in Alice's array
59 ++aliceIndex;
60 }
61 }
62 }
63}
64
1class Solution {
2public:
3 bool canAliceWin(vector<string>& a, vector<string>& b) {
4 // Initialize indices for Alice's and Bob's word arrays
5 int aliceIndex = 1; // Start from index 1 since Alice already played index 0
6 int bobIndex = 0; // Bob starts from index 0
7
8 // Track whose turn it is (1 = Bob's turn, 0 = Alice's turn)
9 int isBobTurn = 1;
10
11 // Current word in play (Alice starts with her first word)
12 string currentWord = a[0];
13
14 // Game simulation loop
15 while (true) {
16 if (isBobTurn) {
17 // Bob's turn
18
19 // If Bob has no more words to play, Alice wins
20 if (bobIndex == b.size()) {
21 return true;
22 }
23
24 // Check if Bob can play the current word
25 // Valid move: same starting letter but lexicographically greater
26 // OR starting letter is exactly one more than current word's starting letter
27 if ((b[bobIndex][0] == currentWord[0] && currentWord < b[bobIndex]) ||
28 b[bobIndex][0] - currentWord[0] == 1) {
29
30 // Bob makes a valid move
31 currentWord = b[bobIndex];
32 isBobTurn ^= 1; // Switch turn to Alice
33 }
34
35 // Move to Bob's next word
36 ++bobIndex;
37
38 } else {
39 // Alice's turn
40
41 // If Alice has no more words to play, she loses
42 if (aliceIndex == a.size()) {
43 return false;
44 }
45
46 // Check if Alice can play the current word
47 // Valid move: same starting letter but lexicographically greater
48 // OR starting letter is exactly one more than current word's starting letter
49 if ((a[aliceIndex][0] == currentWord[0] && currentWord < a[aliceIndex]) ||
50 a[aliceIndex][0] - currentWord[0] == 1) {
51
52 // Alice makes a valid move
53 currentWord = a[aliceIndex];
54 isBobTurn ^= 1; // Switch turn to Bob
55 }
56
57 // Move to Alice's next word
58 ++aliceIndex;
59 }
60 }
61 }
62};
63
1/**
2 * Determines if Alice can win the word chain game
3 * @param aliceWords - Array of words available to Alice
4 * @param bobWords - Array of words available to Bob
5 * @returns true if Alice can win, false otherwise
6 */
7function canAliceWin(aliceWords: string[], bobWords: string[]): boolean {
8 // Initialize indices and turn indicator
9 let aliceIndex: number = 1; // Start from second word (first already used)
10 let bobIndex: number = 0; // Bob starts from first word
11 let isAliceTurn: number = 1; // 1 = Alice's turn, 0 = Bob's turn
12
13 // Current word in play (Alice starts with her first word)
14 let currentWord: string = aliceWords[0];
15
16 // Game loop - continue until someone cannot make a valid move
17 while (true) {
18 if (isAliceTurn) {
19 // Alice's turn - she needs to find a valid word from Bob's list
20
21 // Check if Bob has no more words to play
22 if (bobIndex === bobWords.length) {
23 return true; // Alice wins as Bob cannot make a move
24 }
25
26 // Check if Bob's current word is valid
27 // Valid if: same first letter AND lexicographically greater, OR first letter is next in alphabet
28 if ((bobWords[bobIndex][0] === currentWord[0] && currentWord < bobWords[bobIndex]) ||
29 bobWords[bobIndex].charCodeAt(0) - currentWord.charCodeAt(0) === 1) {
30
31 // Valid move found - update current word and switch turns
32 currentWord = bobWords[bobIndex];
33 isAliceTurn ^= 1; // Toggle turn using XOR
34 }
35
36 // Move to next word in Bob's list
37 ++bobIndex;
38
39 } else {
40 // Bob's turn - he needs to find a valid word from Alice's list
41
42 // Check if Alice has no more words to play
43 if (aliceIndex === aliceWords.length) {
44 return false; // Bob wins as Alice cannot make a move
45 }
46
47 // Check if Alice's current word is valid
48 // Valid if: same first letter AND lexicographically greater, OR first letter is next in alphabet
49 if ((aliceWords[aliceIndex][0] === currentWord[0] && currentWord < aliceWords[aliceIndex]) ||
50 aliceWords[aliceIndex].charCodeAt(0) - currentWord.charCodeAt(0) === 1) {
51
52 // Valid move found - update current word and switch turns
53 currentWord = aliceWords[aliceIndex];
54 isAliceTurn ^= 1; // Toggle turn using XOR
55 }
56
57 // Move to next word in Alice's list
58 ++aliceIndex;
59 }
60 }
61}
62
Time and Space Complexity
Time Complexity: O(m + n)
, where m
is the length of array a
and n
is the length of array b
.
The algorithm uses two pointers i
and j
to traverse through arrays a
and b
respectively. In each iteration of the while loop, either i
or j
is incremented by 1. The loop continues until either:
j
reaches the end of arrayb
(Alice wins), ori
reaches the end of arraya
(Bob wins)
Since each element in both arrays is visited at most once, and the pointers only move forward, the total number of iterations is bounded by m + n
. Therefore, the time complexity is O(m + n)
.
Space Complexity: O(1)
The algorithm only uses a constant amount of extra space:
- Three integer variables:
i
,j
, andk
for indexing and turn tracking - One string variable
w
to store the current word
These variables do not depend on the input size, so the space complexity is constant O(1)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Turn Tracking Logic
The most critical pitfall in this solution is the confusing turn tracking system. The code uses alice_turn = True
to indicate whose turn it is, but the logic is inverted:
- When
alice_turn = True
, the code processes Bob's turn - When
alice_turn = False
, the code processes Alice's turn
This is counterintuitive and can lead to bugs when modifying or debugging the code.
Solution: Rename the variable to accurately reflect its purpose, or fix the logic:
# Option 1: Rename variable to match actual behavior is_bobs_turn = True # Start with Bob since Alice already played while True: if is_bobs_turn: # Bob's turn logic ... else: # Alice's turn logic ...
# Option 2: Fix the logic to match variable name alice_turn = False # False because Alice already played, now it's Bob's turn while True: if not alice_turn: # Bob's turn # Bob's turn logic ... else: # Alice's turn # Alice's turn logic ...
2. Missing Validation for "Closely Greater" Condition
The code doesn't validate that when checking ord(b[bob_index][0]) - ord(current_word[0]) == 1
, the word is also lexicographically greater. A word starting with the next letter might still be lexicographically smaller (e.g., "ba" is not greater than "bz" even though 'b' comes after 'a').
Solution: Add explicit lexicographic comparison:
if (b[bob_index][0] == current_word[0] and b[bob_index] > current_word) or \
(ord(b[bob_index][0]) - ord(current_word[0]) == 1 and b[bob_index] > current_word):
# Valid move
3. Inefficient Array Scanning
The code increments indices even when no valid word is found, potentially scanning through many invalid words repeatedly. This doesn't affect correctness but impacts performance.
Solution: Use binary search to find the next valid word more efficiently:
def find_next_valid_word(words, start_idx, current_word):
# Binary search for the first word that is closely greater
left, right = start_idx, len(words)
result_idx = -1
while left < right:
mid = (left + right) // 2
if is_closely_greater(words[mid], current_word):
result_idx = mid
right = mid # Look for earlier valid word
else:
left = mid + 1
return result_idx
4. Edge Case: Empty Arrays
The code assumes array a
has at least one element (accessing a[0]
directly). If either array is empty, the code will crash.
Solution: Add validation at the beginning:
if not a or not b: return False # Or handle according to game rules
What's the output of running the following function using the following tree as input?
1def serialize(root):
2 res = []
3 def dfs(root):
4 if not root:
5 res.append('x')
6 return
7 res.append(root.val)
8 dfs(root.left)
9 dfs(root.right)
10 dfs(root)
11 return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4 StringJoiner res = new StringJoiner(" ");
5 serializeDFS(root, res);
6 return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10 if (root == null) {
11 result.add("x");
12 return;
13 }
14 result.add(Integer.toString(root.val));
15 serializeDFS(root.left, result);
16 serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2 let res = [];
3 serialize_dfs(root, res);
4 return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8 if (!root) {
9 res.push("x");
10 return;
11 }
12 res.push(root.val);
13 serialize_dfs(root.left, res);
14 serialize_dfs(root.right, res);
15}
16
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Want a Structured Path to Master System Design Too? Donβt Miss This!