Facebook Pixel

3339. Find the Number of K-Even Arrays 🔒


Problem Description

You are given three integers n, m, and k. An array arr is called k-even if there are exactly k indices such that, for each of these indices i (where (0 \leq i < n - 1)), the expression ((\text{arr}[i] \times \text{arr}[i + 1]) - \text{arr}[i] - \text{arr}[i + 1]) is even. The task is to return the number of possible k-even arrays of size n where all elements are within the range ([1, m]). Since the answer may be very large, return it modulo (10^9 + 7).

Intuition

To solve this problem, we need to determine the number of ways to fill an array of size n with numbers between 1 and m such that exactly k contiguous pairs within the array satisfy the given even condition.

Key Observations:

  1. Parity of Expressions: The expression ((\text{arr}[i] \times \text{arr}[i + 1]) - \text{arr}[i] - \text{arr}[i + 1]) becomes even if the product (\text{arr}[i] \times \text{arr}[i + 1]) is odd, which happens only when both numbers are odd.

  2. Counting Parities: There are (\text{cnt0} = \left\lfloor \frac{m}{2} \right\rfloor) even numbers and (\text{cnt1} = m - \text{cnt0}) odd numbers within the range [1, m].

  3. Recursive Approach: We define a recursive function, dfs(i, j, k), representing the number of valid ways to fill up to the (i^{th}) position of the array with j needed pairs left to satisfy the even condition. The third parameter k indicates the parity of the last added element (0 for even and 1 for odd).

Recursive Steps:

  • If j < 0, the configuration is invalid, so return 0.
  • If i >= n, check if all needed pairs j are used up; if j == 0, it is a valid configuration, return 1; otherwise, return 0.
  • Depending on whether the last number added was odd or even, calculate the possibilities of adding another odd or even number and continue.

This approach is efficiently implemented using memoization to store and reuse previously computed results, significantly optimizing the recursive calculations. The modulo operation ensures the result's constraints are satisfied.

Learn more about Dynamic Programming patterns.

Solution Approach

The problem is tackled using a recursive approach with memoization, which efficiently explores all possible configurations of the array while minimizing redundant calculations. Here's a detailed explanation of the solution implementation:

Recursive Function Design:

  • Function Definition: We define dfs(i, j, k), where:

    • i is the current index in the array we are trying to fill.
    • j is the number of remaining pair indices that need to satisfy the even condition.
    • k indicates the parity of the last element added (0 for even and 1 for odd).
  • Base Cases:

    • If j < 0, we've exceeded the required number of valid pairs, and thus return 0.
    • If i >= n, we've filled the array. If j equals 0, indicating all required pairs are valid, return 1; otherwise, return 0.
  • Recursive Logic:

    • Consider the current number being added, checking if it's odd or even (k).
    • If the last element added is odd (k == 1), calculate the possible ways to continue with the next element being odd or even:
      • Add an odd: cnt1 * dfs(i + 1, j, 1) (where cnt1 is the count of odd numbers).
      • Add an even: cnt0 * dfs(i + 1, j - 1, 0) (where cnt0 is the count of even numbers and j-1 since a valid pair is formed).
    • Combine these possibilities and take the sum modulo (10^9 + 7).

Pre-computation:

  • Calculate the number of odd and even numbers in the range ([1, m]) as cnt1 and cnt0 respectively.

  • Modulo Operation: The operations are performed under modulo (10^9 + 7) to ensure numbers remain manageable and satisfy output constraints.

The algorithm efficiently combines recursive exploration with the use of a cached dfs utility to handle overlapping subproblems, thereby achieving optimal time complexity. The approach leverages depth-first search with memoization, enabling it to handle larger inputs within the given constraints.

Ready to land your dream job?

Unlock your dream job with a 2-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a simple example to better understand the solution approach.

Example:

Suppose we have n = 3, m = 3, and k = 2. We want to find how many 3-element arrays satisfy exactly 2 pairs with the given even condition.

