Facebook Pixel

3850. Count Sequences to K

HardMemoizationArrayMathDynamic ProgrammingNumber Theory
LeetCode ↗

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 val by nums[i].
  • Divide val by nums[i].
  • Leave val unchanged.

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why Dynamic Programming?

This problem maps to Dynamic Programming through a short path in the full flowchart.

Countingways orsequences?yesLinkedlist?noDynamicProgramming

Memoized DFS counts sequences of multiply/divide/skip choices that reach exactly k.

Open in Flowchart

Intuition

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 becomes p / (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:

  1. Reducing the fraction. If we keep multiplying numerators and denominators without simplifying, the numbers grow huge. After every multiply or divide, we compute the gcd of 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.

  2. 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:

  1. Keep it unchanged, i.e., dfs(i + 1, p, q).
  2. Multiply by nums[i], i.e., the new value is (p * nums[i]) / q.
  3. Divide by nums[i], i.e., the new value is p / (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: return 1 if p == k and q == 1, else 0.
  • 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^n possible 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 call dfs.cache_clear() to release the cached states.
  • GCD reduction: Using gcd to 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 unchangeddfs(1, 1, 1) (value stays 1/1).
  • Multiply by 2 → new value 2/1. We reduce: g = gcd(2, 1) = 1, so dfs(1, 2, 1).
  • Divide by 2 → new value 1/2. We reduce: g = gcd(1, 2) = 1, so dfs(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/1dfs(2, 2, 1)p==2 ≠ 1 ❌ returns 0
  • Divide → 1/2dfs(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/1dfs(2, 4, 1) → ❌ returns 0
  • Divide → 2/2 reduced to 1/1dfs(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/2 reduced to 1/1dfs(2, 1, 1) → ✅ returns 1
  • Divide → 1/4dfs(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):

SeqIndex 0Index 1ComputationFinal
1leaveleave11
2× 2÷ 21 × 2 ÷ 21
3÷ 2× 21 ÷ 2 × 21

Key observations from the trace:

  • GCD reduction in action: In Child B, dividing 2/1 by 2 produced 2/2, which we reduced to the canonical 1/1. Likewise in Child C, multiplying 1/2 by 2 gave 2/2 → 1/1. Without reduction these would look different from 1/1 and the final check p==k and q==1 would 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
40
1class 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}
94
1class 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};
50
1// 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}
87

Time and Space Complexity

  • Time complexity: O(n^4 + log k), where n is the length of the array nums.

    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 i ranges over O(n) values.
    • The parameters p and q represent the numerator and denominator of a fraction in lowest terms, formed by multiplying or dividing by elements of nums. The number of distinct values each of p and q can take is bounded by O(n^2), because each is built from products of subsets of the n numbers (the distinct attainable values for the reduced numerator/denominator scale on the order of n^2).

    Combining these, the total number of states is O(n) * O(n^2) * O(n^2), but since p and q are linked (forming a single reduced fraction), the effective number of distinct (p, q) pairs at each level is O(n^2), giving O(n) * O(n^2) = O(n^3) reachable states. Each state performs constant transitions, but each transition involves a gcd computation costing O(log k). Accounting for the full enumeration of fraction states and the comparison against k, the total work resolves to O(n^4 + log k), where the additional log k term comes from the gcd operations dependent on the magnitude of k.

  • Space complexity: O(n^4), where n is the length of the array nums.

    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 is O(n^4), matching the dominant term of the time complexity. The recursion depth contributes only O(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:

  1. Incorrect final comparison. The base case relies on the fact that a fully reduced fraction equals an integer k only when numerator == k and denominator == 1. If the fraction is not reduced, a value like 4/2 (which equals 2) would fail the check numerator == k and denominator == 1, producing a wrong count.
  2. Cache ineffectiveness. Memoization only collapses repeated work when equal values map to identical keys. Without reduction, 2/4 and 1/2 become distinct cache keys, so the cache rarely hits and the search degenerates toward 3^n behavior.

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 Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

How many times is a tree node visited in a depth first search?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More