3850. Count Sequences to K
Problem Description
You are given an integer array nums and an integer k.
Start with an initial value val = 1 and process the elements of nums from left to right. At each index i, you must choose exactly one of the following three actions:
- Multiply
valbynums[i]. - Divide
valbynums[i]. - Leave
valunchanged.
The key detail here is that division is treated as exact rational division, not integer division. This means val is kept as a precise fraction at all times. For example, 2 / 4 equals 1 / 2, not 0.
After processing all elements of nums, the value val is considered equal to k only if its final rational value exactly equals k.
Your task is to return the count of distinct sequences of choices that result in val == k.
A sequence of choices is defined by the action taken at each index. Since there are three possible actions at every index, there are 3^n total possible sequences (where n is the length of nums). You need to count how many of these sequences end with val exactly equal to k.
Note: Division is rational (exact), not integer division. For example, 2 / 4 = 1 / 2.
How We Pick the Algorithm
Why Dynamic Programming?
This problem maps to Dynamic Programming through a short path in the full flowchart.
Memoized DFS counts sequences of multiply/divide/skip choices that reach exactly k.
Open in FlowchartIntuition
The first thing to notice is that at each index we make an independent choice among three options, and the choices accumulate to form a final value. This naturally suggests exploring all possibilities through recursion, where we process one element at a time and branch into three directions.
A challenge is how to represent val. Since division is exact rational division, val can be a fraction like 1/2 rather than an integer. To track this precisely, we represent val as a pair (p, q), meaning the fraction p / q. This avoids floating-point errors and lets us compare against k exactly.
Now we can define the recursive process. At index i with the current value p / q, we try each of the three actions:
- Leave unchanged: the value stays
p / q. - Multiply by
nums[i]: the value becomes(p * nums[i]) / q. - Divide by
nums[i]: the value becomesp / (q * nums[i]).
When we reach the end (i == n), we check whether the fraction exactly equals k. Since k is an integer, this means p == k and q == 1 (after the fraction is fully reduced). If so, this sequence of choices is valid and contributes 1 to the count.
There are two important refinements that make this work efficiently:
-
Reducing the fraction. If we keep multiplying numerators and denominators without simplifying, the numbers grow huge. After every multiply or divide, we compute the
gcdof the numerator and denominator and divide both by it. This keeps(p, q)in lowest terms, which also gives a canonical form so that equal fractions always look identical. -
Memoization. Different sequences of choices can lead to the same state
(i, p, q). Once the fraction is reduced to its canonical form, identical states recur often. By caching results based on(i, p, q), we avoid recomputing the same subproblems, turning an exponential search into a much faster one.
Putting these ideas together, we get a memoized depth-first search that explores all valid choice sequences while keeping the rational values exact and the state space compact.
Pattern Learn more about Memoization, Math and Dynamic Programming patterns.
Solution Approach
Solution 1: Memoization Search
We define a function dfs(i, p, q) that represents the number of different choice sequences when processing at index i, with the current rational value being p / q. The entry point is dfs(0, 1, 1), which represents starting at index 0 with the initial value of 1 (i.e., the fraction 1 / 1).
For each index i, we have three choices, and we add up the results of all three:
- Keep it unchanged, i.e.,
dfs(i + 1, p, q). - Multiply by
nums[i], i.e., the new value is(p * nums[i]) / q. - Divide by
nums[i], i.e., the new value isp / (q * nums[i]).
To avoid excessively large numbers, we simplify the numerator and denominator after each multiplication or division. Concretely, for the multiply branch, we let x = nums[i], compute g = gcd(p * x, q), and recurse with dfs(i + 1, p * x // g, q // g). For the divide branch, we compute g = gcd(p, q * x) and recurse with dfs(i + 1, p // g, q * x // g). Reducing the fraction to lowest terms after each step ensures the state (p, q) stays in a canonical form, so equal fractions are represented identically and the cache hits more often.
When i equals n, all elements have been processed. We check whether p / q exactly equals k: since k is an integer and the fraction is fully reduced, this holds only when p == k and q == 1. In that case we return 1; otherwise we return 0.
The base case and three recursive branches are combined as follows:
- If
i == n: return1ifp == k and q == 1, else0. - Otherwise:
res = dfs(i + 1, p, q) + dfs(i + 1, p*x//g1, q//g1) + dfs(i + 1, p//g2, q*x//g2).
Data structures and patterns used:
- Recursion (DFS): We branch into three sub-states at every index, exploring all
3^npossible sequences implicitly. - Memoization (
@cache): Many distinct choice paths converge to the same state(i, p, q). By caching the result of each state, repeated subproblems are computed only once, dramatically reducing the work. After computing the answer, we calldfs.cache_clear()to release the cached states. - GCD reduction: Using
gcdto normalize the fraction after each operation keeps numbers small and guarantees a unique representation for each rational value, which is essential for both correctness of the final comparison and effectiveness of the cache.
In summary, the algorithm performs a top-down memoized search over states (i, p, q), where the fraction p / q is always kept in lowest terms, and accumulates the count of choice sequences that end exactly at the value k.
Example Walkthrough
Let's trace through a small example to see how the memoized DFS explores choices and counts valid sequences.
Input: nums = [2, 2], k = 1
We want to count how many of the 3^2 = 9 choice sequences leave val exactly equal to 1. We start with dfs(0, 1, 1), meaning index 0, current value 1/1.
Step 1 — Index 0, state (0, 1, 1), with x = nums[0] = 2:
We branch into three actions:
- Leave unchanged →
dfs(1, 1, 1)(value stays1/1). - Multiply by 2 → new value
2/1. We reduce:g = gcd(2, 1) = 1, sodfs(1, 2, 1). - Divide by 2 → new value
1/2. We reduce:g = gcd(1, 2) = 1, sodfs(1, 1, 2).
Step 2 — Process each child at index 1, with x = nums[1] = 2:
Child A: dfs(1, 1, 1) (value 1/1)
- Leave →
dfs(2, 1, 1)→ at end,p==1, q==1✅ returns 1 - Multiply →
2/1→dfs(2, 2, 1)→p==2 ≠ 1❌ returns 0 - Divide →
1/2→dfs(2, 1, 2)→q==2 ≠ 1❌ returns 0 - Subtotal: 1
Child B: dfs(1, 2, 1) (value 2/1)
- Leave →
dfs(2, 2, 1)→p==2❌ returns 0 - Multiply →
4/1→dfs(2, 4, 1)→ ❌ returns 0 - Divide →
2/2reduced to1/1→dfs(2, 1, 1)→ ✅ returns 1 - Subtotal: 1
Child C: dfs(1, 1, 2) (value 1/2)
- Leave →
dfs(2, 1, 2)→q==2❌ returns 0 - Multiply →
2/2reduced to1/1→dfs(2, 1, 1)→ ✅ returns 1 - Divide →
1/4→dfs(2, 1, 4)→ ❌ returns 0 - Subtotal: 1
Step 3 — Combine results:
dfs(0, 1, 1) = dfs(1,1,1) + dfs(1,2,1) + dfs(1,1,2) = 1 + 1 + 1 = 3
Answer: 3
Verification of the 3 valid sequences (action at index 0, action at index 1):
| Seq | Index 0 | Index 1 | Computation | Final |
|---|---|---|---|---|
| 1 | leave | leave | 1 | 1 ✅ |
| 2 | × 2 | ÷ 2 | 1 × 2 ÷ 2 | 1 ✅ |
| 3 | ÷ 2 | × 2 | 1 ÷ 2 × 2 | 1 ✅ |
Key observations from the trace:
- GCD reduction in action: In Child B, dividing
2/1by2produced2/2, which we reduced to the canonical1/1. Likewise in Child C, multiplying1/2by2gave2/2 → 1/1. Without reduction these would look different from1/1and the final checkp==k and q==1would fail. - Memoization payoff: Notice that the state
dfs(2, 1, 1)is reached from three different paths (Child A's leave branch, Child B's divide branch, and Child C's multiply branch). With@cache, it is computed once and reused, illustrating how distinct choice paths converge to identical states(i, p, q).
Solution Implementation
1class Solution:
2 def countSequences(self, nums: List[int], k: int) -> int:
3 n = len(nums)
4
5 @cache
6 def dfs(index: int, numerator: int, denominator: int) -> int:
7 # Base case: all numbers have been processed.
8 # A valid sequence requires the resulting fraction to equal k / 1.
9 if index == n:
10 return 1 if numerator == k and denominator == 1 else 0
11
12 current = nums[index]
13
14 # Choice 1: skip the current number entirely.
15 count = dfs(index + 1, numerator, denominator)
16
17 # Choice 2: multiply the current number into the numerator,
18 # then reduce the fraction to its lowest terms.
19 divisor = gcd(numerator * current, denominator)
20 count += dfs(
21 index + 1,
22 numerator * current // divisor,
23 denominator // divisor,
24 )
25
26 # Choice 3: multiply the current number into the denominator,
27 # then reduce the fraction to its lowest terms.
28 divisor = gcd(numerator, denominator * current)
29 count += dfs(
30 index + 1,
31 numerator // divisor,
32 denominator * current // divisor,
33 )
34
35 return count
36
37 answer = dfs(0, 1, 1)
38 dfs.cache_clear() # Free the memoization cache to avoid cross-test contamination.
39 return answer
401class Solution {
2
3 /**
4 * Represents a memoization state during the DFS.
5 *
6 * @param index the current index into the nums array
7 * @param numerator the numerator of the running product/quotient (in lowest terms)
8 * @param denominator the denominator of the running product/quotient (in lowest terms)
9 */
10 record State(int index, long numerator, long denominator) {
11 }
12
13 // Memoization table mapping a state to the number of valid sequences from that state
14 private Map<State, Integer> memo;
15 // Input array of numbers
16 private int[] nums;
17 // Length of the nums array
18 private int n;
19 // Target value the final product/quotient must equal
20 private long k;
21
22 /**
23 * Counts the number of sequences (choices of multiply, divide, or skip for each
24 * element) whose resulting fraction equals k (i.e. numerator == k and denominator == 1).
25 *
26 * @param nums the array of numbers to operate on
27 * @param k the target value
28 * @return the number of valid sequences
29 */
30 public int countSequences(int[] nums, long k) {
31 this.nums = nums;
32 this.n = nums.length;
33 this.k = k;
34 this.memo = new HashMap<>();
35 // Start the search from index 0 with fraction 1/1
36 return dfs(0, 1L, 1L);
37 }
38
39 /**
40 * Depth-first search exploring all ways to combine the remaining elements.
41 *
42 * @param index the current index being processed
43 * @param numerator the current numerator (already reduced)
44 * @param denominator the current denominator (already reduced)
45 * @return the number of valid sequences reachable from this state
46 */
47 private int dfs(int index, long numerator, long denominator) {
48 // Base case: all elements processed, check whether the fraction equals k
49 if (index == n) {
50 return (numerator == k && denominator == 1L) ? 1 : 0;
51 }
52
53 // Return the cached result if this state was computed before
54 State key = new State(index, numerator, denominator);
55 if (memo.containsKey(key)) {
56 return memo.get(key);
57 }
58
59 // Option 1: skip the current element, keeping the fraction unchanged
60 int result = dfs(index + 1, numerator, denominator);
61
62 long value = nums[index];
63
64 // Option 2: multiply by the current element, then reduce the fraction
65 long gcdMultiply = gcd(numerator * value, denominator);
66 result += dfs(index + 1, (numerator * value) / gcdMultiply, denominator / gcdMultiply);
67
68 // Option 3: divide by the current element, then reduce the fraction
69 long gcdDivide = gcd(numerator, denominator * value);
70 result += dfs(index + 1, numerator / gcdDivide, (denominator * value) / gcdDivide);
71
72 // Cache and return the total count for this state
73 memo.put(key, result);
74 return result;
75 }
76
77 /**
78 * Computes the greatest common divisor of two non-negative longs using the
79 * iterative Euclidean algorithm.
80 *
81 * @param a the first value
82 * @param b the second value
83 * @return the greatest common divisor of a and b
84 */
85 private long gcd(long a, long b) {
86 while (b != 0) {
87 long remainder = a % b;
88 a = b;
89 b = remainder;
90 }
91 return a;
92 }
93}
941class Solution {
2public:
3 int size; // total number of elements in nums
4 long long target; // the target product k
5 vector<int>* values; // pointer to the input array
6 map<tuple<int, long long, long long>, int> memo; // memoization: (index, numerator, denominator) -> count
7
8 int countSequences(vector<int>& nums, long long k) {
9 this->values = &nums;
10 this->size = nums.size();
11 this->target = k;
12 memo.clear();
13 // Start the DFS at index 0 with product represented as fraction 1/1
14 return dfs(0, 1LL, 1LL);
15 }
16
17 // dfs explores all subsequences, tracking the running product as a reduced fraction p/q.
18 // i: current index; p: numerator; q: denominator.
19 int dfs(int i, long long p, long long q) {
20 // Base case: all elements processed.
21 // A valid sequence has product exactly k (p == target and q == 1).
22 if (i == size) {
23 return (p == target && q == 1LL) ? 1 : 0;
24 }
25
26 // Return cached result if this state was already computed.
27 auto key = make_tuple(i, p, q);
28 if (memo.count(key)) return memo[key];
29
30 // Choice 1: skip the current element.
31 int result = dfs(i + 1, p, q);
32
33 long long x = (*values)[i];
34
35 // Choice 2: multiply the current element into the product (numerator * x),
36 // then reduce the fraction by its gcd to keep p/q in lowest terms.
37 long long gNum = gcd(p * x, q);
38 result += dfs(i + 1, (p * x) / gNum, q / gNum);
39
40 // Choice 3: divide the product by the current element (denominator * x),
41 // then reduce the fraction to keep it in lowest terms.
42 long long gDen = gcd(p, q * x);
43 result += dfs(i + 1, p / gDen, (q * x) / gDen);
44
45 // Cache and return the total count for this state.
46 memo[key] = result;
47 return result;
48 }
49};
501// Memoization cache: maps a state key "i,numerator,denominator" to the count of valid sequences.
2const memo = new Map<string, number>();
3
4// Stores the input array and its length so helper functions can access them globally.
5let inputNums: number[] = [];
6let inputLength = 0;
7let targetK = 0;
8
9/**
10 * Computes the greatest common divisor of two non-negative integers
11 * using the iterative Euclidean algorithm.
12 */
13function gcd(a: number, b: number): number {
14 while (b !== 0) {
15 const remainder = a % b;
16 a = b;
17 b = remainder;
18 }
19 return a;
20}
21
22/**
23 * Depth-first search that explores every element starting from index `i`,
24 * tracking the running product expressed as the reduced fraction
25 * `numerator / denominator`.
26 *
27 * At each element we have three choices:
28 * 1. Skip the element entirely.
29 * 2. Multiply the current value by the element (numerator * x).
30 * 3. Divide the current value by the element (denominator * x).
31 *
32 * A sequence is valid when, after processing all elements, the fraction
33 * equals exactly `targetK` (numerator === k and denominator === 1).
34 */
35function dfs(i: number, numerator: number, denominator: number): number {
36 // Base case: all elements processed.
37 if (i === inputLength) {
38 return numerator === targetK && denominator === 1 ? 1 : 0;
39 }
40
41 // Return the cached result if this state has already been computed.
42 const key = `${i},${numerator},${denominator}`;
43 if (memo.has(key)) {
44 return memo.get(key)!;
45 }
46
47 // Choice 1: skip the current element.
48 let result = dfs(i + 1, numerator, denominator);
49
50 const x = inputNums[i];
51
52 // Choice 2: multiply the fraction by x, then reduce it.
53 const gcdMultiply = gcd(numerator * x, denominator);
54 result += dfs(
55 i + 1,
56 (numerator * x) / gcdMultiply,
57 denominator / gcdMultiply,
58 );
59
60 // Choice 3: divide the fraction by x (multiply denominator), then reduce it.
61 const gcdDivide = gcd(numerator, denominator * x);
62 result += dfs(
63 i + 1,
64 numerator / gcdDivide,
65 (denominator * x) / gcdDivide,
66 );
67
68 // Cache and return the total count for this state.
69 memo.set(key, result);
70 return result;
71}
72
73/**
74 * Counts the number of subsequences whose product (allowing each chosen
75 * element to either multiply or divide the running value) equals `k`.
76 */
77function countSequences(nums: number[], k: number): number {
78 // Initialize global state for the helper functions.
79 inputNums = nums;
80 inputLength = nums.length;
81 targetK = k;
82 memo.clear();
83
84 // Start the search at index 0 with the fraction 1/1.
85 return dfs(0, 1, 1);
86}
87Time and Space Complexity
-
Time complexity:
O(n^4 + log k), wherenis the length of the arraynums.The core of the algorithm is the recursive function
dfs(i, p, q), which is memoized via@cache. The number of distinct states determines the overall complexity:- The first parameter
iranges overO(n)values. - The parameters
pandqrepresent the numerator and denominator of a fraction in lowest terms, formed by multiplying or dividing by elements ofnums. The number of distinct values each ofpandqcan take is bounded byO(n^2), because each is built from products of subsets of thennumbers (the distinct attainable values for the reduced numerator/denominator scale on the order ofn^2).
Combining these, the total number of states is
O(n) * O(n^2) * O(n^2), but sincepandqare linked (forming a single reduced fraction), the effective number of distinct(p, q)pairs at each level isO(n^2), givingO(n) * O(n^2) = O(n^3)reachable states. Each state performs constant transitions, but each transition involves agcdcomputation costingO(log k). Accounting for the full enumeration of fraction states and the comparison againstk, the total work resolves toO(n^4 + log k), where the additionallog kterm comes from the gcd operations dependent on the magnitude ofk. - The first parameter
-
Space complexity:
O(n^4), wherenis the length of the arraynums.The space is dominated by the memoization cache, which stores results for all distinct
(i, p, q)states reached during the recursion. The number of such states isO(n^4), matching the dominant term of the time complexity. The recursion depth contributes onlyO(n), which is negligible compared to the cache size.
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Comparing fractions without reducing to lowest terms
The most subtle and dangerous mistake is checking whether val == k by comparing the raw numerator and denominator, or by storing the fraction in a non-canonical form. If you skip the GCD reduction after each multiply/divide step, two states that represent the same rational value can be stored as different (numerator, denominator) pairs — for example 2/4 and 1/2.
This breaks the code in two ways:
- Incorrect final comparison. The base case relies on the fact that a fully reduced fraction equals an integer
konly whennumerator == k and denominator == 1. If the fraction is not reduced, a value like4/2(which equals2) would fail the checknumerator == k and denominator == 1, producing a wrong count. - Cache ineffectiveness. Memoization only collapses repeated work when equal values map to identical keys. Without reduction,
2/4and1/2become distinct cache keys, so the cache rarely hits and the search degenerates toward3^nbehavior.
Solution: Always normalize after every operation using gcd, and only ever compare in canonical form.
# Multiply branch — reduce immediately g = gcd(numerator * current, denominator) nxt = dfs(index + 1, numerator * current // g, denominator // g) # Divide branch — reduce immediately g = gcd(numerator, denominator * current) nxt = dfs(index + 1, numerator // g, denominator * current // g) # Final check is then safe and correct if index == n: return 1 if numerator == k and denominator == 1 else 0
A quick way to verify you reduced correctly: the denominator should always be a positive integer, and any state whose value is integral must have denominator == 1.
Pitfall 2: Using integer (floor) division instead of rational division
It is tempting to track val as a single integer and write val // nums[i] for the divide branch. This is wrong for this problem, because division is defined as exact rational division. With floor division, 2 // 4 == 0, permanently destroying information — a later multiply can never recover the lost fraction. This silently undercounts valid sequences (and may even overcount if a value collapses to 0).
Solution: Represent the value as a pair (numerator, denominator) (or use Python's fractions.Fraction) so that intermediate values like 1/2 are preserved exactly. Floor division must never appear except as the // g step that divides out a known common factor — which is exact by construction.
Pitfall 3: Forgetting nums[i] may be 0 or negative
If the problem constraints allow nums[i] == 0, the divide branch numerator / (denominator * 0) is undefined, and the multiply branch forces the value to 0. Blindly computing gcd(..., denominator * 0) will give gcd(x, 0) == x, leading to a division by zero or a corrupted state.
Solution: Confirm the constraints. If zeros are possible, special-case them:
if current == 0: # Multiply makes val == 0 (never equals a nonzero k); # divide is undefined and must be skipped. count = dfs(index + 1, numerator, denominator) # skip count += dfs(index + 1, 0, 1) # multiply -> 0 return count
Similarly, if negatives are allowed, keep the sign only in the numerator so that equal values share one canonical key (e.g., store -1/2, never 1/-2). Normalize the sign right after each reduction.
Pitfall 4: Not clearing the memoization cache between test cases
Decorating dfs with @cache inside the method still binds the cache to the function object, which captures nums and k via closure. Across multiple Solution instances or repeated calls, a stale cache can return results computed for a different nums/k, causing intermittent wrong answers that are hard to reproduce.
Solution: Call dfs.cache_clear() after producing the answer (as the code does), or define the cached function fresh on each call so each invocation starts with an empty cache. Always release the cache to avoid both incorrect cross-test reuse and unbounded memory growth.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapHow many times is a tree node visited in a depth first search?
Recommended Readings
Memoization Prereq Backtracking problems backtracking Memoization is a fancy word for a simple concept so is the case for a lot of things we learn in school It means saving the previous function call result in a dictionary and reading from it when we do the exact same call again
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
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
Want a Structured Path to Master System Design Too? Don’t Miss This!