Step-by-step Walkthrough:

  1. Identify Odd and Even Numbers:

    • Given m = 3, the numbers [1, 2, 3] are possible.
    • Odd numbers: [1, 3] (count cnt1 = 2)
    • Even numbers: [2] (count cnt0 = 1)
  2. Understand the Condition:

    • The expression becomes even if both numbers in the pair are odd.
    • We need exactly 2 such pairs formed from our array of size 3.
  3. Define Recursive Steps:

    Use a function dfs(i, j, k):

    • i: Current index in the array.
    • j: Remaining pairs needed to satisfy the even condition.
    • k: Parity of the last number added (0 for even, 1 for odd).
  4. Recursive Exploration Example:

    • Start from the first position: dfs(0, 2, -1) (begin with no parity set).

    • First Element:

      • Can be 1 (odd): dfs(1, 2, 1)
      • Can be 2 (even): dfs(1, 2, 0)
      • Can be 3 (odd): dfs(1, 2, 1)
    • Second Element (if first was odd):

      • If first was 1, and add 1: dfs(2, 2, 1) (No valid pair formed)
      • If first was 1, and add 2: dfs(2, 1, 0) (Valid pair: (1, 2) formed)
      • If first was 1, and add 3: dfs(2, 2, 1) (No valid pair formed)
      • Repeat for other possibilities similarly.
    • Check Base Cases:

      • If we reach i = n and j == 0, it means all k pairs are formed.
  5. Final Calculation:

    • Summing up all valid configurations, apply modulo (10^9 + 7) to keep the result manageable.

This method explores all configurations recursively with state (index, remaining pairs, last parity), effectively utilizing memoization to store sub-problems results, thus optimizing the solution.

Solution Implementation

