LeetCode 2964 - Number of Divisible Triplet Sums
The problem gives us an integer array nums and an integer d. We must count how many triplets of indices (i, j, k) satisfy two conditions: 1. The indices are strictly increasing, meaning i < j < k. 2. The sum of the corresponding elements is divisible by d.
Difficulty: 🟡 Medium
Topics: Array, Hash Table
Solution
LeetCode 2964 - Number of Divisible Triplet Sums
Problem Understanding
The problem gives us an integer array nums and an integer d. We must count how many triplets of indices (i, j, k) satisfy two conditions:
- The indices are strictly increasing, meaning
i < j < k. - The sum of the corresponding elements is divisible by
d.
In other words, for every possible combination of three distinct positions in the array, we check whether:
$$(nums[i] + nums[j] + nums[k]) \bmod d = 0$$
If the condition is true, that triplet contributes one to the answer.
The input array can contain up to 1000 elements, and each element can be as large as 10^9. The divisor d can also be as large as 10^9.
The array size is the most important constraint. A naive solution would examine every possible triplet, which requires checking:
$$\binom{1000}{3} \approx 166,000,000$$
triplets. This is far too many for an efficient solution.
Since divisibility only depends on remainders modulo d, the actual values are less important than their residues. This observation allows us to avoid enumerating all triplets explicitly.
Some important edge cases include arrays with fewer than three valid combinations, arrays where every triplet is valid, arrays where no triplet is valid, and situations where d is much larger than all numbers in the array. The problem guarantees valid inputs, so we do not need to handle invalid arrays or invalid divisors.
Approaches
Brute Force
The most direct approach is to try every possible triplet (i, j, k).
We use three nested loops:
- The first loop chooses
i. - The second loop chooses
j > i. - The third loop chooses
k > j.
For each triplet, we compute the sum and check whether it is divisible by d.
This approach is clearly correct because it examines every possible triplet exactly once and counts those satisfying the condition.
Unfortunately, its time complexity is O(n³). With n = 1000, this results in roughly 166 million triplet checks, which is too slow.
Optimal Approach
Instead of fixing three elements, we can fix the middle index j and count valid pairs (i, k) around it.
Suppose we choose a specific j.
For every k > j, we know:
$$(nums[i] + nums[j] + nums[k]) \bmod d = 0$$
Rearranging:
$$nums[i] \bmod d = (- (nums[j] + nums[k])) \bmod d$$
This means that once j and k are fixed, we only need to know how many previous elements have the required remainder.
We maintain a hash map containing remainder frequencies for all indices before j.
Then for every k > j, we:
- Compute the required remainder.
- Look up how many earlier elements have that remainder.
- Add that count to the answer.
This converts the problem from checking triplets explicitly into counting matching remainders.
Since each pair (j, k) is processed once, the complexity becomes O(n²).
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n³) | O(1) | Enumerates every triplet directly |
| Optimal | O(n²) | O(min(n, d)) | Uses remainder frequency counting with a hash map |
Algorithm Walkthrough
- Compute the remainder of each number modulo
d. Since divisibility only depends on remainders, we can work entirely with these values. - Initialize the answer to zero.
- Iterate through the array choosing each possible middle index
j. - Maintain a hash map called
remainder_countthat stores the frequencies of remainders appearing beforej. Initially, before processing a givenj, this map contains all indicesi < j. - For every index
ksuch thatk > j, compute:
$$required = (d - (nums[j] + nums[k]) \bmod d) \bmod d$$
This is the remainder needed from some earlier index i.
6. Look up required in the hash map. Every occurrence corresponds to a valid choice of i, so add that frequency to the answer.
7. After finishing all k values for the current j, insert the remainder of nums[j] into the hash map. This ensures it becomes available as a possible i for future middle indices.
8. Continue until all middle indices have been processed.
9. Return the accumulated answer.
Why it works
At every iteration, the hash map contains exactly the remainders of elements whose indices are less than the current middle index j. For each pair (j, k), the algorithm calculates the unique remainder that would make the triplet sum divisible by d. Every occurrence of that remainder in the hash map corresponds to a valid index i < j. Since each valid triplet is counted exactly once when its (j, k) pair is processed, the final answer is correct.
Python Solution
from typing import List
from collections import defaultdict
class Solution:
def divisibleTripletCount(self, nums: List[int], d: int) -> int:
n = len(nums)
remainders = [num % d for num in nums]
answer = 0
remainder_count = defaultdict(int)
for j in range(n):
for k in range(j + 1, n):
required = (d - (remainders[j] + remainders[k]) % d) % d
answer += remainder_count[required]
remainder_count[remainders[j]] += 1
return answer
The implementation begins by converting every number into its remainder modulo d. This simplifies all future divisibility calculations.
The variable remainder_count stores frequencies of remainders seen before the current middle index. At the beginning of each iteration for j, the map contains exactly the values that can serve as valid indices i.
For every k > j, the code computes the remainder needed from an earlier element. Looking up that remainder in the hash map immediately tells us how many valid choices of i exist.
After all pairs involving the current middle index have been processed, the current remainder is inserted into the map so that it can participate as an earlier index in future iterations.
This directly implements the algorithm described above and avoids enumerating all triplets explicitly.
Go Solution
func divisibleTripletCount(nums []int, d int) int {
n := len(nums)
remainders := make([]int, n)
for i := 0; i < n; i++ {
remainders[i] = nums[i] % d
}
answer := 0
remainderCount := make(map[int]int)
for j := 0; j < n; j++ {
for k := j + 1; k < n; k++ {
required := (d - (remainders[j]+remainders[k])%d) % d
answer += remainderCount[required]
}
remainderCount[remainders[j]]++
}
return answer
}
The Go implementation follows exactly the same logic as the Python version. A map[int]int replaces Python's defaultdict(int), and missing keys automatically return the zero value 0.
The maximum possible answer is:
$$\binom{1000}{3} = 166,167,000$$
which safely fits within Go's int type on all supported LeetCode platforms.
Worked Examples
Example 1
Input
nums = [3,3,4,7,8]
d = 5
Remainders:
[3,3,4,2,3]
| j | remainder_count before processing | k | Required Remainder | Matches | Answer |
|---|---|---|---|---|---|
| 0 | {} | 1 | 4 | 0 | 0 |
| 0 | {} | 2 | 3 | 0 | 0 |
| 0 | {} | 3 | 0 | 0 | 0 |
| 0 | {} | 4 | 4 | 0 | 0 |
| add 3 | {3:1} | ||||
| 1 | {3:1} | 2 | 3 | 1 | 1 |
| 1 | {3:1} | 3 | 0 | 0 | 1 |
| 1 | {3:1} | 4 | 4 | 0 | 1 |
| add 3 | {3:2} | ||||
| 2 | {3:2} | 3 | 4 | 0 | 1 |
| 2 | {3:2} | 4 | 3 | 2 | 3 |
| add 4 | {3:2,4:1} |
Final answer:
3
Example 2
Input
nums = [3,3,3,3]
d = 3
Remainders:
[0,0,0,0]
| j | Map Before | k | Required | Matches | Answer |
|---|---|---|---|---|---|
| 0 | {} | 1 | 0 | 0 | 0 |
| 0 | {} | 2 | 0 | 0 | 0 |
| 0 | {} | 3 | 0 | 0 | 0 |
| add 0 | {0:1} | ||||
| 1 | {0:1} | 2 | 0 | 1 | 1 |
| 1 | {0:1} | 3 | 0 | 1 | 2 |
| add 0 | {0:2} | ||||
| 2 | {0:2} | 3 | 0 | 2 | 4 |
Final answer:
4
Example 3
Input
nums = [3,3,3,3]
d = 6
Remainders:
[3,3,3,3]
| j | Map Before | k | Required | Matches | Answer |
|---|---|---|---|---|---|
| 0 | {} | 1 | 0 | 0 | 0 |
| 0 | {} | 2 | 0 | 0 | 0 |
| 0 | {} | 3 | 0 | 0 | 0 |
| add 3 | {3:1} | ||||
| 1 | {3:1} | 2 | 0 | 0 | 0 |
| 1 | {3:1} | 3 | 0 | 0 | 0 |
| add 3 | {3:2} | ||||
| 2 | {3:2} | 3 | 0 | 0 | 0 |
Final answer:
0
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n²) | Every pair (j, k) is processed exactly once |
| Space | O(min(n, d)) | Hash map stores distinct remainders encountered so far |
The outer loop iterates through all possible middle indices, and the inner loop iterates through all later indices. This results in approximately n(n-1)/2 pair evaluations, giving O(n²) time complexity.
The hash map stores frequencies of remainders that have appeared before the current position. There can be at most n distinct remainders observed, so the space complexity is O(min(n, d)).
Test Cases
from typing import List
s = Solution()
assert s.divisibleTripletCount([3, 3, 4, 7, 8], 5) == 3 # example 1
assert s.divisibleTripletCount([3, 3, 3, 3], 3) == 4 # example 2
assert s.divisibleTripletCount([3, 3, 3, 3], 6) == 0 # example 3
assert s.divisibleTripletCount([1], 2) == 0 # fewer than three elements
assert s.divisibleTripletCount([1, 2], 3) == 0 # still no triplet possible
assert s.divisibleTripletCount([1, 2, 3], 3) == 0 # single triplet not divisible
assert s.divisibleTripletCount([1, 2, 3], 6) == 1 # single triplet divisible
assert s.divisibleTripletCount([0, 0, 0, 0], 1) == 4 # all triplets valid
assert s.divisibleTripletCount([5, 10, 15, 20], 5) == 4 # all remainders zero
assert s.divisibleTripletCount([1, 1, 1, 1, 1], 2) == 10 # every triplet sums to 3
assert s.divisibleTripletCount([1000000000, 1000000000, 1000000000], 1000000000) == 1 # large values
assert s.divisibleTripletCount([1, 2, 4, 8, 16], 7) == 2 # mixed remainders
Test Summary
| Test | Why |
|---|---|
[3,3,4,7,8], d=5 |
Official example with multiple valid triplets |
[3,3,3,3], d=3 |
Every triplet is valid |
[3,3,3,3], d=6 |
No triplet is valid |
[1], d=2 |
Minimum array size |
[1,2], d=3 |
Fewer than three elements |
[1,2,3], d=3 |
Single triplet that fails |
[1,2,3], d=6 |
Single triplet that succeeds |
[0,0,0,0], d=1 |
Divisor of one |
[5,10,15,20], d=5 |
All remainders are zero |
[1,1,1,1,1], d=2 |
Large number of valid combinations |
Large 10^9 values |
Verifies handling of maximum element sizes |
[1,2,4,8,16], d=7 |
General mixed remainder scenario |
Edge Cases
Arrays With Fewer Than Three Elements
If the array contains fewer than three elements, no valid triplet can exist because the condition requires three distinct indices. The implementation naturally handles this because the nested loops never find a valid (j, k) pair, so the answer remains zero.
All Numbers Have Remainder Zero
When every element is divisible by d, every remainder is zero. In this situation every triplet sum is also divisible by d, and the answer should equal the total number of triplets. The hash map correctly accumulates counts of remainder 0, causing every valid combination to be counted.
Very Large Values and Divisors
Both nums[i] and d can be as large as 10^9. A common mistake is to work directly with full sums repeatedly. The implementation immediately reduces every value modulo d, keeping all remainder calculations within manageable bounds and ensuring efficient processing regardless of input magnitude.
No Valid Remainder Exists
For some (j, k) pairs, the required remainder may not appear among earlier elements. In that case the hash map lookup returns zero and contributes nothing to the answer. This avoids special-case handling and correctly represents the fact that no valid i exists for that pair.