Facebook Pixel

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:

  1. Divisibility by 3: A number is divisible by 3 if and only if the sum of its digits is divisible by 3
  2. Maximizing the value: Once you know which digits to use, arrange them in descending order to get the largest number
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Using as many digits as possible (more digits = larger number)
  2. 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 to n (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 first i digits such that their sum mod 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, so f[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 remainder k would have been if we selected digits[i-1]
  • If f[i-1][k] + 1 == f[i][j], then we selected this digit
  • Add selected digits to arr and update j = 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 Evaluator

Example 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]
  • 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]
  • 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]
  • 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]
  • 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]

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() takes O(n × log n) time where n is the length of the digits array
  • The nested loop for dynamic programming fills a (n+1) × 3 table, which takes O(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:])) takes O(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 uses O(n × 3) = O(n) space
  • The result array arr which can contain up to n elements, using O(n) space
  • The sorted digits array (if not sorting in-place) would use O(n) space
  • Other variables like i, j, k use O(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 to i = 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)]
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How does merge sort divide the problem into subproblems?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More