3320. Count The Number of Winning Sequences
Problem Description
Alice and Bob are engaged in a fantasy battle game, which unfolds over n
rounds. In each round, they summon one of three magical creatures: a Fire Dragon, a Water Serpent, or an Earth Golem. The creatures have specific interactions with each other:
- A Fire Dragon beats an Earth Golem.
- A Water Serpent beats a Fire Dragon.
- An Earth Golem beats a Water Serpent.
- If both players summon the same creature, it's a tie with no points awarded.
Given Alice's sequence of moves as a string s
with characters 'F' for Fire Dragon, 'W' for Water Serpent, and 'E' for Earth Golem, Bob needs to choose his moves. It's guaranteed that Bob won't summon the same creature in consecutive rounds. Bob wins if his total points exceed Alice's after all rounds. The task is to determine how many sequences Bob can use to beat Alice, with the final answer modulo (10^9 + 7).
Intuition
To solve the problem, we utilize a recursive approach with memoization. The function dfs(i, j, k)
is defined as follows:
i
is the index in strings
where the current round starts.j
is the score difference between Bob and Alice.k
is the last summoned creature by Bob.
The goal is to determine how many sequences of moves Bob can make that results in a win over Alice. We begin the exploration with dfs(0, 0, -1)
, where -1
signifies Bob hasn't summoned any creatures yet.
The recursive strategy involves:
- Checking if remaining rounds (
n - i
) are inadequate for altering the score difference such that it's in Bob's favor (i.e.,j
being positive). - Ending recursion when all rounds are complete (
i >= n
), returning1
if Bob's score is greater, otherwise returning0
. - Iterating over possible creatures Bob can summon, ensuring no repetition of creatures in consecutive rounds, and employing the helper function
calc(x, y)
to update the score based on outcomes of the summoned creatures.
This solution efficiently maps characters to integers and calculates possible winning sequences using a depth-first search, systematically summing them while applying modulo (10^9 + 7) to handle large numbers.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution uses a memoization search strategy, combining recursion and caching to explore all potential sequences of Bob's moves efficiently. Here's a breakdown of the implementation:
-
DFS Function Definition: We define a function
dfs(i, j, k)
to recursively determine the number of winning sequences Bob can employ:i
represents the current index in Alice's move sequences
.j
is the score difference between Bob and Alice (positive if Bob leads).k
is the last creature Bob summoned.
-
Base Conditions:
- If the remaining rounds aren't sufficient for Bob to overtake Alice (
n - i <= j
), return0
. - If all rounds are concluded (
i >= n
), check if Bob's score leads (j < 0
). Return1
if true, otherwise0
.
- If the remaining rounds aren't sufficient for Bob to overtake Alice (
-
Recursion with Choices:
- Each recursion step considers all three creatures Bob can summon (
0
,1
,2
, corresponding to "F", "W", "E"), skipping the previous creature to ensure no consecutive repeats. - Use the helper function
calc(x, y)
to compute points based on the interaction of Alice's and Bob's choices.
- Each recursion step considers all three creatures Bob can summon (
-
Score Calculation:
- The helper
calc(x, y)
returns the point effect based on Bob's and Alice's summoned creatures. Specifically:- Win situations:
(0, 2)
,(1, 0)
,(2, 1)
return1
. - Loss situations are the reverse of win cases.
- Ties return
0
.
- Win situations:
- The helper
-
Caching for Optimization:
- Use
@cache
to memoize results ofdfs(i, j, k)
, preventing redundant calculations and improving efficiency.
- Use
-
Modulo Operation:
- Since potential outcomes can be extensive, results are taken modulo (10^9 + 7).
This solution effectively evaluates all possible arrangements of Bob's moves, using recursive exploration with memoization to achieve the desired result within computational limits.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's consider a small example to illustrate the solution approach. Assume Alice's sequence of moves is "FWE", which represents 3 rounds where Alice plays:
- Round 1: Fire Dragon ('F')
- Round 2: Water Serpent ('W')
- Round 3: Earth Golem ('E')
The objective for Bob is to choose his moves in such a way that he wins more rounds than Alice, while ensuring no creature is summoned consecutively.
Step-by-Step Process:
-
Initialize Variables:
- Start with
dfs(0, 0, -1)
, indicating we begin at the first round, with a score difference of zero, and no prior creature summoned by Bob.
- Start with
-
Round 1 Choices:
- Alice plays 'F'.
- Bob considers three possibilities: 'W', 'E', 'F'. Based on the rule that Bob can't repeat creatures, we note:
- If Bob chooses 'W': Bob beats Alice, so the score difference becomes +1 (Bob is leading).
- If Bob chooses 'E': Bob is defeated by Alice, the score difference becomes -1 (Alice is leading).
- If Bob chooses 'F': It's a tie, score difference remains 0.
-
Round 2 Choices:
- Move on to
dfs(1, score_difference, last_choice)
. - Alice plays 'W'.
- If Bob's previous choice was 'W', he must choose between 'F' or 'E':
- 'E' beats 'W': Bob gains a point, score difference increases by 1.
- 'F' loses to 'W': Bob loses a point, score decreases by 1.
- If Bob’s previous choice was 'E' or 'F', adjust accordingly while ensuring no repeat of the immediate prior choice.
- Move on to
-
Round 3 Choices:
- Process
dfs(2, score_difference, last_choice)
. - Alice plays 'E'.
- Again, make sure not to repeat Bob’s Round 2 choice:
- If Bob chooses 'F', Bob wins the round.
- If Bob chooses 'W', Bob loses the round.
- Previous choice-aware adjustments continue here.
- Process
-
Evaluate Final Outcome:
- At the end of all rounds (
i = n
), if Bob's score is greater than Alice’s (score_difference > 0
), count this sequence as a valid win for Bob. - Sum all valid sequences modulo (10^9 + 7) to handle large results.
- At the end of all rounds (
Through recursive exploration, we evaluate each sequence of Bob’s moves, tracking scores and respecting non-repetition rules, using memoization to enhance computation efficiency. This walkthrough demonstrates the approach to finding all possible winning move sequences for Bob given Alice's predefined moves.
Solution Implementation
1class Solution:
2 def countWinningSequences(self, s: str) -> int:
3 from functools import cache # Import cache for memoization
4
5 # Calculate the result of a match between move x and move y
6 def calc(move1: int, move2: int) -> int:
7 if move1 == move2:
8 return 0 # Draw
9 if move1 < move2:
10 return 1 if move1 == 0 and move2 == 2 else -1
11 return -1 if move1 == 2 and move2 == 0 else 1
12
13 @cache
14 def dfs(index: int, score: int, lastMove: int) -> int:
15 # If not enough characters are left to make a difference, return 0
16 if len(s) - index <= score:
17 return 0
18 if index >= len(s):
19 return int(score < 0) # Convert Boolean to int (1 if True, 0 if False)
20
21 result = 0
22 # Iterate over possible moves (0, 1, 2)
23 for currentMove in range(3):
24 # Continue if current move is same as last move
25 if currentMove == lastMove:
26 continue
27 # Update result using DFS with recalculated score and current move
28 result = (result + dfs(index + 1, score + calc(d[s[index]], currentMove), currentMove)) % mod
29 return result
30
31 mod = 10**9 + 7 # Modulo value to prevent integer overflow
32 d = {"F": 0, "W": 1, "E": 2} # Dictionary to map character to moves
33 answer = dfs(0, 0, -1) # Start DFS from index 0, initial score 0, and no last move
34 dfs.cache_clear() # Clear the cache after calculation
35 return answer
36
1class Solution {
2 private int n; // Length of the string
3 private char[] s; // Array representation of input string
4 private int[] d = new int[26]; // Array to map 'W', 'E', 'I' to numbers
5 private Integer[][][] memo; // Memoization table for dynamic programming
6 private final int MOD = 1000000007; // Modulo constant for large numbers
7
8 public int countWinningSequences(String s) {
9 d['W' - 'A'] = 1; // Map 'W' to 1
10 d['E' - 'A'] = 2; // Map 'E' to 2
11 this.s = s.toCharArray(); // Convert input string to char array
12 n = this.s.length; // Set n as the length of the input string
13 memo = new Integer[n][n + n + 1][4]; // Initialize memoization table
14 return dfs(0, n, 3); // Start DFS from index 0 with j = n and k = 3
15 }
16
17 private int dfs(int i, int j, int k) {
18 // Check termination condition for invalid state
19 if (n - i <= j - n) {
20 return 0;
21 }
22 // Check if reached the end of string
23 if (i >= n) {
24 // If valid sequence, return 1, otherwise 0
25 return j - n < 0 ? 1 : 0;
26 }
27 // Return already computed result from memoization table
28 if (memo[i][j][k] != null) {
29 return memo[i][j][k];
30 }
31
32 int ans = 0; // Initialize answer for current state
33 // Iterating over all possible next states l
34 for (int l = 0; l < 3; ++l) {
35 if (l == k) { // Ensure transition is to a different state
36 continue;
37 }
38 // Recursively calculate the result for the new state and accumulate answers
39 ans = (ans + dfs(i + 1, j + calc(d[s[i] - 'A'], l), l)) % MOD;
40 }
41 // Store the computed result in memoization table
42 return memo[i][j][k] = ans;
43 }
44
45 private int calc(int x, int y) {
46 if (x == y) {
47 return 0; // No change needed if states are the same
48 }
49 if (x < y) {
50 return x == 0 && y == 2 ? 1 : -1; // Special rule for moving 0 to 2
51 }
52 return x == 2 && y == 0 ? -1 : 1; // Special rule for moving 2 to 0
53 }
54}
55
1class Solution {
2public:
3 int countWinningSequences(string s) {
4 int n = s.size(); // Length of the string
5 int directions[26]{}; // Array to store the direction mapping for 'W' and 'E'
6
7 // Map 'W' to 1 and 'E' to 2
8 directions['W' - 'A'] = 1;
9 directions['E' - 'A'] = 2;
10
11 // 3D memoization array initialized to -1 for dynamic programming
12 int memo[n][n + n + 1][4];
13 memset(memo, -1, sizeof(memo));
14
15 // Helper function to calculate score between two elements
16 auto calculateScore = [](int x, int y) -> int {
17 if (x == y) {
18 return 0; // Equal elements result in zero score
19 }
20 // Determine score based on conditions
21 if (x < y) {
22 return (x == 0 && y == 2) ? 1 : -1;
23 }
24 return (x == 2 && y == 0) ? -1 : 1;
25 };
26
27 const int MOD = 1e9 + 7; // Modulo constant for preventing overflow
28
29 // Depth-first search function using lambda
30 auto dfs = [&](auto&& self, int i, int j, int k) -> int {
31 // If the balance (j - n) exceeds unmatched elements, return 0
32 if (n - i <= j - n) {
33 return 0;
34 }
35 // Base case: if we've processed all elements, return whether balance is negative
36 if (i >= n) {
37 return (j - n < 0) ? 1 : 0;
38 }
39 // Return already computed value if available
40 if (memo[i][j][k] != -1) {
41 return memo[i][j][k];
42 }
43 int ans = 0; // Initialize answer to 0
44 // Try each possible move
45 for (int l = 0; l < 3; ++l) {
46 if (l == k) continue; // Skip if move is same as previous (to avoid repetition)
47 // Accumulate results of valid subsequences
48 ans = (ans + self(self, i + 1, j + calculateScore(directions[s[i] - 'A'], l), l)) % MOD;
49 }
50 // Memoize result before returning
51 return memo[i][j][k] = ans;
52 };
53
54 // Start DFS with initial parameters
55 return dfs(dfs, 0, n, 3);
56 }
57};
58
1let countWinningSequences = function(s: string): number {
2 const n = s.length; // Length of the string
3 const directions: number[] = new Array(26).fill(0); // Array to store the direction mapping for 'W' and 'E'
4
5 // Map 'W' to 1 and 'E' to 2
6 directions['W'.charCodeAt(0) - 'A'.charCodeAt(0)] = 1;
7 directions['E'.charCodeAt(0) - 'A'.charCodeAt(0)] = 2;
8
9 // 3D memoization array initialized to -1 for dynamic programming
10 const memo: number[][][] = Array.from({length: n}, () => Array.from({length: n + n + 1}, () => new Array(4).fill(-1)));
11
12 // Helper function to calculate score between two elements
13 const calculateScore = (x: number, y: number): number => {
14 if (x === y) {
15 return 0; // Equal elements result in zero score
16 }
17 // Determine score based on conditions
18 if (x < y) {
19 return (x === 0 && y === 2) ? 1 : -1;
20 }
21 return (x === 2 && y === 0) ? -1 : 1;
22 };
23
24 const MOD = 1e9 + 7; // Modulo constant for preventing overflow
25
26 // Depth-first search function
27 const dfs = (i: number, j: number, k: number): number => {
28 // If the balance (j - n) exceeds unmatched elements, return 0
29 if (n - i <= j - n) {
30 return 0;
31 }
32 // Base case: if we've processed all elements, return whether balance is negative
33 if (i >= n) {
34 return (j - n < 0) ? 1 : 0;
35 }
36 // Return already computed value if available
37 if (memo[i][j][k] !== -1) {
38 return memo[i][j][k];
39 }
40 let ans = 0; // Initialize answer to 0
41 // Try each possible move
42 for (let l = 0; l < 3; ++l) {
43 if (l === k) continue; // Skip if move is the same as previous (to avoid repetition)
44 // Accumulate results of valid subsequences
45 ans = (ans + dfs(i + 1, j + calculateScore(directions[s.charCodeAt(i) - 'A'.charCodeAt(0)], l), l)) % MOD;
46 }
47 // Memoize result before returning
48 return memo[i][j][k] = ans;
49 };
50
51 // Start DFS with initial parameters
52 return dfs(0, n, 3);
53};
54
Time and Space Complexity
The function dfs
is called recursively with three parameters: i
, j
, and k
, where i
is the current index, j
represents a cumulative score, and k
is the last move. The recursion explores all possible sequences of moves.
Time Complexity
The function's complexity arises from the potential number of calls made due to different combinations of the parameters. The potential states are determined by:
- The length of the string
s
, denoted asn
. - The range of cumulative scores
j
, which depends on the length and can go up toO(n)
. - The possible moves represented by
k
, which can be one of 3 values: 0, 1, or 2.
Thus, the number of possible states is O(n) * O(n) * O(k)
, where k
is the size of the character set (3 different moves). Therefore, the time complexity is O(n^2 * k)
.
Space Complexity
The space complexity is due to the cache used for memoization and the recursion stack:
- The memoization cache stores results for combinations of
(i, j, k)
. This results in a space requirement ofO(n^2 * k)
. - The recursion depth is
O(n)
, contributing an additional space factor.
Thus, the overall space complexity is O(n^2 * k)
.
Learn more about how to find time and space complexity quickly.
What's the relationship between a tree and a graph?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
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 algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!