1262. Greatest Sum Divisible by Three
Problem Description
You are given an integer array nums
. Your task is to find the maximum possible sum of elements from the array such that this sum is divisible by three.
You can select any subset of numbers from the array (including selecting all numbers or selecting none). The goal is to maximize the sum while ensuring that the total sum, when divided by 3, gives a remainder of 0.
For example:
- If
nums = [3, 6, 5, 1, 8]
, you could select[3, 6]
for a sum of 9 (divisible by 3), or[3, 6, 1, 8]
for a sum of 18 (also divisible by 3). The maximum sum divisible by 3 would be 18. - If no combination of numbers can form a sum divisible by 3, and you cannot select any numbers to get 0, then return 0.
The problem requires finding the optimal selection of array elements to achieve the largest sum that satisfies the divisibility constraint.
Intuition
When we need to find a sum divisible by 3, we need to think about remainders. Any number, when divided by 3, gives a remainder of 0, 1, or 2. The key insight is that the remainder of a sum depends only on the remainders of its parts.
For example, if we have numbers with remainders 1 and 2, their sum has remainder (1 + 2) % 3 = 0
. This means we can track the maximum possible sum for each remainder value (0, 1, or 2) as we process the array.
Think of it this way: as we go through each number in the array, we have three "buckets" representing the maximum sums we can achieve with remainders 0, 1, and 2. For each new number, we can either:
- Skip it - keeping our current bucket values unchanged
- Add it to one of our existing sums - which might change which remainder bucket the sum belongs to
When we add a number x
to a sum that has remainder j
, the new sum will have remainder (j + x) % 3
. So if we want to achieve remainder j
, we could have taken a previous sum with remainder (j - x) % 3
and added x
to it.
This naturally leads to a dynamic programming approach where we track:
f[i][j]
= maximum sum using numbers from the firsti
elements, where the sum has remainderj
when divided by 3
We build up our answer by considering each number one by one, updating our three remainder buckets, and finally returning the maximum sum with remainder 0, which is f[n][0]
.
Learn more about Greedy, Dynamic Programming and Sorting patterns.
Solution Approach
We implement the dynamic programming solution using a 2D array f
where f[i][j]
represents the maximum sum we can achieve using the first i
numbers such that the sum modulo 3 equals j
.
Initialization:
- Create a 2D array
f
of size(n+1) × 3
, wheren
is the length of the input array - Initialize all values to
-inf
(negative infinity) to represent impossible states - Set
f[0][0] = 0
because with 0 numbers selected, we can achieve a sum of 0 (which has remainder 0)
State Transition:
For each number x
at position i
(1-indexed), we update all three remainder states:
f[i][j] = max(f[i-1][j], f[i-1][(j-x) % 3] + x)
This formula means:
f[i-1][j]
: Don't include the current numberx
, keep the maximum sum with remainderj
from previous numbersf[i-1][(j-x) % 3] + x
: Include the current numberx
. To get remainderj
after addingx
, we need the previous sum to have remainder(j-x) % 3
Implementation Details:
- We iterate through each number in the array using
enumerate(nums, 1)
to get 1-indexed positions - For each position
i
and each possible remainderj
(0, 1, 2), we calculate the maximum between:- Not taking the current number
- Taking the current number and adding it to a previous sum
Computing the Previous Remainder:
The expression (j - x) % 3
in Python handles negative numbers correctly. For example:
- If
j = 0
andx = 2
, then(0 - 2) % 3 = 1
in Python - This means to get remainder 0 after adding a number with remainder 2, we need a previous sum with remainder 1
Final Answer:
After processing all numbers, f[n][0]
gives us the maximum sum that is divisible by 3 (remainder 0).
The time complexity is O(n)
since we iterate through each number once and perform constant operations for each of the 3 remainder states. The space complexity is O(n)
for the 2D DP array.
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 nums = [3, 6, 5, 1, 8]
.
We'll build a DP table f[i][j]
where i
represents using the first i
numbers, and j
represents the remainder when the sum is divided by 3.
Initial Setup:
- Initialize
f[0][0] = 0
(sum of 0 has remainder 0) - Initialize
f[0][1] = -inf
andf[0][2] = -inf
(impossible to have non-zero remainders with no numbers)
Processing each number:
Step 1: Process 3 (remainder = 0)
- For remainder 0:
f[1][0] = max(f[0][0], f[0][(0-3)%3] + 3) = max(0, f[0][0] + 3) = max(0, 3) = 3
- For remainder 1:
f[1][1] = max(f[0][1], f[0][(1-3)%3] + 3) = max(-inf, f[0][1] + 3) = max(-inf, -inf) = -inf
- For remainder 2:
f[1][2] = max(f[0][2], f[0][(2-3)%3] + 3) = max(-inf, f[0][2] + 3) = max(-inf, -inf) = -inf
Step 2: Process 6 (remainder = 0)
- For remainder 0:
f[2][0] = max(f[1][0], f[1][(0-6)%3] + 6) = max(3, f[1][0] + 6) = max(3, 9) = 9
- For remainder 1:
f[2][1] = max(f[1][1], f[1][(1-6)%3] + 6) = max(-inf, f[1][1] + 6) = -inf
- For remainder 2:
f[2][2] = max(f[1][2], f[1][(2-6)%3] + 6) = max(-inf, f[1][2] + 6) = -inf
Step 3: Process 5 (remainder = 2)
- For remainder 0:
f[3][0] = max(f[2][0], f[2][(0-5)%3] + 5) = max(9, f[2][1] + 5) = max(9, -inf) = 9
- For remainder 1:
f[3][1] = max(f[2][1], f[2][(1-5)%3] + 5) = max(-inf, f[2][2] + 5) = -inf
- For remainder 2:
f[3][2] = max(f[2][2], f[2][(2-5)%3] + 5) = max(-inf, f[2][0] + 5) = max(-inf, 14) = 14
Step 4: Process 1 (remainder = 1)
- For remainder 0:
f[4][0] = max(f[3][0], f[3][(0-1)%3] + 1) = max(9, f[3][2] + 1) = max(9, 15) = 15
- For remainder 1:
f[4][1] = max(f[3][1], f[3][(1-1)%3] + 1) = max(-inf, f[3][0] + 1) = max(-inf, 10) = 10
- For remainder 2:
f[4][2] = max(f[3][2], f[3][(2-1)%3] + 1) = max(14, f[3][1] + 1) = max(14, -inf) = 14
Step 5: Process 8 (remainder = 2)
- For remainder 0:
f[5][0] = max(f[4][0], f[4][(0-8)%3] + 8) = max(15, f[4][1] + 8) = max(15, 18) = 18
- For remainder 1:
f[5][1] = max(f[4][1], f[4][(1-8)%3] + 8) = max(10, f[4][2] + 8) = max(10, 22) = 22
- For remainder 2:
f[5][2] = max(f[4][2], f[4][(2-8)%3] + 8) = max(14, f[4][0] + 8) = max(14, 23) = 23
Final Answer:
f[5][0] = 18
, which represents the maximum sum divisible by 3.
This corresponds to selecting [3, 6, 1, 8]
which sum to 18.
Solution Implementation
1class Solution:
2 def maxSumDivThree(self, nums: List[int]) -> int:
3 """
4 Find the maximum sum of elements that is divisible by 3.
5
6 Dynamic Programming approach:
7 - dp[i][j] represents the maximum sum using first i elements
8 where the sum modulo 3 equals j
9 - For each number, we can either include it or exclude it
10
11 Args:
12 nums: List of non-negative integers
13
14 Returns:
15 Maximum sum divisible by 3
16 """
17 n = len(nums)
18
19 # Initialize DP table
20 # dp[i][j] = maximum sum using first i elements where sum % 3 == j
21 # Use -inf for impossible states
22 dp = [[-float('inf')] * 3 for _ in range(n + 1)]
23
24 # Base case: using 0 elements, sum is 0 (which is divisible by 3)
25 dp[0][0] = 0
26
27 # Process each number
28 for i in range(1, n + 1):
29 current_num = nums[i - 1]
30
31 # For each possible remainder (0, 1, 2)
32 for remainder in range(3):
33 # Option 1: Don't include current number
34 dp[i][remainder] = dp[i - 1][remainder]
35
36 # Option 2: Include current number
37 # Find what remainder we need from previous state
38 prev_remainder = (remainder - current_num) % 3
39 dp[i][remainder] = max(
40 dp[i][remainder],
41 dp[i - 1][prev_remainder] + current_num
42 )
43
44 # Return maximum sum divisible by 3 (remainder = 0)
45 return dp[n][0]
46
1class Solution {
2 public int maxSumDivThree(int[] nums) {
3 int arrayLength = nums.length;
4 // Use a large negative value to represent impossible states
5 final int NEGATIVE_INFINITY = -(1 << 30);
6
7 // Dynamic programming table
8 // dp[i][j] represents the maximum sum using first i elements
9 // where the sum modulo 3 equals j
10 int[][] dp = new int[arrayLength + 1][3];
11
12 // Initialize base case: with 0 elements
13 // dp[0][0] = 0 (sum of 0 elements is 0, which mod 3 = 0)
14 // dp[0][1] and dp[0][2] are impossible states
15 dp[0][1] = NEGATIVE_INFINITY;
16 dp[0][2] = NEGATIVE_INFINITY;
17
18 // Fill the DP table
19 for (int i = 1; i <= arrayLength; i++) {
20 int currentNum = nums[i - 1];
21
22 // For each possible remainder when divided by 3
23 for (int remainder = 0; remainder < 3; remainder++) {
24 // Two choices for current element:
25 // 1. Don't include current element: dp[i-1][remainder]
26 // 2. Include current element: dp[i-1][previousRemainder] + currentNum
27 // where previousRemainder is calculated such that
28 // (previousRemainder + currentNum % 3) % 3 = remainder
29 int previousRemainder = (remainder - currentNum % 3 + 3) % 3;
30
31 dp[i][remainder] = Math.max(
32 dp[i - 1][remainder], // Don't take current element
33 dp[i - 1][previousRemainder] + currentNum // Take current element
34 );
35 }
36 }
37
38 // Return the maximum sum that is divisible by 3
39 // (sum % 3 == 0, so we need dp[arrayLength][0])
40 return dp[arrayLength][0];
41 }
42}
43
1class Solution {
2public:
3 int maxSumDivThree(vector<int>& nums) {
4 int n = nums.size();
5 const int INF = 1 << 30; // Large negative value to represent impossible states
6
7 // dp[i][j] represents the maximum sum using first i elements
8 // where the sum modulo 3 equals j
9 int dp[n + 1][3];
10
11 // Base case: with 0 elements, only sum 0 is possible
12 dp[0][0] = 0;
13 dp[0][1] = -INF; // Impossible to get remainder 1 with no elements
14 dp[0][2] = -INF; // Impossible to get remainder 2 with no elements
15
16 // Process each element
17 for (int i = 1; i <= n; ++i) {
18 int currentNum = nums[i - 1];
19
20 // For each possible remainder when divided by 3
21 for (int remainder = 0; remainder < 3; ++remainder) {
22 // Option 1: Don't include current number
23 int excludeCurrent = dp[i - 1][remainder];
24
25 // Option 2: Include current number
26 // We need previous sum with remainder that, when added to currentNum,
27 // gives us the target remainder
28 int prevRemainder = (remainder - currentNum % 3 + 3) % 3;
29 int includeCurrent = dp[i - 1][prevRemainder] + currentNum;
30
31 // Take maximum of both options
32 dp[i][remainder] = max(excludeCurrent, includeCurrent);
33 }
34 }
35
36 // Return maximum sum divisible by 3 (remainder = 0)
37 return dp[n][0];
38 }
39};
40
1/**
2 * Finds the maximum sum of elements that is divisible by three
3 * @param nums - Array of integers to select from
4 * @returns Maximum sum divisible by three
5 */
6function maxSumDivThree(nums: number[]): number {
7 const arrayLength: number = nums.length;
8 const NEGATIVE_INFINITY: number = 1 << 30;
9
10 // dp[i][j] represents the maximum sum using first i elements
11 // where the sum modulo 3 equals j
12 const dp: number[][] = Array(arrayLength + 1)
13 .fill(0)
14 .map(() => Array(3).fill(-NEGATIVE_INFINITY));
15
16 // Base case: sum of 0 elements is 0 (divisible by 3)
17 dp[0][0] = 0;
18
19 // Process each element
20 for (let elementIndex = 1; elementIndex <= arrayLength; ++elementIndex) {
21 const currentElement: number = nums[elementIndex - 1];
22
23 // For each possible remainder when divided by 3
24 for (let remainder = 0; remainder < 3; ++remainder) {
25 // Option 1: Don't include current element
26 const excludeCurrent: number = dp[elementIndex - 1][remainder];
27
28 // Option 2: Include current element
29 // Calculate previous remainder needed to achieve current remainder
30 const previousRemainder: number = (remainder - (currentElement % 3) + 3) % 3;
31 const includeCurrent: number = dp[elementIndex - 1][previousRemainder] + currentElement;
32
33 // Take maximum of both options
34 dp[elementIndex][remainder] = Math.max(excludeCurrent, includeCurrent);
35 }
36 }
37
38 // Return maximum sum where remainder is 0 (divisible by 3)
39 return dp[arrayLength][0];
40}
41
Time and Space Complexity
Time Complexity: O(n)
, where n
is the length of the input array nums
. The algorithm uses two nested loops - the outer loop iterates through all n
elements, and the inner loop iterates exactly 3 times for each element (constant factor). Therefore, the overall time complexity is O(n × 3) = O(n)
.
Space Complexity: O(n)
. The code creates a 2D array f
with dimensions (n+1) × 3
, which requires O(n)
space. According to the reference answer, since f[i][j]
only depends on f[i-1][j]
and f[i-1][(j-x) % 3]
from the previous row, we could optimize this to use a rolling array approach with only two rows or even a single row with careful updates, reducing the space complexity to O(1)
(constant space for storing just 3 values for the current and previous states).
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of Impossible States
The Pitfall: A common mistake is initializing the DP table with 0s instead of negative infinity. This leads to incorrect results because 0 is a valid sum, and using it as a placeholder for "impossible" states causes the algorithm to consider invalid transitions.
# WRONG: Initializing with zeros
dp = [[0] * 3 for _ in range(n + 1)]
dp[0][0] = 0 # This looks correct but...
Why it's wrong: When we have dp[0][1] = 0
and dp[0][2] = 0
, the algorithm thinks we can achieve sums with remainders 1 and 2 using zero elements, which is impossible. This leads to incorrect state transitions when including numbers.
The Solution:
# CORRECT: Initialize with negative infinity for impossible states
dp = [[-float('inf')] * 3 for _ in range(n + 1)]
dp[0][0] = 0 # Only this state is possible initially
2. Space Optimization Without Proper State Copying
The Pitfall: When optimizing space to use only two 1D arrays (or even one), developers often forget to properly copy states between iterations:
# WRONG: Modifying the same array in-place
dp = [-float('inf')] * 3
dp[0] = 0
for num in nums:
for j in range(3):
prev_remainder = (j - num) % 3
dp[j] = max(dp[j], dp[prev_remainder] + num) # Bug: dp[prev_remainder] might be already updated!
Why it's wrong: When updating dp[j]
in the same array, we might use values that have already been updated in the current iteration, breaking the DP logic.
The Solution:
# CORRECT: Use a temporary array or process in reverse order
dp = [-float('inf')] * 3
dp[0] = 0
for num in nums:
new_dp = dp.copy() # Create a copy for the new state
for j in range(3):
prev_remainder = (j - num) % 3
new_dp[j] = max(dp[j], dp[prev_remainder] + num)
dp = new_dp
3. Modulo Operation Confusion with Negative Numbers
The Pitfall: In some programming languages, the modulo operation with negative numbers behaves differently. Developers might write:
# Potentially problematic in other languages prev_remainder = (j - current_num) % 3
Why it could be wrong: In languages like Java or C++, -1 % 3
returns -1
, not 2
as in Python. This would require additional handling.
The Solution for other languages:
# Universal solution that works in all languages prev_remainder = ((j - current_num) % 3 + 3) % 3
This ensures the result is always in the range [0, 2] regardless of the language's modulo implementation.
What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
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
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Want a Structured Path to Master System Design Too? Don’t Miss This!