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:
-
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.
-
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].
-
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 withj
needed pairs left to satisfy the even condition. The third parameterk
indicates the parity of the last added element (0
for even and1
for odd).
Recursive Steps:
- If
j < 0
, the configuration is invalid, so return0
. - If
i >= n
, check if all needed pairsj
are used up; ifj == 0
, it is a valid configuration, return1
; otherwise, return0
. - 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 and1
for odd).
-
Base Cases:
- If
j < 0
, we've exceeded the required number of valid pairs, and thus return0
. - If
i >= n
, we've filled the array. Ifj
equals0
, indicating all required pairs are valid, return1
; otherwise, return0
.
- If
-
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)
(wherecnt1
is the count of odd numbers). - Add an even:
cnt0 * dfs(i + 1, j - 1, 0)
(wherecnt0
is the count of even numbers andj-1
since a valid pair is formed).
- Add an odd:
- Combine these possibilities and take the sum modulo (10^9 + 7).
- Consider the current number being added, checking if it's odd or even (
Pre-computation:
-
Calculate the number of odd and even numbers in the range ([1, m]) as
cnt1
andcnt0
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 EvaluatorExample 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:
-
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
)
- Given
-
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.
-
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).
-
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)
- Can be 1 (odd):
-
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.
- If first was 1, and add 1:
-
Check Base Cases:
- If we reach
i = n
andj == 0
, it means allk
pairs are formed.
- If we reach
-
-
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.
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
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!