2338. Count the Number of Ideal Arrays
Problem Description
You're tasked with finding the number of distinct "ideal" integer arrays of a certain length n
, each containing integers that range from 1
to maxValue
. An array qualifies as "ideal" if it meets two conditions:
- Every element
arr[i]
takes on a value between1
andmaxValue
(inclusive). - For every
i
from1
ton-1
, the elementarr[i]
is divisible by the element before itarr[i - 1]
.
The result should be returned as the count of these distinct arrays modulo 10^9 + 7
.
Intuition
The problem requires counting configurations under certain constraints, which is a classic dynamic programming challenge. The idea is to use a memoized recursive function (dfs
in the given code) to explore different array configurations, starting from each potential value [1, maxValue] as the first element. The dynamic programming function serves to count the distinct ways to build up the ideal array by incrementally choosing subsequent elements that follow the divisibility rule.
Here's the approach followed in the provided solution:
-
Initialize a combinatorics table
c
that will be used later to calculate combinations efficiently (c[n][k]
will represent the number of combinations to pickk
items fromn
items). -
The
dfs
function is recursively used to explore all possible valid next elements for a given current elementi
and a countcnt
of how many elements have been chosen so far. If a numberk*i
is still withinmaxValue
, it can become the next element in the ideal array. -
The number of ways to extend the array from the current element
i
andcnt
is the sum of ways to extend all valid next elements (k*i, cnt+1
). -
It's important to keep in mind that the answer could be very large so the modulo operation is performed at every step to ensure the numbers stay within the bounds.
-
We iterate over all possible starting elements from 1 to
maxValue
and usedfs
to count all distinct arrays starting from that element.
Through this recursive exploration that carefully counts combinations while respecting the constraints, we can amass the total number of distinct ideal arrays.
Learn more about Math, Dynamic Programming and Combinatorics patterns.
Solution Approach
The solution uses dynamic programming, recursion with memoization, combinatorics (for counting combinations), and modulus operations to prevent integer overflow. Here's a walkthrough of the implementation details:
Dynamic Programming and Memoization
- A two-dimensional array called
c
is prepared in advance to store binomial coefficients which are used for calculating combinations. This uses the well-known formulaC(n, k) = C(n-1, k) + C(n-1, k-1)
to populate the array efficiently. - The
dfs
function is memoized using the@cache
decorator (assuming Python 3.9+), which means that it remembers the results of previous computations and doesn't re-calculate them, leading to significant time savings.
Recursive Function dfs
- The
dfs
function takes two arguments: the current valuei
and the countcnt
of elements in the array so far. This function calculates the number of ways to build an ideal array starting with the valuei
and havingcnt
elements already. - The base case is that when
cnt
reachesn
, no more elements can be added, and soc[n-1][cnt-1]
is returned, which represents the number of ways to arrangecnt
elements in positions up ton-1
. - For each possible next multiple
k * i
(k >= 2
), the function recursively calculates the number of ideal arrays that can be built by adding this next multiple, incrementingcnt
by 1 for each recursive call. - The modulo
mod = 10**9 + 7
is applied at every addition to keep the number manageable as per the problem statement.
Combination Preprocessing
- A loop is used to calculate and cache binomial coefficients in advance for the combinatorial part of the computation. This is essential for efficiently calculating the number of combinations in the recursion.
Summation and Modulo Application
- A loop iterates from
1
tomaxValue
, callingdfs(i, 1)
for eachi
. The function returns the number of distinct ideal arrays starting with the valuei
. - The results are summed up (with modulo applied) to get the final answer.
Final Result
- The answer variable
ans
is initialized to0
. It accumulates the number of ideal arrays calculated by thedfs
function for every possible starting elementi
. - The final answer is then returned, which represents the total number of distinct ideal arrays of length
n
with values up tomaxValue
.
By applying dynamic programming with recursive memoization and combinatorial mathematics, the solution efficiently calculates the total number of configurations that meet the problem's requirements.
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 illustrate the solution approach using an example where the length of the array n
is 3, and the maximum value maxValue
is 4. We will walk through some steps to understand how the algorithm works in this simple case.
First, we've initialized a combinatorics table c
for calculating combinations. This table would look something like this for n=3
, though the actual table might have more entries for efficiency in real problems:
0 | 1 | 2 | 3 | |
---|---|---|---|---|
0 | 1 | 0 | 0 | 0 |
1 | 1 | 1 | 0 | 0 |
2 | 1 | 2 | 1 | 0 |
3 | 1 | 3 | 3 | 1 |
Now, let's examine how we might use the dfs
function to compute the number of ideal arrays using memoization and recursion.
-
Starting with
i=1
, we calldfs(1, 1)
. This means that we are looking to build an array that starts with1
and currently has one element. -
Since
i=1
andcnt=1
, we look at the next multiples ofi
, which are2*1, 3*1,
and4*1
. Each of these can be the next element in the array because they are all divisible by1
. -
We then make recursive calls to
dfs
for each of these multiples, incrementingcnt
each time. So we getdfs(2, 2)
,dfs(3, 2)
, anddfs(4, 2)
. -
When
dfs
is called withcnt=3
, we reach the base case, and we count the number of configurations using our combinatorics tablec
. In this case, it returnsc[2][2]
which is1
because there's only one way to arrange two elements in two positions. -
Next, we would move on to
i=2
and do the same process, but this time the possible multiples are4*2
because multiples like6*2, 8*2
exceedmaxValue
. -
Similarly for
i=3
andi=4
, we notice that there are no valid multiples within themaxValue
for these starting points, so the recursive calls would immediately hit the base case and returnc[n-1][cnt-1]
.
Once all possible starting i
values are considered, we would sum up all the computed ways, applying the modulo as necessary, to obtain the total number of distinct ideal arrays.
In our small example, let's say the dfs calls return the following configurations:
- For
i=1
, we get 3 arrays[1,1,1]
,[1,2,2]
,[1,4,4]
(assumingdfs
fori=3
returns 0 since it has no multiples withinmaxValue
). - For
i=2
, we get 1 array[2,2,2]
. - For
i=3
, we get 0 arrays as explained. - For
i=4
, we also get 0 arrays.
Adding these up, we get a total of 4 distinct "ideal" integer arrays for n=3
and maxValue=4
.
Each of the sums is calculated modulo 10^9 + 7
to prevent overflow and ensure the calculation remains within the required bounds. So, our final answer for this example would be 4 % (10^9 + 7), which is still 4.
Solution Implementation
1# The provided code is already in Python 3 syntax, but I've made changes
2# to improve readability, standardized naming, and added comments for better understanding.
3
4from functools import lru_cache # Importing lru_cache for caching
5
6class Solution:
7 def idealArrays(self, n: int, maxValue: int) -> int:
8 # Using the lru_cache decorator to memoize the results of the dfs function
9 @lru_cache(maxsize=None)
10 def dfs(value, length):
11 """
12 Performs a depth-first search starting with a given value
13 and length of the sequence, and returns the number of ideal arrays.
14 """
15 # Initialize result with the combination C(n-1, length-1)
16 result = comb_table[-1][length - 1]
17 # If we haven't reached the desired length, continue building the sequence
18 if length < n:
19 multiple = 2
20 # Iterate through the possible multiples of 'value' within the maxValue
21 while multiple * value <= maxValue:
22 # Recursively call dfs and update the result
23 result = (result + dfs(multiple * value, length + 1)) % MODULE
24 multiple += 1
25 return result
26
27 # Comb table creation using dynamic programming to calculate combinations
28 comb_table = [[0] * 16 for _ in range(n)]
29 MODULE = 10**9 + 7 # Module for the problem, to prevent integer overflow
30 for i in range(n):
31 for j in range(min(16, i + 1)):
32 # Base case: There is one way to choose 0 items
33 if j == 0:
34 comb_table[i][j] = 1
35 else:
36 # Use the recurrence relation C(n, k) = C(n-1, k) + C(n-1, k-1)
37 comb_table[i][j] = (comb_table[i - 1][j] + comb_table[i - 1][j - 1]) % MODULE
38
39 # Initial answer is set to zero
40 answer = 0
41 # Iterate through all possible starting values
42 for i in range(1, maxValue + 1):
43 # Update the answer by using the dfs function
44 answer = (answer + dfs(i, 1)) % MODULE
45
46 return answer # Return the final answer
47
1import java.util.Arrays;
2
3class Solution {
4 private int[][] memoization;
5 private int[][] combinationMatrix;
6 private int numElements;
7 private int maxValue;
8 private static final int MOD = (int) 1e9 + 7;
9
10 public int idealArrays(int n, int maxValue) {
11 this.numElements = n;
12 this.maxValue = maxValue;
13
14 // Initialize memoization array with -1 to indicate uncalculated states
15 this.memoization = new int[maxValue + 1][16];
16 for (int[] row : memoization) {
17 Arrays.fill(row, -1);
18 }
19
20 // Pre-calculate the combination values (n choose k) up to n this combinationMatrix will be used in dfs
21 combinationMatrix = new int[n][16];
22 for (int i = 0; i < n; ++i) {
23 for (int j = 0; j <= i && j < 16; ++j) {
24 combinationMatrix[i][j] = (j == 0 ? 1 : (combinationMatrix[i - 1][j] + combinationMatrix[i - 1][j - 1]) % MOD);
25 }
26 }
27
28 int answer = 0;
29
30 // Calculate the total number of ideal arrays for all starting values
31 for (int i = 1; i <= maxValue; ++i) {
32 answer = (answer + dfs(i, 1)) % MOD;
33 }
34
35 return answer;
36 }
37
38 // Depth First Search to find the number of ideal arrays starting with 'startValue'
39 // and having 'length' number of distinct integers sorted in strictly increasing order.
40 private int dfs(int startValue, int length) {
41 // If previously computed, retrieve the result from memoization table
42 if (memoization[startValue][length] != -1) {
43 return memoization[startValue][length];
44 }
45
46 // Base value is 'n choose length-1'
47 int count = combinationMatrix[numElements - 1][length - 1];
48
49 // If length is less than the number of elements, continue to find all possible arrays
50 if (length < numElements) {
51 for (int k = 2; startValue * k <= maxValue; ++k) {
52 count = (count + dfs(startValue * k, length + 1)) % MOD;
53 }
54 }
55
56 // Store the result in the memoization table before returning
57 memoization[startValue][length] = count;
58 return count;
59 }
60}
61
1class Solution {
2public:
3 int maxValue, sequenceLength;
4 const int MOD = 1e9 + 7; // Modulus for the problem to handle large numbers
5
6 // f = memoization table for results of dfs function with index i and count cnt
7 // c = combinations table to store number of combinations to choose j elements from a set of size i
8 vector<vector<int>> f;
9 vector<vector<int>> c;
10
11 // Main function that starts the process to count ideal arrays
12 int idealArrays(int n, int maxValue) {
13 this->maxValue = maxValue;
14 this->sequenceLength = n;
15 // Initialize memoization table with -1 to represent uncalculated states
16 f.assign(maxValue + 1, vector<int>(16, -1));
17 // Initialize combinations table with 0 values
18 c.assign(n, vector<int>(16, 0));
19
20 // Precalculate combinations using Pascal's triangle relationship
21 for (int i = 0; i < n; ++i) {
22 for (int j = 0; j <= i && j < 16; ++j) {
23 c[i][j] = j == 0 ? 1 : (c[i - 1][j] + c[i - 1][j - 1]) % MOD;
24 }
25 }
26
27 int ans = 0;
28 // Sum the results of dfs for each number starting from 1 to maxValue
29 for (int i = 1; i <= maxValue; ++i) {
30 ans = (ans + dfs(i, 1)) % MOD;
31 }
32 return ans;
33 }
34
35 // Depth-first search function that calculates the number of ideal arrays
36 // ending with the integer 'i', having 'cnt' unique integers
37 int dfs(int i, int cnt) {
38 // If the result has already been calculated, return it to avoid recalculation
39 if (f[i][cnt] != -1) return f[i][cnt];
40
41 // Base result is the number of sequences that can be formed with 'cnt' copies of 'i'
42 int res = c[sequenceLength - 1][cnt - 1];
43
44 // If we can add more unique numbers to the array
45 if (cnt < sequenceLength) {
46 // Try to extend the sequence by adding multiples of 'i'
47 for (int k = 2; i * k <= maxValue; ++k) {
48 res = (res + dfs(i * k, cnt + 1)) % MOD;
49 }
50 }
51
52 // Store the calculated result in the memoization table
53 f[i][cnt] = res;
54 return res;
55 }
56};
57
1const MOD = 1e9 + 7; // Modulus for the problem to handle large numbers
2
3let maxValue: number; // The maximum value that array elements can take
4let sequenceLength: number; // The length of the sequence
5let f: number[][]; // Memoization table for results of dfs function with index i and count cnt
6let c: number[][]; // Combinations table to store number of combinations
7
8// Function to initialize the necessary variables and tables
9function initializeSolution(n: number, mValue: number): void {
10 maxValue = mValue; // Store the provided maximum value
11 sequenceLength = n; // Store the provided sequence length
12 f = Array.from({ length: maxValue + 1 }, () => Array(16).fill(-1)); // Initialize memoization table with -1
13 c = Array.from({ length: n }, () => Array(16).fill(0)); // Initialize combinations table with 0
14
15 // Precalculate combinations using Pascal's triangle relationship
16 for (let i = 0; i < n; ++i) {
17 for (let j = 0; j <= i && j < 16; ++j) {
18 c[i][j] = j === 0 ? 1 : (c[i - 1][j] + c[i - 1][j - 1]) % MOD;
19 }
20 }
21}
22
23// Main function to calculate the count of ideal arrays
24function idealArrays(n: number, mValue: number): number {
25 initializeSolution(n, mValue);
26
27 let ans = 0;
28 // Sum the results of dfs for each number starting from 1 to maxValue
29 for (let i = 1; i <= maxValue; ++i) {
30 ans = (ans + dfs(i, 1)) % MOD;
31 }
32 return ans;
33}
34
35// Depth-first search function to calculate the count of ideal arrays
36// ending with the integer 'i', having 'cnt' unique integers
37function dfs(i: number, cnt: number): number {
38 // Check the memoization table to avoid recalculating
39 if (f[i][cnt] !== -1) return f[i][cnt];
40
41 // The base result is the count of sequences that can be formed with 'cnt' copies of 'i'
42 let res = c[sequenceLength - 1][cnt - 1];
43
44 // If more unique numbers can be added to the array
45 if (cnt < sequenceLength) {
46 // Extend the sequence by adding multiples of 'i'
47 for (let k = 2; i * k <= maxValue; ++k) {
48 res = (res + dfs(i * k, cnt + 1)) % MOD;
49 }
50 }
51
52 // Store the calculated result in the memoization table
53 f[i][cnt] = res;
54 return res;
55}
56
Time and Space Complexity
The given Python code represents a solution for computing the number of ideal arrays of length n
with elements not exceeding maxValue
. It uses dynamic programming with memoization (caching previous results) and combinatorial mathematics.
Time Complexity
The time complexity of this solution depends on the depth of the recursion and the number of iterations. Let's break down the time complexity:
-
The
dfs
function computes the number of ways to build an array starting with the elementi
and havingcnt
elements so far. It recursively calls itself while the multiples ofi
are less than or equal tomaxValue
. The memoization (@cache
) dramatically reduces the number of computations by storing already computed values ofdfs(i, cnt)
. -
The recursion depth is at most
log(maxValue)
because we are considering the multiples ofi
untilmaxValue
. Since the multiples are found by multiplying byk (k >= 2)
, the number of recursive calls fordfs
is limited by the logarithmic scale ofmaxValue
. -
Inside every recursive call, a constant-time operation of combination calculation (+, %) is performed. For each
i
, the recursion exploreslog(maxValue)
levels. -
The outer loop runs from
1
tomaxValue
.
So, roughly, the time complexity can be estimated as O(maxValue * log(maxValue))
.
Space Complexity
The space complexity includes the space taken by the recursive call stack and the space used to store combinations:
-
The cache used for memoization will have at most
maxValue * n
entries because each unique pair of(i, cnt)
will be stored only once. -
The space for storing combinations is
O(n * 16)
since we have limited the combination array to 16 elements wide. -
The call stack's maximum depth would be
log(maxValue)
due to the nature of the recursion.
The dominant term in the space complexity is the cache size i.e. O(maxValue * n)
, so the overall space complexity can be considered as O(maxValue * n)
.
Learn more about how to find time and space complexity quickly using problem constraints.
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
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
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Want a Structured Path to Master System Design Too? Don’t Miss This!