1363. Largest Multiple of Three
Problem Description
You are given an array of digits digits
where each element is a single digit from 0 to 9. Your task is to form the largest possible number that is a multiple of three by concatenating some (or all) of the given digits in any order you choose.
The key requirements are:
- The resulting number must be divisible by 3
- You want to make this number as large as possible
- You can use any subset of the given digits
- You can arrange the selected digits in any order
- The answer should be returned as a string (since it might be too large for integer data types)
- Leading zeros should be removed from the result (except when the answer is "0")
- If it's impossible to form a multiple of 3, return an empty string
For example, if you're given digits = [8, 1, 9]
, you could form "981", "918", "891", etc. Since all these numbers are divisible by 3 (because 8+1+9=18, which is divisible by 3), you would return "981" as it's the largest possible arrangement.
The challenge involves two main considerations:
- Divisibility by 3: A number is divisible by 3 if and only if the sum of its digits is divisible by 3
- Maximizing the value: Once you know which digits to use, arrange them in descending order to get the largest number
Intuition
To build the largest number that's divisible by 3, we need to understand a fundamental property: a number is divisible by 3 if and only if the sum of its digits is divisible by 3.
This means we need to select digits whose sum ≡ 0 (mod 3)
. But we also want the largest possible number, which requires:
- Using as many digits as possible (more digits = larger number)
- Arranging selected digits in descending order
The challenge is finding the optimal subset of digits. We can't just greedily take all digits, as their sum might not be divisible by 3. We also can't just take the largest digits, as we might miss a valid combination.
This leads us to think about dynamic programming. We can track the maximum number of digits we can select for each possible remainder when divided by 3. Specifically, we define f[i][j]
as the maximum count of digits we can select from the first i
digits such that their sum modulo 3 equals j
.
Why track all three remainders (0, 1, 2)? Because as we add more digits, we might transition from one remainder to another. For instance, if we currently have digits summing to remainder 1, and we add a digit with remainder 2, we get remainder 0 (since (1 + 2) % 3 = 0
).
The state transition follows naturally: for each digit, we either skip it or include it. If we include digit x
, we transition from remainder (j - x % 3 + 3) % 3
to remainder j
, gaining one more digit in our selection.
After computing the DP table, f[n][0]
tells us the maximum number of digits we can select to form a multiple of 3. To reconstruct which digits to use, we backtrack through our decisions. Starting from f[n][0]
, we check whether each digit was included by seeing if including it would have led to our current state.
Finally, to maximize the numeric value, we sort the digits initially and select from largest to smallest during backtracking, then handle the edge case of leading zeros.
Learn more about Greedy, Math, Dynamic Programming and Sorting patterns.
Solution Approach
The solution implements dynamic programming with backtracking to find the largest multiple of 3. Here's the step-by-step implementation:
1. Sorting the Input
digits.sort()
We sort the digits in ascending order first. This helps during backtracking - we'll process larger digits first when reconstructing the answer.
2. Dynamic Programming Table Setup
f = [[-inf] * 3 for _ in range(n + 1)]
f[0][0] = 0
We create a 2D DP table f[i][j]
where:
i
ranges from 0 ton
(number of digits considered)j
ranges from 0 to 2 (possible remainders when divided by 3)f[i][j]
stores the maximum count of digits we can select from the firsti
digits such that their summod 3 = j
Initialize f[0][0] = 0
(selecting 0 digits gives sum 0, which has remainder 0), and all other states to -inf
(impossible states).
3. Filling the DP Table
for i, x in enumerate(digits, 1):
for j in range(3):
f[i][j] = max(f[i - 1][j], f[i - 1][(j - x % 3 + 3) % 3] + 1)
For each digit x
at position i
, and each remainder j
:
- Option 1: Don't select digit
x
, sof[i][j] = f[i-1][j]
- Option 2: Select digit
x
, transitioning from state where previous remainder was(j - x % 3 + 3) % 3
The formula (j - x % 3 + 3) % 3
calculates the previous remainder needed. If we want remainder j
after adding x
, we need the previous sum to have remainder (j - x % 3)
. The +3
and % 3
handle negative values correctly.
4. Check for Valid Solution
if f[n][0] <= 0: return ""
If f[n][0] ≤ 0
, no valid selection exists that forms a multiple of 3.
5. Backtracking to Find Selected Digits
arr = []
j = 0
for i in range(n, 0, -1):
k = (j - digits[i - 1] % 3 + 3) % 3
if f[i - 1][k] + 1 == f[i][j]:
arr.append(digits[i - 1])
j = k
Starting from f[n][0]
, we trace back our decisions:
- For each position
i
, calculate what the previous remainderk
would have been if we selecteddigits[i-1]
- If
f[i-1][k] + 1 == f[i][j]
, then we selected this digit - Add selected digits to
arr
and updatej = k
for the next iteration - Since we process from largest to smallest indices and digits were sorted,
arr
will contain digits in descending order
6. Handle Leading Zeros
i = 0
while i < len(arr) - 1 and arr[i] == 0:
i += 1
return "".join(map(str, arr[i:]))
Remove unnecessary leading zeros, keeping at least one digit if all selected digits are zeros. Convert the result to a string and return.
The time complexity is O(n log n + 3n) = O(n log n)
for sorting and DP computation. The space complexity is O(n)
for the DP table.
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 digits = [8, 6, 7, 1, 0]
.
Step 1: Sort the digits
After sorting: [0, 1, 6, 7, 8]
Step 2: Initialize DP table
We create f[i][j]
where i
is from 0 to 5 (number of digits) and j
is from 0 to 2 (remainders).
Initial state: f[0][0] = 0
, all others = -inf
Step 3: Fill DP table Process each digit and update the table:
For digit 0
(i=1):
f[1][0] = max(f[0][0], f[0][0] + 1) = max(0, 1) = 1
f[1][1] = max(f[0][1], f[0][1] + 1) = max(-inf, -inf) = -inf
f[1][2] = max(f[0][2], f[0][2] + 1) = max(-inf, -inf) = -inf
For digit 1
(i=2):
f[2][0] = max(f[1][0], f[1][2] + 1) = max(1, -inf) = 1
f[2][1] = max(f[1][1], f[1][0] + 1) = max(-inf, 2) = 2
f[2][2] = max(f[1][2], f[1][1] + 1) = max(-inf, -inf) = -inf
For digit 6
(i=3):
f[3][0] = max(f[2][0], f[2][0] + 1) = max(1, 2) = 2
f[3][1] = max(f[2][1], f[2][1] + 1) = max(2, 3) = 3
f[3][2] = max(f[2][2], f[2][2] + 1) = max(-inf, -inf) = -inf
For digit 7
(i=4):
f[4][0] = max(f[3][0], f[3][2] + 1) = max(2, -inf) = 2
f[4][1] = max(f[3][1], f[3][0] + 1) = max(3, 3) = 3
f[4][2] = max(f[3][2], f[3][1] + 1) = max(-inf, 4) = 4
For digit 8
(i=5):
f[5][0] = max(f[4][0], f[4][1] + 1) = max(2, 4) = 4
f[5][1] = max(f[4][1], f[4][2] + 1) = max(3, 5) = 5
f[5][2] = max(f[4][2], f[4][0] + 1) = max(4, 3) = 4
Step 4: Check validity
f[5][0] = 4 > 0
, so a valid solution exists with 4 digits.
Step 5: Backtrack to find selected digits
Starting from f[5][0] = 4
, trace back:
-
At i=5 (digit 8): Check if we used it
- Previous remainder needed:
(0 - 8%3 + 3) % 3 = (0 - 2 + 3) % 3 = 1
- Check:
f[4][1] + 1 = 3 + 1 = 4 = f[5][0]
✓ - Select digit 8, move to state
f[4][1]
- Previous remainder needed:
-
At i=4 (digit 7): Check if we used it
- Previous remainder needed:
(1 - 7%3 + 3) % 3 = (1 - 1 + 3) % 3 = 0
- Check:
f[3][0] + 1 = 2 + 1 = 3 = f[4][1]
✓ - Select digit 7, move to state
f[3][0]
- Previous remainder needed:
-
At i=3 (digit 6): Check if we used it
- Previous remainder needed:
(0 - 6%3 + 3) % 3 = (0 - 0 + 3) % 3 = 0
- Check:
f[2][0] + 1 = 1 + 1 = 2 = f[3][0]
✓ - Select digit 6, move to state
f[2][0]
- Previous remainder needed:
-
At i=2 (digit 1): Check if we used it
- Previous remainder needed:
(0 - 1%3 + 3) % 3 = (0 - 1 + 3) % 3 = 2
- Check:
f[1][2] + 1 = -inf + 1 ≠ 1
✗ - Don't select digit 1, stay at state
f[2][0]
- Previous remainder needed:
-
At i=1 (digit 0): Check if we used it
- Previous remainder needed:
(0 - 0%3 + 3) % 3 = 0
- Check:
f[0][0] + 1 = 0 + 1 = 1 = f[1][0]
✓ - Select digit 0, move to state
f[0][0]
- Previous remainder needed:
Selected digits in order: [8, 7, 6, 0]
Step 6: Handle leading zeros No leading zeros in our result. The final answer is "8760".
Verification: 8 + 7 + 6 + 0 = 21, which is divisible by 3. ✓
Solution Implementation
1class Solution:
2 def largestMultipleOfThree(self, digits: List[int]) -> str:
3 # Sort digits in ascending order for later reconstruction
4 digits.sort()
5 num_digits = len(digits)
6
7 # Dynamic programming table
8 # dp[i][j] represents the maximum number of digits we can use
9 # from the first i digits to form a number with remainder j when divided by 3
10 dp = [[-float('inf')] * 3 for _ in range(num_digits + 1)]
11
12 # Base case: using 0 digits gives remainder 0 with 0 digits used
13 dp[0][0] = 0
14
15 # Fill the DP table
16 for i, digit in enumerate(digits, 1):
17 for remainder in range(3):
18 # Option 1: Don't include current digit
19 dont_include = dp[i - 1][remainder]
20
21 # Option 2: Include current digit
22 # Calculate what remainder we need before adding this digit
23 prev_remainder = (remainder - digit % 3 + 3) % 3
24 include = dp[i - 1][prev_remainder] + 1
25
26 # Take the maximum of both options
27 dp[i][remainder] = max(dont_include, include)
28
29 # If we can't form a number divisible by 3, return empty string
30 if dp[num_digits][0] <= 0:
31 return ""
32
33 # Reconstruct the solution by backtracking through the DP table
34 result_digits = []
35 current_remainder = 0
36
37 # Traverse from the last digit to the first
38 for i in range(num_digits, 0, -1):
39 current_digit = digits[i - 1]
40
41 # Calculate what the remainder would have been before including this digit
42 prev_remainder = (current_remainder - current_digit % 3 + 3) % 3
43
44 # Check if this digit was included in the optimal solution
45 if dp[i - 1][prev_remainder] + 1 == dp[i][current_remainder]:
46 # This digit was included, add it to result
47 result_digits.append(current_digit)
48 current_remainder = prev_remainder
49
50 # Remove leading zeros, keeping at least one digit
51 leading_zero_index = 0
52 while leading_zero_index < len(result_digits) - 1 and result_digits[leading_zero_index] == 0:
53 leading_zero_index += 1
54
55 # Convert digits to string and return
56 return "".join(map(str, result_digits[leading_zero_index:]))
57
1class Solution {
2 public String largestMultipleOfThree(int[] digits) {
3 // Sort digits in ascending order for later reconstruction
4 Arrays.sort(digits);
5 int numDigits = digits.length;
6
7 // dp[i][j] represents the maximum number of digits we can use
8 // from the first i digits to form a number with remainder j when divided by 3
9 int[][] dp = new int[numDigits + 1][3];
10
11 // Initialize with negative infinity to mark invalid states
12 final int NEGATIVE_INFINITY = 1 << 30;
13 for (int[] row : dp) {
14 Arrays.fill(row, -NEGATIVE_INFINITY);
15 }
16
17 // Base case: using 0 digits gives remainder 0 with 0 digits used
18 dp[0][0] = 0;
19
20 // Fill the DP table
21 for (int i = 1; i <= numDigits; ++i) {
22 for (int remainder = 0; remainder < 3; ++remainder) {
23 // Option 1: Don't include current digit
24 int skipCurrent = dp[i - 1][remainder];
25
26 // Option 2: Include current digit
27 // Calculate the previous remainder needed before adding current digit
28 int prevRemainder = (remainder - digits[i - 1] % 3 + 3) % 3;
29 int includeCurrent = dp[i - 1][prevRemainder] + 1;
30
31 // Take the maximum of both options
32 dp[i][remainder] = Math.max(skipCurrent, includeCurrent);
33 }
34 }
35
36 // If we can't form a number divisible by 3, return empty string
37 if (dp[numDigits][0] <= 0) {
38 return "";
39 }
40
41 // Reconstruct the solution by backtracking through the DP table
42 StringBuilder result = new StringBuilder();
43 int currentRemainder = 0;
44
45 for (int i = numDigits; i > 0; --i) {
46 // Check if current digit was included in the optimal solution
47 int prevRemainder = (currentRemainder - digits[i - 1] % 3 + 3) % 3;
48
49 if (dp[i - 1][prevRemainder] + 1 == dp[i][currentRemainder]) {
50 // Current digit was included
51 result.append(digits[i - 1]);
52 currentRemainder = prevRemainder;
53 }
54 }
55
56 // Remove leading zeros (keep at least one digit)
57 int startIndex = 0;
58 while (startIndex < result.length() - 1 && result.charAt(startIndex) == '0') {
59 ++startIndex;
60 }
61
62 return result.substring(startIndex);
63 }
64}
65
1class Solution {
2public:
3 string largestMultipleOfThree(vector<int>& digits) {
4 // Sort digits in ascending order for reconstruction later
5 sort(digits.begin(), digits.end());
6
7 int numDigits = digits.size();
8
9 // dp[i][j] represents the maximum number of digits we can use
10 // from the first i digits such that their sum mod 3 equals j
11 int dp[numDigits + 1][3];
12
13 // Initialize with negative infinity (impossible state)
14 memset(dp, -0x3f, sizeof(dp));
15
16 // Base case: using 0 digits gives sum 0 (which is 0 mod 3)
17 dp[0][0] = 0;
18
19 // Fill the DP table
20 for (int i = 1; i <= numDigits; ++i) {
21 for (int remainder = 0; remainder < 3; ++remainder) {
22 // Option 1: Don't include current digit
23 dp[i][remainder] = dp[i - 1][remainder];
24
25 // Option 2: Include current digit
26 // Calculate what remainder we need before adding current digit
27 int prevRemainder = (remainder - digits[i - 1] % 3 + 3) % 3;
28
29 // Take maximum of including or not including current digit
30 dp[i][remainder] = max(dp[i][remainder],
31 dp[i - 1][prevRemainder] + 1);
32 }
33 }
34
35 // If we can't form a number divisible by 3, return empty string
36 if (dp[numDigits][0] <= 0) {
37 return "";
38 }
39
40 // Reconstruct the answer by backtracking through DP table
41 string result;
42 int currentRemainder = 0; // We want sum divisible by 3
43
44 for (int i = numDigits; i >= 1; --i) {
45 // Check if current digit was included in optimal solution
46 int prevRemainder = (currentRemainder - digits[i - 1] % 3 + 3) % 3;
47
48 // If including this digit gives us the optimal value, add it to result
49 if (dp[i - 1][prevRemainder] + 1 == dp[i][currentRemainder]) {
50 result += digits[i - 1] + '0'; // Convert digit to char
51 currentRemainder = prevRemainder; // Update remainder for next iteration
52 }
53 }
54
55 // Remove leading zeros (except if the result is just "0")
56 int startIndex = 0;
57 while (startIndex < result.size() - 1 && result[startIndex] == '0') {
58 ++startIndex;
59 }
60
61 return result.substr(startIndex);
62 }
63};
64
1/**
2 * Finds the largest number that is a multiple of three from given digits
3 * @param digits - Array of single digits (0-9)
4 * @returns String representation of the largest multiple of three, or empty string if impossible
5 */
6function largestMultipleOfThree(digits: number[]): string {
7 // Sort digits in ascending order for processing
8 digits.sort((a, b) => a - b);
9 const digitCount = digits.length;
10
11 // Dynamic programming table
12 // dp[i][j] represents the maximum number of digits we can use from first i digits
13 // such that their sum modulo 3 equals j
14 const dp: number[][] = new Array(digitCount + 1)
15 .fill(0)
16 .map(() => new Array(3).fill(-Infinity));
17
18 // Base case: using 0 digits, sum is 0 (mod 3 is 0)
19 dp[0][0] = 0;
20
21 // Fill the DP table
22 for (let i = 1; i <= digitCount; ++i) {
23 for (let remainder = 0; remainder < 3; ++remainder) {
24 // Option 1: Don't include current digit
25 const excludeCurrent = dp[i - 1][remainder];
26
27 // Option 2: Include current digit
28 // Calculate previous remainder needed to reach current remainder
29 const previousRemainder = (remainder - (digits[i - 1] % 3) + 3) % 3;
30 const includeCurrent = dp[i - 1][previousRemainder] + 1;
31
32 dp[i][remainder] = Math.max(excludeCurrent, includeCurrent);
33 }
34 }
35
36 // Check if we can form a number divisible by 3
37 if (dp[digitCount][0] <= 0) {
38 return '';
39 }
40
41 // Backtrack to find which digits were selected
42 const selectedDigits: number[] = [];
43 let currentIndex = digitCount;
44 let currentRemainder = 0;
45
46 while (currentIndex > 0) {
47 // Calculate what remainder we would need if we included this digit
48 const previousRemainder = (currentRemainder - (digits[currentIndex - 1] % 3) + 3) % 3;
49
50 // Check if including this digit gives us the optimal solution
51 if (dp[currentIndex - 1][previousRemainder] + 1 === dp[currentIndex][currentRemainder]) {
52 selectedDigits.push(digits[currentIndex - 1]);
53 currentRemainder = previousRemainder;
54 }
55 currentIndex--;
56 }
57
58 // Remove leading zeros (keep at least one digit)
59 let leadingZeroIndex = 0;
60 while (leadingZeroIndex < selectedDigits.length - 1 && selectedDigits[leadingZeroIndex] === 0) {
61 leadingZeroIndex++;
62 }
63
64 // Return the result as a string
65 return selectedDigits.slice(leadingZeroIndex).join('');
66}
67
Time and Space Complexity
Time Complexity: O(n × log n)
The time complexity is dominated by the following operations:
digits.sort()
takesO(n × log n)
time wheren
is the length of the digits array- The nested loop for dynamic programming fills a
(n+1) × 3
table, which takesO(n × 3) = O(n)
time - The backtracking loop to construct the result array runs in
O(n)
time - The final loop to skip leading zeros takes
O(n)
time in the worst case - Converting the array to string with
"".join(map(str, arr[i:]))
takesO(n)
time
Since O(n × log n) + O(n) + O(n) + O(n) = O(n × log n)
, the overall time complexity is O(n × log n)
.
Space Complexity: O(n)
The space complexity consists of:
- The 2D DP table
f
with dimensions(n+1) × 3
, which usesO(n × 3) = O(n)
space - The result array
arr
which can contain up ton
elements, usingO(n)
space - The sorted digits array (if not sorting in-place) would use
O(n)
space - Other variables like
i
,j
,k
useO(1)
space
Therefore, the total space complexity is O(n) + O(n) + O(1) = O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Reconstruction Order Leading to Non-Optimal Results
The most critical pitfall in this solution is that the backtracking reconstruction doesn't guarantee the largest possible number. The current approach adds digits to result_digits
in the order they appear during backtracking (from largest index to smallest), but this doesn't ensure the digits are arranged to form the maximum value.
The Problem:
- When we sort digits in ascending order initially and backtrack from
i = n
toi = 1
, we're adding digits to our result based on DP decisions, not based on maximizing the final number. - For example, with
digits = [3, 1, 4, 1, 5, 9]
, the backtracking might select[9, 5, 4]
but arrange them as[9, 4, 5]
instead of[9, 5, 4]
.
The Solution: After collecting all selected digits during backtracking, sort them in descending order before constructing the final string:
# After backtracking and collecting result_digits
result_digits.sort(reverse=True) # Sort in descending order for maximum value
# Then handle leading zeros
leading_zero_index = 0
while leading_zero_index < len(result_digits) - 1 and result_digits[leading_zero_index] == 0:
leading_zero_index += 1
2. Edge Case: All Zeros
Another pitfall is improperly handling the case where all selected digits are zeros.
The Problem:
- If
digits = [0, 0, 0]
, the sum is 0 (divisible by 3), so we should return "0" - The current leading zero removal could accidentally return an empty string if not careful
The Solution: Ensure at least one digit remains after removing leading zeros:
# The current code handles this correctly with:
while leading_zero_index < len(result_digits) - 1 and result_digits[leading_zero_index] == 0:
leading_zero_index += 1
# The condition `< len(result_digits) - 1` ensures at least one digit remains
3. Unnecessary Initial Sorting
The initial digits.sort()
is actually counterproductive if we're going to sort the selected digits again later.
The Problem:
- Sorting the input doesn't help with the DP computation (order doesn't matter for calculating remainders)
- It might confuse the reconstruction logic
The Solution: Remove the initial sort and only sort the final selected digits:
def largestMultipleOfThree(self, digits: List[int]) -> str:
# Don't sort initially
num_digits = len(digits)
# ... DP computation remains the same ...
# After collecting result_digits in backtracking:
result_digits.sort(reverse=True) # Only sort here for maximum value
4. Floating Point Infinity vs Integer Constants
Using -float('inf')
for impossible states works but can be less efficient than using a large negative integer.
The Problem:
- Floating-point operations are generally slower than integer operations
- Mixed float/int comparisons can lead to unexpected behavior in edge cases
The Solution: Use a large negative integer constant instead:
# Instead of:
dp = [[-float('inf')] * 3 for _ in range(num_digits + 1)]
# Use:
NEG_INF = -10**9 # Or -(num_digits + 1) since max count is num_digits
dp = [[NEG_INF] * 3 for _ in range(num_digits + 1)]
How does merge sort divide the problem into subproblems?
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
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
Want a Structured Path to Master System Design Too? Don’t Miss This!