2964. Number of Divisible Triplet Sums
Problem Description
The challenge is to find out the number of unique triplets within an array nums
, where each triplet consists of different indices (i, j, k)
that follow a specific criterion. This criterion requires that the indices form a strictly increasing sequence (i < j < k
) and the sum of the elements at these indices is divisible by a given integer d
. In other words, nums[i] + nums[j] + nums[k]
should be an integer multiple of d
.
To tackle this problem, we need to identify combinations of three different array elements whose indices are in ascending order and check for the divisibility of their sum by d
. It's a computational challenge that requires efficient enumeration of the possible triplets to avoid a brute force approach that would take too long.
Intuition
To optimize the process of finding these triplets, the solution leverages a hash table strategy to avoid redundant calculations. The main idea behind the solution is to use a hash table, denoted as cnt
, to keep track of how many times each remainder (when elements of nums
are divided by d
) occurs up to the current index being considered. As we iterate through the array, we calculate what remainder we would need from the nums[i]
(where i < j
) to ensure that the sum of nums[i]
, nums[j]
, and nums[k]
is divisible by d
.
Here's the thinking process:
-
As we move through the array, with each
nums[j]
, we look ahead to all futurenums[k]
wherek > j
. For each of these pairs, we calculate the remainder that would be needed by a precedingnums[i]
to make the sum ofnums[i] + nums[j] + nums[k]
divisible byd
. We use the aforementioned hash table to quickly check the count of such potentialnums[i]
elements. -
We then add the count from our hash table to our answer (accumulating the number of triplets that meet our criteria up to the current
j
). -
After considering pairs of
nums[j]
andnums[k]
, we increment the count of the remainder ofnums[j]
itself in the hash table before continuing to the nextj
.
By doing this, we avoid calculating the same remainder over and over for each potential triplet, which drastically reduces the number of operations from what would be required in a brute force solution.
Solution Approach
The solution to this LeetCode problem is centered around a clever use of a hash table, specifically a defaultdict
from Python's collections module, which allows us to automatically initialize missing keys with an integer (initialized to 0
in this case). This helps us to track the frequency of remained parts of numbers modulo d
.
The algorithm can be broken down into the following steps:
-
First, we iterate over the elements of the
nums
array while calculating the remainder when each elementnums[j]
is divided byd
(i.e.,nums[j] % d
). We use this to determine what the correspondingnums[i]
's remainder should be in order to have(nums[i] + nums[j] + nums[k]) % d == 0
. -
For any given index
j
, we look ahead to the elementsnums[k]
for allk
such thatk > j
and calculatex
, which is equal to(d - (nums[j] + nums[k]) % d) % d
. This represents the remainder we need from somenums[i]
(wherei < j
) so that the sum of the three elements is divisible byd
. -
The calculated
x
is then used to check in ourcnt
hash table how many times we've seen such a remainder before indexj
. We sum up these occurrences in a variableans
, which ultimately holds the total number of triplets that satisfy the problem's condition. -
Before we move on to the next
j
, we increase the count ofnums[j]
's remainder in the hash table by 1, i.e.,cnt[nums[j] % d] += 1
, representing that we have seen another occurrence of this particular remainder. -
Once we exhaust all possibilities for
j
and its correspondingk
, the variableans
will hold the correct answer, which is then returned.
One of the clever patterns employed in this solution is the recognition that for triplets (i, j, k)
to satisfy our condition, it is not necessary to track each i
explicitly. By using remainders and counting their occurrences, we implicitly handle all possible i
candidates while iterating over j
and k
. This avoids the need for a full, expensive three-level loop and thus significantly improves the time complexity.
The use of mathematics to track the needed remainder part instead of the raw sum also reduces the memory complexity as we only need to store counts for each possible remainder range from 0
to d-1
. This is much smaller than the potential range of the sums if d
is small relative to the values in nums
.
This algorithm runs in O(n^2)
time complexity, with n
being the size of the array nums
, which is much more efficient than the brute force O(n^3)
. The space complexity is O(d)
due to the hash table storing at most d
different remainders.
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:
Consider nums = [2, 3, 5, 7, 11]
and d = 5
.
We initiate a defaultdict(int)
which will serve as the cnt
hash table to store the counts of seen remainders.
As there are no values in cnt
yet, all remainders start with a count of 0.
We start by iterating through the array:
-
For
nums[0] = 2
, the remainder when divided byd
is 2.- There are no previous elements, so we just move to the next index.
- Update
cnt[2]
to 1 (since 2 % 5 = 2).
-
At
nums[1] = 3
, the remainder is 3.- We are looking for a triplet that includes
nums[i]
,nums[j]=3
, and some futurenums[k]
. - Update
cnt[3]
to 1 (3 % 5 = 3).
- We are looking for a triplet that includes
-
Moving on to
nums[2] = 5
, the remainder is 0.- Now, let's look ahead for future
k
values:- For a potential
k
withnums[k] = 7
(next element), we need a remainderx
(needed from some previousnums[i]
), which is(5 - (3 + 7) % 5) % 5 = 0
. We look intocnt
and see thatcnt[0] = 0
(0 hasn't been seen yet as a remainder before index2
), so no triplets can be formed here.
- For a potential
- Update
cnt[0]
to 1 (5 % 5 = 0).
- Now, let's look ahead for future
-
At
nums[3] = 7
, the remainder when divided byd
is 2.- Let's look ahead:
- For a future
k
withnums[k] = 11
(next element), we need a remainderx
from some previousnums[i]
which is(5 - (3 + 11) % 5) % 5 = 1
. We don't have any elements that left a remainder of 1 until now (cnt[1]
is 0), so no triplet can be formed.
- For a future
- Update
cnt[2]
to 2.
- Let's look ahead:
-
Finally, at
nums[4] = 11
, the remainder is 1.- No need to look ahead because
11
is the last element. - Update
cnt[1]
to 1 (11 % 5 = 1).
- No need to look ahead because
Since there are no triplets that satisfy (nums[i] + nums[j] + nums[k]) % 5 == 0
, our answer thus far is 0
.
In this example, we failed to find any valid triplets, but the process demonstrates how we would systematically check for them using the remainders and the cnt
hash table to avoid redundancy. In cases where nums
contains the right combinations, the cnt
table would help us tally up the valid triplet counts quickly and efficiently.
Solution Implementation
1from collections import defaultdict
2
3class Solution:
4 def divisibleTripletCount(self, nums: List[int], divisor: int) -> int:
5 # Dictionary to store the frequency of remainders
6 remainder_count = defaultdict(int)
7
8 # Initialize the count of valid triplets
9 valid_triplet_count = 0
10
11 # Length of the input list
12 n = len(nums)
13
14 # Loop over the list to find valid triplets
15 for j in range(n):
16 for k in range(j + 1, n):
17 # Compute the remainder needed from the third element
18 # for the sum of the triplet to be divisible by 'divisor'
19 needed_remainder = (divisor - (nums[j] + nums[k]) % divisor) % divisor
20
21 # Add the count of numbers previously encountered with the needed remainder
22 valid_triplet_count += remainder_count[needed_remainder]
23
24 # Increment the count of the remainder for the current element
25 remainder_count[nums[j] % divisor] += 1
26
27 # Return the total count of valid triplets
28 return valid_triplet_count
29
1class Solution {
2
3 /**
4 * Counts the number of divisible triplets in the given array.
5 * A treeplit is a sequence of three numbers (a, b, c), such that (a + b + c) % d == 0.
6 *
7 * @param nums The input array of integers.
8 * @param d The divisor for checking divisibility.
9 * @return The count of divisible triplets.
10 */
11 public int divisibleTripletCount(int[] nums, int d) {
12 // Map to store frequency counts of numbers modulo d
13 Map<Integer, Integer> frequencyCounts = new HashMap<>();
14 // The answer (count of divisible triplets)
15 int answer = 0;
16 // Length of the nums array
17 int length = nums.length;
18
19 // Iterate through the pairs (j, k) where j < k
20 for (int j = 0; j < length; ++j) {
21 for (int k = j + 1; k < length; ++k) {
22 // Calculate the modulo of the negative sum of nums[j] + nums[k]
23 // This is the number needed to complete triplet to be divisible by d
24 int neededModulo = (d - (nums[j] + nums[k]) % d) % d;
25 // Add to answer the count of numbers that have the neededModulo
26 answer += frequencyCounts.getOrDefault(neededModulo, 0);
27 }
28 // Update the frequencyCounts map with the current number's modulo
29 frequencyCounts.merge(nums[j] % d, 1, Integer::sum);
30 }
31
32 // Return the total count of divisible triplets
33 return answer;
34 }
35}
36
1#include <vector>
2#include <unordered_map>
3using namespace std;
4
5class Solution {
6public:
7 // Function to count the number of triplets such that sum of two elements is divisible by 'd'.
8 int divisibleTripletCount(vector<int>& nums, int d) {
9 // 'counts' is used to store the frequency of elements mod 'd'.
10 unordered_map<int, int> counts;
11 int answer = 0;
12 int n = nums.size(); // Length of the array.
13
14 // Iterate over all pairs of numbers in 'nums'.
15 for (int j = 0; j < n; ++j) {
16 for (int k = j + 1; k < n; ++k) {
17 // Calculate the complement that would make the sum of a triplet divisible by 'd'.
18 int complement = (d - (nums[j] + nums[k]) % d) % d;
19
20 // Add the count of the complement to the answer.
21 answer += counts[complement];
22 }
23 // For the number at position 'j', increment its frequency.
24 counts[nums[j] % d]++;
25 }
26 return answer; // Return the total count of divisible triplets.
27 }
28};
29
1function divisibleTripletCount(nums: number[], divisor: number): number {
2 const arrayLength = nums.length; // get the length of the nums array
3 const remainderCount: Map<number, number> = new Map(); // map to count occurrences of each remainder
4 let tripletCount = 0; // initialize triplet count to zero
5
6 // Iterate over each pair of numbers in the array
7 for (let middleIndex = 0; middleIndex < arrayLength; ++middleIndex) {
8 for (let lastIndex = middleIndex + 1; lastIndex < arrayLength; ++lastIndex) {
9 // calculate the required value to complete the triplet
10 const requiredValue = (divisor - ((nums[middleIndex] + nums[lastIndex]) % divisor)) % divisor;
11 // increase the count of valid triplets by the amount found in remainderCount
12 tripletCount += remainderCount.get(requiredValue) || 0;
13 }
14 // update the remainder count for the current number
15 const currentRemainder = nums[middleIndex] % divisor;
16 remainderCount.set(currentRemainder, (remainderCount.get(currentRemainder) || 0) + 1);
17 }
18
19 return tripletCount; // return the total count of divisible triplets
20}
21
Time and Space Complexity
The given Python code defines a method divisibleTripletCount
within a Solution
class that counts triplets in an array, where the sum of each triplet is divisible by a given integer d
. The code primarily involves two nested loops: the outer loop iterates over each element j
of the array, and the inner loop iterates over the elements following j
(k
). Here's a breakdown of the complexities:
Time Complexity:
The time complexity is determined by the number of nested iterations over the input list nums
.
- The outer loop runs for
n
iterations, withn
being the length ofnums
. - For each iteration of the outer loop, the inner loop executes
n - j - 1
times, which results in an average case ofn/2
times per iteration of the outer loop.
This creates a total of around n * (n/2)
comparisons, simplifying to (n^2)/2
which is in the order of O(n^2)
. This is because constants are ignored in Big O notation.
Therefore, the time complexity of the code is O(n^2)
.
Space Complexity:
The space complexity refers to the amount of additional memory used by the program in relation to the input size.
-
A
defaultdict(int)
is created to store counts ofnums[j] % d
. In the worst case, we would have to store a number for every unique value ofnums[j] % d
. However, since% d
createsd
possible remainders, thedefaultdict
will at most containd
entries. -
We also have a few integer variables (
ans
,n
,x
), but these do not scale with input sizen
, and thus contribute a constant amount to the space complexity.
The space complexity of the code is therefore dictated by the defaultdict
size, along with a small constant for the variables used, so it is O(d)
. However, given that d
is a single integer value and not related to the size of the input array nums
, the d
in the space complexity could be considered a constant factor.
Thus, the space complexity of the code is O(1)
if d
is considered constant with respect to n
. However, since the reference answer suggests that the space complexity is O(n)
, it seems there might be a presumption that the array might contain up to n
distinct values modulo d
, binding the space complexity to the length of the array nums
. Under this assumption, the space complexity would indeed be O(n)
.
In conclusion, the time complexity of the code is O(n^2)
, and the space complexity, depending on the context, is either O(1)
or O(n)
based on the aforementioned reasoning.
Learn more about how to find time and space complexity quickly using problem constraints.
What does the following code do?
1def f(arr1, arr2):
2 i, j = 0, 0
3 new_arr = []
4 while i < len(arr1) and j < len(arr2):
5 if arr1[i] < arr2[j]:
6 new_arr.append(arr1[i])
7 i += 1
8 else:
9 new_arr.append(arr2[j])
10 j += 1
11 new_arr.extend(arr1[i:])
12 new_arr.extend(arr2[j:])
13 return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2 int i = 0, j = 0;
3 List<Integer> newArr = new ArrayList<>();
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.add(arr1[i]);
8 i++;
9 } else {
10 newArr.add(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.add(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.add(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
1function f(arr1, arr2) {
2 let i = 0, j = 0;
3 let newArr = [];
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.push(arr1[i]);
8 i++;
9 } else {
10 newArr.push(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.push(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.push(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
Recommended Readings
LeetCode 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 we
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
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!