1735. Count Ways to Make Array With Product
Problem Description
The given problem involves combinatorics and number theory. We are tasked with determining the number of distinct ways we can fill an array of length n
such that the product of all the elements in the array equals a specific value k
. The constraints that must be considered:
- The array must contain only positive integers.
- The order of the elements in the array matters, meaning different permutations of the same numbers count as separate ways.
- The problem doesn't ask for the actual configurations but rather just the total count of these configurations.
- The count can become very large, so we return the result modulo
10^9 + 7
, a common practice to control the size of the output in combinatorial problems.
Each query queries[i]
represents a separate instance of the problem with its own n
and k
.
We aim to return an array where each element is the solution to the corresponding query.
Intuition
The key to solve this problem is understanding how to break down the prime factors of k
and utilize the stars-and-bars combinatorial method.
Here is the intuition behind the solution:
-
If
k
can be factorized into prime factors (e.g.,k = p1^a1 * p2^a2 * ... * pm^am
), the problem then reduces to how we can distribute thea1
counts ofp1
,a2
counts ofp2
, and so on inton
slots in the array. -
The problem, therefore, becomes one of partitioning each group of identical prime factors among the
n
slots, which is a typical scenario where the stars-and-bars theorem can be applied. -
Stars-and-bars is a well-known combinatorial method that can determine the number of ways
N
identical items can be placed intok
distinct bins.
The formula derived from stars-and-bars is comb(n+k-1, n-1)
which gives the count of ways we can insert k
indistinguishable items into n
bins.
-
The prime factorization for each
k
is done once and used to calculate all requested combinations, to improve efficiency. -
The provided code uses dynamic programming to calculate the factorial of numbers and their modular inverses, which are essential for calculating combinations modulo
10^9 + 7
. -
The function
comb
is used to apply the stars-and-bars theorem as discussed. -
The key point to understanding is that for each prime factor
pi
and its countai
, we havecomb(ai+n-1, n-1)
ways to distribute those prime factors into then
slots. Since the prime factors are independent of each other, we can multiply the results for each prime factor count to get the total count fork
. -
Finally, the solution iteratively computes the result for each query by multiplying the combinations of distributions for each prime factor count, thereby giving the total number of ways to fill the array for each query.
Learn more about Math, Dynamic Programming and Combinatorics patterns.
Solution Approach
The solution uses a combination of dynamic programming, number theory (prime factorization), and combinatorics (stars-and-bars theorem) to efficiently solve the problem. The implementation can be divided into several steps:
-
Pre-computation of Factorials and Inverses:
- The
f
array pre-computes and stores factorials modulo10^9 + 7
up to a certain numberN
. This is given by the relationf[i] = f[i - 1] * i % MOD
. - The
g
array stores the modular inverses of the factorials, which are precomputed using Fermat's Little Theorem.g[i] = pow(f[i], MOD - 2, MOD)
gives the modular inverse off[i]
. - Pre-computing these values allows constant-time access for calculating combinations later on.
- The
-
Prime Factorization:
- A dictionary
p
is used to store the counts of the prime factors for each number less thanN
. - For each number
i
, divide it by its prime factors, count their occurrences, and store the counts inp[i]
.
- A dictionary
-
Calculating Combinations:
- The
comb
function uses the pre-computed factorials and their inverses to calculate combinations. It follows the formulaf[n] * g[k] * g[n - k] % MOD
which providesC(n, k)
modulo10^9 + 7
.
- The
-
Solution Class:
- The
Solution
class contains the methodwaysToFillArray
, which takes a list of queries as input and returns the counts of ways to fill arrays as required. - For each query
[n, k]
, initialize a result variablet
to 1. This variable will hold the product of combinations for each prime factor count. - Iterate over the prime factor counts for
k
stored inp[k]
, and for each countx
, use thecomb
function to calculate the ways to distribute these prime factors inton
slots. Multiplyt
by this number moduloMOD
. - Append the final product
t
to theans
array after the inner loop. Thist
represents the answer to the query.
- The
-
Applying Stars-And-Bars Theorem:
- For each prime factor count
x
, the problem of distribution corresponds to assigningx
identical items ton
distinct bins, which is where the stars-and-bars theoremcomb(x + n - 1, n - 1)
is applied.
- For each prime factor count
-
Modular Arithmetic:
- Due to potentially large numbers and to abide by the problem statement, all calculations are done under modulo
10^9 + 7
. This ensures that the resulting number stays within the limit of a 32-bit integer.
- Due to potentially large numbers and to abide by the problem statement, all calculations are done under modulo
By combining these methods and following the solution step by step for each query, the solution approach effectively addresses the problem in a way that is optimized for time and space complexity.
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 illustrate the solution approach. Suppose we have one query with n = 2
and k = 8
. Our goal is to find the number of distinct ways we can fill an array of length n
such that the product of all elements in the array equals k
.
-
First, let's do the prime factorization of
k
. Sincek = 8
, and8
is2^3
, our prime factors are2
with a count of3
. -
Using stars-and-bars, we need to find the number of ways to distribute these three
2
s into then
slots. Using the theorem, we get the formulacomb(3 + 2 - 1, 2 - 1)
which calculates how to distribute 3 indistinguishable items (the prime factor2
in this case) into 2 bins (the array slots). -
Using the combination function
comb
, we findcomb(4, 1)
. We can assumef
andg
arrays have been pre-calculated so thatf
stores factorials modulo10^9 + 7
andg
stores the inverses of these factorials. Therefore,comb(4, 1)
will use these precomputed values to efficiently calculate the result. -
The combination would be:
f[4] * g[1] * g[4 - 1] % MOD
. If we have precomputed values, let's sayf[4] = 24
,g[1] = 1
, andg[3] = some_inverse_value
, the result would be24 * 1 * some_inverse_value % MOD
. -
The result of this calculation gives us the count of ways we can distribute the prime factors of
k
inton
slots. Since there's only one prime factor in this example, that result is the total count of ways to fill the array. -
We multiply all the combinations of distributions for each prime factor count. However, in this specific case, we only have one prime factor, so we don't need to multiply further.
-
The final result is what we calculated from
comb(4, 1)
, and that's the answer for the given query[n, k]
. -
All steps are performed using modular arithmetic to ensure that the output remains within the required bounds.
Applying the steps above, let's say we calculated the inverses and found that g[3]
is some_inverse_value
such that the product 24 * 1 * some_inverse_value % MOD
equals 4
. Therefore, for the query [2, 8]
, there are 4
distinct ways to fill the array such that the product of the elements is 8
.
For a concrete answer, let's just indicate the combinations without considering the modulo operation:
- Array
[1, 8]
: The elements' product is1*8 = 8
. - Array
[2, 4]
: The product is2*4 = 8
. - Array
[4, 2]
: Since order matters, this is different from[2, 4]
, so the product is4*2 = 8
. - Array
[8, 1]
: This is also different from[1, 8]
and the product is8*1 = 8
.
Thus, there are indeed 4
distinct ways to fill the array.
Solution Implementation
1from collections import defaultdict
2from typing import List
3
4# Define the maximum number for which we will pre-calculate the factorials.
5MAX_N = 10020
6# Define the modulo for calculations.
7MOD = 10**9 + 7
8
9# Pre-calculate factorials and their modular inverses.
10factorials = [1] * MAX_N
11inverse_factorials = [1] * MAX_N
12prime_factors_counts = defaultdict(list)
13
14# Calculate the values for 'factorials' and 'inverse_factorials',
15# and also pre-calculate the exponents of prime factors for all numbers up to MAX_N.
16for i in range(1, MAX_N):
17 factorials[i] = factorials[i - 1] * i % MOD
18 inverse_factorials[i] = pow(factorials[i], MOD - 2, MOD)
19 x = i
20 j = 2
21 while j <= x // j:
22 if x % j == 0:
23 count = 0
24 while x % j == 0:
25 count += 1
26 x //= j
27 prime_factors_counts[i].append(count)
28 j += 1
29 if x > 1:
30 prime_factors_counts[i].append(1)
31
32# Define a combination function using the pre-computed factorials and modular inverses.
33def combination(n, k):
34 return factorials[n] * inverse_factorials[k] * inverse_factorials[n - k] % MOD
35
36class Solution:
37 def waysToFillArray(self, queries: List[List[int]]) -> List[int]:
38 results = []
39 # For each query, calculate the number of ways to fill the array.
40 for n, k in queries:
41 total_ways = 1
42 # Use the counts of each prime factor to calculate combinations.
43 for exponent in prime_factors_counts[k]:
44 total_ways = total_ways * combination(exponent + n - 1, n - 1) % MOD
45 results.append(total_ways)
46 return results
47
1import java.util.ArrayList;
2import java.util.Arrays;
3import java.util.List;
4
5class Solution {
6 private static final int MAX_VALUE = 10020; // Maximum value for pre-computation
7 private static final int MODULUS = (int) 1e9 + 7; // Modulus value for calculations to prevent overflow
8 private static final long[] factorial = new long[MAX_VALUE]; // Cache for factorial values
9 private static final long[] inverseFactorial = new long[MAX_VALUE]; // Cache for inverse factorial values
10 private static final List<Integer>[] primeFactorsCounts = new List[MAX_VALUE]; // Lists to store counts of prime factors for each number
11
12 // Pre-compute factorials, inverse factorials, and prime factors counts
13 static {
14 factorial[0] = 1;
15 inverseFactorial[0] = 1;
16 Arrays.setAll(primeFactorsCounts, k -> new ArrayList<>());
17 for (int i = 1; i < MAX_VALUE; ++i) {
18 factorial[i] = factorial[i - 1] * i % MODULUS;
19 inverseFactorial[i] = modInverse(factorial[i], MODULUS);
20 int x = i;
21 for (int j = 2; j <= x / j; ++j) {
22 if (x % j == 0) {
23 int count = 0;
24 while (x % j == 0) {
25 count++;
26 x /= j;
27 }
28 primeFactorsCounts[i].add(count);
29 }
30 }
31 if (x > 1) {
32 primeFactorsCounts[i].add(1);
33 }
34 }
35 }
36
37 // Calculate modular inverse using fast exponentiation
38 public static long modInverse(long a, long k, long p) {
39 long res = 1;
40 while (k != 0) {
41 if ((k & 1) == 1) {
42 res = res * a % p;
43 }
44 k >>= 1;
45 a = a * a % p;
46 }
47 return res;
48 }
49
50 // Function to calculate combination C(n, k) using precomputed factorials
51 public static long combination(int n, int k) {
52 return (factorial[n] * inverseFactorial[k] % MODULUS) * inverseFactorial[n - k] % MODULUS;
53 }
54
55 // Given an array of queries, calculate the number of ways to fill an array for each query
56 public int[] waysToFillArray(int[][] queries) {
57 int queryLength = queries.length;
58 int[] result = new int[queryLength];
59 for (int i = 0; i < queryLength; ++i) {
60 int n = queries[i][0], k = queries[i][1];
61 long totalWays = 1;
62 for (int primeFactorCount : primeFactorsCounts[k]) {
63 totalWays = totalWays * combination(primeFactorCount + n - 1, n - 1) % MODULUS;
64 }
65 result[i] = (int) totalWays;
66 }
67 return result;
68 }
69}
70
1#include <vector>
2using namespace std;
3
4const int MAX_N = 10020;
5const long MOD = 1e9 + 7;
6long factorial[MAX_N];
7long inverseFactorial[MAX_N];
8vector<int> primeFactors[MAX_N];
9
10// Quick Modulo Exponentiation Function
11long qmi(long base, long exponent, long modulus) {
12 long res = 1;
13 while (exponent != 0) {
14 if ((exponent & 1) == 1) {
15 res = res * base % modulus;
16 }
17 exponent >>= 1; // equivalent to exponent /= 2
18 base = base * base % modulus;
19 }
20 return res;
21}
22
23// Initialization of factorials and prime factors arrays (done once using a lambda function)
24int dummy = []() {
25 factorial[0] = 1;
26 inverseFactorial[0] = 1;
27 // Precompute factorials and their modular inverses
28 for (int i = 1; i < MAX_N; ++i) {
29 factorial[i] = factorial[i - 1] * i % MOD;
30 inverseFactorial[i] = qmi(factorial[i], MOD - 2, MOD);
31
32 // Find and store prime factors for each number
33 int x = i;
34 for (int j = 2; j <= x / j; ++j) {
35 if (x % j == 0) {
36 int count = 0;
37 while (x % j == 0) {
38 ++count;
39 x /= j;
40 }
41 primeFactors[i].push_back(count);
42 }
43 }
44 if (x > 1) { // If there is a prime factor greater than 1
45 primeFactors[i].push_back(1);
46 }
47 }
48 return 0;
49}();
50
51// Compute binomial coefficient using the precomputed factorials and their inverses
52int comb(int n, int k) {
53 return (factorial[n] * inverseFactorial[k] % MOD) * inverseFactorial[n - k] % MOD;
54}
55
56class Solution {
57public:
58 vector<int> waysToFillArray(vector<vector<int>>& queries) {
59 vector<int> answer;
60 for (auto& q : queries) {
61 int n = q[0], k = q[1];
62 long long totalWays = 1;
63 // Calculate the total number of ways using the combinations of prime factors
64 for (int primeFactorCount : primeFactors[k]) {
65 totalWays = totalWays * comb(primeFactorCount + n - 1, n - 1) % MOD;
66 }
67 answer.push_back(totalWays);
68 }
69 return answer;
70 }
71};
72
1// Import statements required for the use of data structures like vectors are not required in TypeScript
2// Constants
3const MAX_N: number = 10020;
4const MOD: number = 1e9 + 7;
5
6// Arrays
7const factorial: number[] = new Array(MAX_N);
8const inverseFactorial: number[] = new Array(MAX_N);
9const primeFactors: number[][] = new Array(MAX_N).fill(null).map(() => []);
10
11// Quick Modulo Exponentiation Function
12const quickModInt = (base: number, exponent: number, modulus: number): number => {
13 let res: number = 1;
14 while (exponent !== 0) {
15 if ((exponent & 1) === 1) {
16 res = (res * base) % modulus;
17 }
18 exponent >>= 1; // equivalent to exponent /= 2
19 base = (base * base) % modulus;
20 }
21 return res;
22};
23
24// Anonymous function to initialize factorials and prime factors arrays
25const initialize = (() => {
26 factorial[0] = inverseFactorial[0] = 1;
27 for (let i = 1; i < MAX_N; ++i) {
28 factorial[i] = (factorial[i - 1] * i) % MOD;
29 inverseFactorial[i] = quickModInt(factorial[i], MOD - 2, MOD);
30
31 // Calculate prime factors for each number
32 let x = i;
33 for (let j = 2; j <= Math.sqrt(x); ++j) {
34 if (x % j === 0) {
35 let count = 0;
36 while (x % j === 0) {
37 count++;
38 x /= j;
39 }
40 primeFactors[i].push(count);
41 }
42 }
43 if (x > 1) {
44 primeFactors[i].push(1);
45 }
46 }
47})();
48
49// Compute binomial coefficient using precomputed factorials and their inverses
50const combination = (n: number, k: number): number => {
51 return (((factorial[n] * inverseFactorial[k]) % MOD) * inverseFactorial[n - k]) % MOD;
52};
53
54// Function to calculate ways to fill the array for each query
55const waysToFillArray = (queries: number[][]): number[] => {
56 const answer: number[] = [];
57 for (const query of queries) {
58 const n: number = query[0], k: number = query[1];
59 let totalWays: number = 1;
60
61 // Calculate total number of ways using combinations of prime factors
62 for (const primeFactorCount of primeFactors[k]) {
63 totalWays = (totalWays * combination(primeFactorCount + n - 1, n - 1)) % MOD;
64 }
65 answer.push(totalWays);
66 }
67 return answer;
68};
69
Time and Space Complexity
Time Complexity:
The time complexity of the code consists of pre-computation and query handling.
Pre-computation:
- Pre-computation of factorials
f[i]
and their modular inversesg[i]
: This is a linear operation with respect toN
, which gives a complexity ofO(N)
. - Calculation of prime factors and their exponents for each integer
i
(up toN
): The inner loop has a worst-case square root complexity for prime factorization, which givesO(sqrt(N))
for eachi
. Since we're doing this for each number up toN
, the pre-computation forp
has complexityO(N * sqrt(N))
.
The overall pre-computation complexity is O(N) + O(N * sqrt(N))
, which simplifies to O(N * sqrt(N))
, as this is the dominating term.
Query handling:
- For each query, we must calculate the total number of ways to form arrays. If
x
is the size of the list of exponents fork
, the complexity of computing the product of combinations isO(x)
. Sincex
could be at mostlog(k)
(the number of unique prime factors ofk
), this isO(log(k))
. - We handle each query in
O(log(k))
, and if there areQ
queries, the overall query handling complexity would beO(Q * log(k))
, wherek
represents the largest number in the queries' second element.
The total time complexity, considering both pre-computation and query handling, is O(N * sqrt(N)) + O(Q * log(k))
.
Space Complexity:
The space complexity is driven by the storage requirements for the factorials, modular inverses, and the prime factors list:
f[i]
andg[i]
arrays each consumeO(N)
space.p
, the dictionary of lists, in the worst case, can hold up tolog(N)
prime factors for each number up toN
, which gives usO(N * log(N))
.
The overall space complexity is O(N) + O(N) + O(N * log(N))
, which simplifies to O(N * log(N))
since this is the dominant term.
Learn more about how to find time and space complexity quickly using problem constraints.
Given a sorted array of integers and an integer called target, find the element that
equals to the target and return its index. Select the correct code that fills the
___
in the given code snippet.
1def binary_search(arr, target):
2 left, right = 0, len(arr) - 1
3 while left ___ right:
4 mid = (left + right) // 2
5 if arr[mid] == target:
6 return mid
7 if arr[mid] < target:
8 ___ = mid + 1
9 else:
10 ___ = mid - 1
11 return -1
12
1public static int binarySearch(int[] arr, int target) {
2 int left = 0;
3 int right = arr.length - 1;
4
5 while (left ___ right) {
6 int mid = left + (right - left) / 2;
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
1function binarySearch(arr, target) {
2 let left = 0;
3 let right = arr.length - 1;
4
5 while (left ___ right) {
6 let mid = left + Math.trunc((right - left) / 2);
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
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!