1class Solution:
2    def countOfArrays(self, n: int, m: int, k: int) -> int:
3        from functools import cache
4
5        # Using memoization to avoid recalculating results for the same state
6        @cache
7        def dfs(index: int, count: int, search_value: int) -> int:
8            if count < 0:
9                return 0
10            if index >= n:
11                # Check if the search count is zero, meaning search targets were met
12                return int(count == 0)
13
14            # Recursively calculate with updated search values
15            return (
16                count_odd * dfs(index + 1, count, 1) +
17                count_even * dfs(index + 1, count - ((k & 1) ^ 1), 0)
18            ) % MOD
19
20        # Calculate number of even and odd options
21        count_even = m // 2
22        count_odd = m - count_even
23        MOD = 10**9 + 7
24
25        # Start the DFS from the first index targeting 'k' searches
26        result = dfs(0, k, 1)
27
28        # Clear cache if used for other recursive purposes
29        dfs.cache_clear()
30      
31        return result
32
1class Solution {
2    private Integer[][][] memoization;
3    private long evenCount, oddCount;
4    private final int MOD = (int) 1e9 + 7;
5
6    public int countOfArrays(int n, int m, int k) {
7        // Initialize memoization table to store intermediate results
8        memoization = new Integer[n][k + 1][2];
9
10        // Calculate counts of even and odd numbers within the range up to m
11        evenCount = m / 2;
12        oddCount = m - evenCount;
13
14        // Start the recursive depth-first search from the first element, with k changes remaining and initially considering odd first
15        return dfs(0, k, 1);
16    }
17
18    private int dfs(int index, int remainingChanges, int lastParity) {
19        // Base case: if negative changes are left, not possible to construct such array, return 0
20        if (remainingChanges < 0) {
21            return 0;
22        }
23
24        // Base case: if index exceeds the array length, check if no changes are left
25        if (index >= memoization.length) {
26            return remainingChanges == 0 ? 1 : 0;
27        }
28
29        // Return the cached result if already computed
30        if (memoization[index][remainingChanges][lastParity] != null) {
31            return memoization[index][remainingChanges][lastParity];
32        }
33
34        // Recursive case:
35        // Calculate the number of arrays if next number is odd and last number was odd
36        int countOddParity = (int) ((oddCount * dfs(index + 1, remainingChanges, 1)) % MOD);
37
38        // Calculate the number of arrays if next number is even and last number was even or odd
39        int countEvenParity = (int) ((evenCount * dfs(index + 1, remainingChanges - (lastParity ^ 1), 0)) % MOD);
40
41        // Store the result in memoization table and return the sum of both possibilities modulo MOD
42        return memoization[index][remainingChanges][lastParity] = (countOddParity + countEvenParity) % MOD;
43    }
44}
45
1#include <vector>
2#include <cstring>
3
4class Solution {
5public:
6    int countOfArrays(int n, int m, int k) {
7        // Dynamic programming memoization table
8        int dp[n][k + 1][2];
9        // Initialize all values in the table to -1
10        memset(dp, -1, sizeof(dp));
11        const int MOD = 1e9 + 7;  // Modulo constant to prevent overflow
12      
13        int cnt0 = m / 2;  // Elements that contribute to a 0-th sequence
14        int cnt1 = m - cnt0;  // Elements that contribute to a 1-st sequence
15
16        // Dynamic programming with memoization using a lambda for depth-first search
17        auto dfs = [&](auto&& dfsRef, int i, int j, int isLastOne) -> int {
18            // If the number of distinct arrays needed (j) is negative, return 0
19            if (j < 0) return 0;
20            // Base case: if index reaches n and exactly k distinct arrays are used
21            if (i >= n) return j == 0 ? 1 : 0;
22            // Return cached result if already computed
23            if (dp[i][j][isLastOne] != -1) return dp[i][j][isLastOne];
24          
25            // Calculate using cnt1 for the sequence of last '1'
26            int option_a = 1LL * cnt1 * dfsRef(dfsRef, i + 1, j, 1) % MOD;
27            // Calculate using cnt0 for the sequence of last '0' and adjust j for alternation
28            int option_b = 1LL * cnt0 * dfsRef(dfsRef, i + 1, j - ((isLastOne + 1) % 2), 0) % MOD;
29          
30            // Store the result in the dp table and return the computed value
31            return dp[i][j][isLastOne] = (option_a + option_b) % MOD;
32        };
33
34        // Start the recursive DFS with initial state
35        return dfs(dfs, 0, k, 1);
36    }
37};
38
1// The module constant for modulo operation
2const MOD = 1e9 + 7;
3
4// This function calculates the number of valid arrays
5function countOfArrays(n: number, m: number, k: number): number {
6    // 3D array to memoize the results of subproblems, initialized with -1
7    const f = Array.from({ length: n }, () =>
8        Array.from({ length: k + 1 }, () => Array(2).fill(-1)),
9    );
10
11    // Calculate the number of elements less than or equal to m/2
12    const cnt0 = Math.floor(m / 2);
13    // Calculate the number of elements greater than m/2
14    const cnt1 = m - cnt0;
15
16    // Helper function `dfs` for recursive exploration
17    const dfs = (i: number, j: number, flag: number): number => {
18        // If j is negative, it's an invalid state, return 0
19        if (j < 0) {
20            return 0;
21        }
22        // If we have filled all positions, check if we have used k distinct values
23        if (i >= n) {
24            return j === 0 ? 1 : 0;
25        }
26        // Use previously computed result if available
27        if (f[i][j][flag] !== -1) {
28            return f[i][j][flag];
29        }
30        // Compute the number of ways using cnt1 and cnt0
31        const countUsingCnt1 = (cnt1 * dfs(i + 1, j, 1)) % MOD;
32        const countUsingCnt0 = (cnt0 * dfs(i + 1, j - ((flag & 1) ^ 1), 0)) % MOD;
33      
34        // Store the computed result and return
35        f[i][j][flag] = (countUsingCnt1 + countUsingCnt0) % MOD;
36        return f[i][j][flag];
37    };
38
39    // Start the depth-first search from position 0 with k distinct values needed and flag 1
40    return dfs(0, k, 1);
41}
42

Time and Space Complexity

The time complexity of the given code is O(n * k), since the depth-first search with memoization (dfs) is called for each of the n indices and up to k distinct values of j, making the number of dfs calls proportional to n * k. The memoization ensures that each unique state (i, j, k) is computed at most once.

The space complexity is O(n * k), which accounts for the space required to store the results of the dfs calls in the cache. This cache stores results for each state defined by the parameters i and j, leading to a maximum of n * k storage requirements.

Learn more about how to find time and space complexity quickly.


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

Consider the classic dynamic programming of longest increasing subsequence:

Find the length of the longest subsequence of a given sequence such that all elements of the subsequence are sorted in increasing order.

For example, the length of LIS for [50, 3, 10, 7, 40, 80] is 4 and LIS is [3, 7, 40, 80].

What is the recurrence relation?


Recommended Readings

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


Load More