2964. Number of Divisible Triplet Sums
Problem Description
You are given a 0-indexed integer array nums and an integer d. Your task is to find the number of triplets (i, j, k) that satisfy the following conditions:
- The indices must follow the order:
i < j < k - The sum of the three elements at these indices must be divisible by
d, meaning(nums[i] + nums[j] + nums[k]) % d == 0
The solution uses a hash table to track remainders and iterates through the array. For each position j, it:
- Iterates through all positions
kwherek > j - Calculates what remainder value
xwould be needed from a previous elementnums[i](wherei < j) to make the sum divisible byd - The required remainder is calculated as
x = (d - (nums[j] + nums[k]) % d) % d - Adds the count of previously seen elements with remainder
xto the answer - After checking all
kvalues for a givenj, recordsnums[j] % din the hash table for future iterations
This approach ensures that we only count valid triplets where i < j < k by maintaining the hash table of remainders seen so far (elements before j) and checking elements after j (at position k).
Intuition
The key insight is recognizing that for three numbers to sum to a value divisible by d, we need (nums[i] + nums[j] + nums[k]) % d == 0. This can be rewritten as: we need to find nums[i] such that nums[i] % d equals a specific value that complements (nums[j] + nums[k]) % d to make the total divisible by d.
Instead of checking all possible triplets with three nested loops (which would be O(n³)), we can be smarter about it. The trick is to fix the middle element j and think about what happens:
- For elements to the right of
j(potentialkvalues), we can iterate through them - For elements to the left of
j(potentialivalues), we don't want to iterate through them again for each(j, k)pair
This leads us to the idea of preprocessing: as we move through the array with j, we can maintain information about all elements we've seen so far (which are valid i candidates). Specifically, we only care about the remainder when each element is divided by d, since that's what determines divisibility.
For a given pair (j, k), we know their sum modulo d is (nums[j] + nums[k]) % d. To make the total sum divisible by d, we need the third element nums[i] to have a remainder that "cancels out" this value. If (nums[j] + nums[k]) % d = r, then we need nums[i] % d = (d - r) % d.
By maintaining a hash table that counts how many elements we've seen with each possible remainder value, we can instantly know how many valid i values exist for any (j, k) pair. This reduces our time complexity from O(n³) to O(n²).
Solution Implementation
1from typing import List
2from collections import defaultdict
3
4class Solution:
5 def divisibleTripletCount(self, nums: List[int], d: int) -> int:
6 # Dictionary to count occurrences of each remainder when divided by d
7 # Key: remainder value, Value: count of elements with that remainder seen so far
8 remainder_count = defaultdict(int)
9
10 # Initialize result counter and get array length
11 result = 0
12 n = len(nums)
13
14 # Iterate through middle element j of potential triplets (i, j, k)
15 for j in range(n):
16 # For each k > j, check how many valid i < j exist
17 for k in range(j + 1, n):
18 # Calculate the required remainder for nums[i] to make the triplet sum divisible by d
19 # If (nums[i] + nums[j] + nums[k]) % d == 0, then nums[i] % d must equal required_remainder
20 required_remainder = (d - (nums[j] + nums[k]) % d) % d
21
22 # Add count of all valid i values (those before j with the required remainder)
23 result += remainder_count[required_remainder]
24
25 # After processing all k values for current j, add nums[j] to our remainder count
26 # This ensures it can be used as a potential i value for future j positions
27 remainder_count[nums[j] % d] += 1
28
29 return result
301class Solution {
2 public int divisibleTripletCount(int[] nums, int d) {
3 // Map to store frequency of remainders (when divided by d) for elements before index j
4 Map<Integer, Integer> remainderCount = new HashMap<>();
5 int totalTriplets = 0;
6 int arrayLength = nums.length;
7
8 // Iterate through each possible middle element of the triplet (index j)
9 for (int j = 0; j < arrayLength; ++j) {
10 // For current j, check all possible third elements (index k where k > j)
11 for (int k = j + 1; k < arrayLength; ++k) {
12 // Calculate the remainder needed from nums[i] to make the sum divisible by d
13 // If (nums[i] + nums[j] + nums[k]) % d == 0, then
14 // nums[i] % d == (d - (nums[j] + nums[k]) % d) % d
15 int requiredRemainder = (d - (nums[j] + nums[k]) % d) % d;
16
17 // Add count of all elements before j that have the required remainder
18 totalTriplets += remainderCount.getOrDefault(requiredRemainder, 0);
19 }
20
21 // After processing all k for current j, add nums[j] to the remainder count map
22 // This ensures nums[j] can be used as nums[i] for future iterations
23 remainderCount.merge(nums[j] % d, 1, Integer::sum);
24 }
25
26 return totalTriplets;
27 }
28}
291class Solution {
2public:
3 int divisibleTripletCount(vector<int>& nums, int d) {
4 // Map to store frequency of remainders when divided by d
5 // Key: remainder value, Value: count of elements with that remainder
6 unordered_map<int, int> remainderCount;
7
8 int result = 0;
9 int n = nums.size();
10
11 // Iterate through array considering each element as middle element (j)
12 for (int j = 0; j < n; ++j) {
13 // For current j, check all possible k values (k > j)
14 for (int k = j + 1; k < n; ++k) {
15 // Calculate required remainder for nums[i] to make triplet divisible by d
16 // We need: (nums[i] + nums[j] + nums[k]) % d == 0
17 // So: nums[i] % d == (d - (nums[j] + nums[k]) % d) % d
18 int requiredRemainder = (d - (nums[j] + nums[k]) % d) % d;
19
20 // Add count of all previous elements with the required remainder
21 // These will form valid triplets (i, j, k) where i < j < k
22 result += remainderCount[requiredRemainder];
23 }
24
25 // After processing all k values for current j,
26 // add nums[j] to the map for future iterations
27 remainderCount[nums[j] % d]++;
28 }
29
30 return result;
31 }
32};
331/**
2 * Counts the number of triplets (i, j, k) where i < j < k
3 * such that (nums[i] + nums[j] + nums[k]) is divisible by d
4 *
5 * @param nums - Array of integers
6 * @param d - The divisor to check divisibility against
7 * @returns Number of valid triplets
8 */
9function divisibleTripletCount(nums: number[], d: number): number {
10 const arrayLength: number = nums.length;
11 // Map to store frequency of remainders when divided by d
12 const remainderFrequency: Map<number, number> = new Map<number, number>();
13 let tripletCount: number = 0;
14
15 // Iterate through array considering element at index j as middle element
16 for (let j = 0; j < arrayLength; ++j) {
17 // For each j, check all possible k values where k > j
18 for (let k = j + 1; k < arrayLength; ++k) {
19 // Calculate required remainder for nums[i] to make the sum divisible by d
20 // If (nums[i] + nums[j] + nums[k]) % d = 0, then
21 // nums[i] % d must equal (d - (nums[j] + nums[k]) % d) % d
22 const requiredRemainder: number = (d - ((nums[j] + nums[k]) % d)) % d;
23
24 // Add count of all previous elements with the required remainder
25 tripletCount += remainderFrequency.get(requiredRemainder) || 0;
26 }
27
28 // After processing all k values for current j,
29 // add nums[j] to the map for future iterations
30 const currentRemainder: number = nums[j] % d;
31 remainderFrequency.set(currentRemainder, (remainderFrequency.get(currentRemainder) || 0) + 1);
32 }
33
34 return tripletCount;
35}
36Solution Approach
The solution uses a hash table combined with enumeration to efficiently count valid triplets.
Data Structure:
cnt: A hash table (defaultdict) that stores the count of remainders. Specifically,cnt[r]represents how many elements we've processed so far that have remainderrwhen divided byd.
Algorithm Steps:
-
Initialize an empty hash table
cntand setans = 0to track the total count of valid triplets. -
Iterate through the array with index
jfrom0ton-1:- For each
j, this element will serve as the middle element of our triplet
- For each
-
For each fixed
j, iterate through all elements after it with indexkfromj+1ton-1:- Calculate the sum
(nums[j] + nums[k]) % d - Determine what remainder we need from
nums[i]to make the total sum divisible byd:x = (d - (nums[j] + nums[k]) % d) % d- The outer modulo operation handles the case when
(nums[j] + nums[k]) % d = 0, ensuringx = 0instead ofx = d
- Add
cnt[x]to the answer, which represents how many validiindices exist for this(j, k)pair
- Calculate the sum
-
After processing all
kvalues for the currentj, update the hash table:- Add
nums[j] % dto the hash table by incrementingcnt[nums[j] % d] += 1 - This makes
nums[j]available as a potentialivalue for future iterations
- Add
Why this works:
- When we're at position
j, all elements beforej(indices0toj-1) have already been recorded in the hash table - These are exactly the valid candidates for index
isince we needi < j - By checking all
k > jand looking up the required remainder in our hash table, we count all valid triplets withjas the middle element - The order
i < j < kis naturally maintained by this approach
Time Complexity: O(n²) - two nested loops
Space Complexity: O(d) - the hash table can store at most d different remainders
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with a concrete example:
nums = [3, 3, 4, 7, 8]d = 5
We need to find triplets (i, j, k) where i < j < k and (nums[i] + nums[j] + nums[k]) % 5 == 0.
Initial State:
cnt = {}(empty hash table)ans = 0
Iteration 1: j = 0 (nums[j] = 3)
- Check all k > 0:
- k = 1:
nums[k] = 3- Sum needed:
(3 + 3) % 5 = 6 % 5 = 1 - Required remainder:
x = (5 - 1) % 5 = 4 cnt[4] = 0(no elements with remainder 4 seen yet)- ans += 0
- Sum needed:
- k = 2:
nums[k] = 4- Sum needed:
(3 + 4) % 5 = 7 % 5 = 2 - Required remainder:
x = (5 - 2) % 5 = 3 cnt[3] = 0(no elements with remainder 3 seen yet)- ans += 0
- Sum needed:
- k = 3:
nums[k] = 7- Sum needed:
(3 + 7) % 5 = 10 % 5 = 0 - Required remainder:
x = (5 - 0) % 5 = 0 cnt[0] = 0(no elements with remainder 0 seen yet)- ans += 0
- Sum needed:
- k = 4:
nums[k] = 8- Sum needed:
(3 + 8) % 5 = 11 % 5 = 1 - Required remainder:
x = (5 - 1) % 5 = 4 cnt[4] = 0- ans += 0
- Sum needed:
- k = 1:
- Update hash table:
3 % 5 = 3, socnt[3] = 1 - Current state:
cnt = {3: 1},ans = 0
Iteration 2: j = 1 (nums[j] = 3)
- Check all k > 1:
- k = 2:
nums[k] = 4- Sum needed:
(3 + 4) % 5 = 2 - Required remainder:
x = (5 - 2) % 5 = 3 cnt[3] = 1(we have one element with remainder 3: nums[0])- ans += 1 (found triplet: i=0, j=1, k=2)
- Sum needed:
- k = 3:
nums[k] = 7- Sum needed:
(3 + 7) % 5 = 0 - Required remainder:
x = (5 - 0) % 5 = 0 cnt[0] = 0- ans += 0
- Sum needed:
- k = 4:
nums[k] = 8- Sum needed:
(3 + 8) % 5 = 1 - Required remainder:
x = (5 - 1) % 5 = 4 cnt[4] = 0- ans += 0
- Sum needed:
- k = 2:
- Update hash table:
3 % 5 = 3, socnt[3] = 2 - Current state:
cnt = {3: 2},ans = 1
Iteration 3: j = 2 (nums[j] = 4)
- Check all k > 2:
- k = 3:
nums[k] = 7- Sum needed:
(4 + 7) % 5 = 11 % 5 = 1 - Required remainder:
x = (5 - 1) % 5 = 4 cnt[4] = 0- ans += 0
- Sum needed:
- k = 4:
nums[k] = 8- Sum needed:
(4 + 8) % 5 = 12 % 5 = 2 - Required remainder:
x = (5 - 2) % 5 = 3 cnt[3] = 2(we have two elements with remainder 3: nums[0] and nums[1])- ans += 2 (found triplets: i=0, j=2, k=4 and i=1, j=2, k=4)
- Sum needed:
- k = 3:
- Update hash table:
4 % 5 = 4, socnt[4] = 1 - Current state:
cnt = {3: 2, 4: 1},ans = 3
Iteration 4: j = 3 (nums[j] = 7)
- Check all k > 3:
- k = 4:
nums[k] = 8- Sum needed:
(7 + 8) % 5 = 15 % 5 = 0 - Required remainder:
x = (5 - 0) % 5 = 0 cnt[0] = 0- ans += 0
- Sum needed:
- k = 4:
- Update hash table:
7 % 5 = 2, socnt[2] = 1 - Current state:
cnt = {3: 2, 4: 1, 2: 1},ans = 3
Iteration 5: j = 4 (nums[j] = 8)
- No k > 4 exists, so no pairs to check
- Update hash table:
8 % 5 = 3, socnt[3] = 3
Final Answer: 3
The three valid triplets are:
- (0, 1, 2): nums[0] + nums[1] + nums[2] = 3 + 3 + 4 = 10, which is divisible by 5
- (0, 2, 4): nums[0] + nums[2] + nums[4] = 3 + 4 + 8 = 15, which is divisible by 5
- (1, 2, 4): nums[1] + nums[2] + nums[4] = 3 + 4 + 8 = 15, which is divisible by 5
Time and Space Complexity
Time Complexity: O(n²)
The code contains two nested loops:
- The outer loop iterates through
jfrom0ton-1, runningntimes - The inner loop iterates through
kfromj+1ton-1, running approximatelyn-j-1times for eachj - The total iterations across both loops is
(n-1) + (n-2) + ... + 1 + 0 = n(n-1)/2, which simplifies toO(n²) - Inside the inner loop, all operations (modulo calculation, dictionary lookup, and addition) are
O(1) - After the inner loop, the dictionary update operation is also
O(1)
Therefore, the overall time complexity is O(n²).
Space Complexity: O(n)
The space usage comes from:
- The
cntdictionary (defaultdict) which stores the count of remainders when elements are divided byd - Since
dis a constant and each element's remainder when divided bydcan only be in the range[0, d-1], the dictionary can have at mostmin(n, d)unique keys - In the worst case where all elements have different remainders or
d ≥ n, the dictionary can store up tonentries - Other variables (
ans,n,j,k,x) use constant spaceO(1)
Therefore, the overall space complexity is O(n).
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Remainder Calculation
One of the most common mistakes is incorrectly calculating the required remainder for nums[i].
Pitfall Code:
# WRONG: This fails when (nums[j] + nums[k]) % d == 0 required_remainder = d - (nums[j] + nums[k]) % d
Issue: When (nums[j] + nums[k]) % d == 0, this formula gives required_remainder = d, but we actually need required_remainder = 0 since we're looking for elements with remainder 0.
Solution:
# CORRECT: The outer modulo ensures we get 0 instead of d required_remainder = (d - (nums[j] + nums[k]) % d) % d
2. Updating Hash Table at Wrong Time
Another critical mistake is updating the hash table at the wrong point in the iteration.
Pitfall Code:
for j in range(n):
# WRONG: Adding nums[j] to hash table BEFORE checking k values
remainder_count[nums[j] % d] += 1
for k in range(j + 1, n):
required_remainder = (d - (nums[j] + nums[k]) % d) % d
result += remainder_count[required_remainder]
Issue: This would count invalid triplets where i == j, violating the constraint i < j < k.
Solution:
for j in range(n):
for k in range(j + 1, n):
required_remainder = (d - (nums[j] + nums[k]) % d) % d
result += remainder_count[required_remainder]
# CORRECT: Add nums[j] AFTER processing all k values
remainder_count[nums[j] % d] += 1
3. Using Regular Dictionary Instead of DefaultDict
Using a regular dictionary without proper initialization can cause KeyError.
Pitfall Code:
remainder_count = {} # Regular dictionary # ... result += remainder_count[required_remainder] # KeyError if key doesn't exist
Solution:
# Option 1: Use defaultdict
from collections import defaultdict
remainder_count = defaultdict(int)
# Option 2: Use get() method with default value
remainder_count = {}
result += remainder_count.get(required_remainder, 0)
4. Handling Negative Numbers Incorrectly
If the array contains negative numbers, the modulo operation in Python handles them correctly, but it's important to understand the behavior.
Example: For d = 5 and nums[j] + nums[k] = -3:
- Python's
(-3) % 5returns2(not-3) - The required remainder calculation still works correctly
Best Practice: Always test with negative numbers to ensure your solution handles them properly.
How many times is a tree node visited in a depth first search?
Recommended Readings
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 If you prefer videos here's a video that explains recursion in a fun and easy way 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 assets algo monster recursion jpg You first call Ben and ask him
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!