Facebook Pixel

3334. Find the Maximum Factor Score of Array

MediumArrayMathNumber Theory
Leetcode Link

Problem Description

You are given an integer array nums.

The factor score of an array is calculated as the product of two values:

  • The LCM (Least Common Multiple) of all elements in the array
  • The GCD (Greatest Common Divisor) of all elements in the array

Your task is to find the maximum possible factor score after removing at most one element from the array. This means you can either:

  1. Keep all elements (remove nothing)
  2. Remove exactly one element

Important notes:

  • For a single-element array, both LCM and GCD equal the element itself
  • For an empty array, the factor score is 0
  • The factor score = LCM(array) × GCD(array)

For example, if nums = [6, 12, 18]:

  • Without removing any element: GCD(6,12,18) = 6, LCM(6,12,18) = 36, factor score = 6 × 36 = 216
  • After removing 6: GCD(12,18) = 6, LCM(12,18) = 36, factor score = 6 × 36 = 216
  • After removing 12: GCD(6,18) = 6, LCM(6,18) = 18, factor score = 6 × 18 = 108
  • After removing 18: GCD(6,12) = 6, LCM(6,12) = 12, factor score = 6 × 12 = 72

The maximum factor score would be 216.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

To find the maximum factor score, we need to consider all possible scenarios: keeping all elements or removing exactly one element. The brute force approach would be to try removing each element one by one and calculate the factor score for each case, but this would involve recalculating GCD and LCM from scratch each time, which is inefficient.

The key insight is that when we remove element at index i, the resulting array is essentially the combination of two parts:

  • Elements before index i (prefix: nums[0...i-1])
  • Elements after index i (suffix: nums[i+1...n-1])

The GCD and LCM of the resulting array would be:

  • GCD = gcd(GCD_of_prefix, GCD_of_suffix)
  • LCM = lcm(LCM_of_prefix, LCM_of_suffix)

This observation leads us to a preprocessing strategy. If we precompute the GCD and LCM values for all suffixes, we can efficiently calculate the factor score when any element is removed.

As we iterate through the array from left to right:

  • We maintain running prefix GCD and LCM values
  • For each position i, we can instantly know what the GCD and LCM would be if we removed element i by combining:
    • The prefix values (elements before i)
    • The precomputed suffix values (elements after i)

This way, we only need one pass to precompute suffix values and another pass to check all possibilities, reducing the time complexity significantly. The solution also handles the case where we don't remove any element (which is just the GCD and LCM of the entire array).

Learn more about Math patterns.

Solution Approach

The implementation uses prefix and suffix arrays to efficiently calculate the factor score for all possible scenarios.

Step 1: Initialize Suffix Arrays

We create two arrays to store suffix GCD and LCM values:

  • suf_gcd[i] stores the GCD of all elements from index i to the end
  • suf_lcm[i] stores the LCM of all elements from index i to the end
suf_gcd = [0] * (n + 1)  # Extra element for boundary condition
suf_lcm = [0] * n + [1]  # Initialize last element as 1 for LCM

Step 2: Build Suffix Arrays (Right to Left)

We iterate from the last element to the first, building up the suffix values:

for i in range(n - 1, -1, -1):
    suf_gcd[i] = gcd(suf_gcd[i + 1], nums[i])
    suf_lcm[i] = lcm(suf_lcm[i + 1], nums[i])

For each position i, we combine the current element nums[i] with the suffix starting at i+1.

Step 3: Calculate Initial Answer (No Removal)

First, we consider keeping all elements:

ans = suf_gcd[0] * suf_lcm[0]

This gives us the factor score of the entire array.

Step 4: Check All Removal Scenarios

We maintain prefix GCD and LCM as we iterate through the array:

pre_gcd, pre_lcm = 0, 1  # Identity values for GCD and LCM
for i, x in enumerate(nums):
    # Calculate score if we remove element at index i
    ans = max(ans, gcd(pre_gcd, suf_gcd[i + 1]) * lcm(pre_lcm, suf_lcm[i + 1]))
  
    # Update prefix values for next iteration
    pre_gcd = gcd(pre_gcd, x)
    pre_lcm = lcm(pre_lcm, x)

When removing element at index i:

  • The remaining array's GCD = gcd(prefix_gcd, suffix_gcd)
  • The remaining array's LCM = lcm(prefix_lcm, suffix_lcm)
  • Factor score = GCD × LCM

Key Implementation Details:

  1. Identity Values: We use 0 for GCD and 1 for LCM as identity values. This ensures gcd(0, x) = x and lcm(1, x) = x.

  2. Boundary Handling: The suffix arrays have an extra element to handle the case when we're at the last position (empty suffix).

  3. Maximum Tracking: We continuously update ans with the maximum factor score found.

The algorithm runs in O(n × log(max_value)) time, where the logarithmic factor comes from the GCD/LCM calculations, and uses O(n) extra space for the suffix arrays.

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 nums = [4, 6, 12].

Step 1: Build Suffix Arrays

We initialize suffix arrays with extra space:

  • suf_gcd = [0, 0, 0, 0] (4 elements)
  • suf_lcm = [0, 0, 0, 1] (last element is 1)

Building from right to left:

  • i = 2: suf_gcd[2] = gcd(0, 12) = 12, suf_lcm[2] = lcm(1, 12) = 12
  • i = 1: suf_gcd[1] = gcd(12, 6) = 6, suf_lcm[1] = lcm(12, 6) = 12
  • i = 0: suf_gcd[0] = gcd(6, 4) = 2, suf_lcm[0] = lcm(12, 4) = 12

Final suffix arrays:

  • suf_gcd = [2, 6, 12, 0]
  • suf_lcm = [12, 12, 12, 1]

Step 2: Calculate Initial Answer (Keep All Elements)

Factor score with all elements = suf_gcd[0] × suf_lcm[0] = 2 × 12 = 24

Step 3: Check Each Removal Scenario

Initialize prefix values: pre_gcd = 0, pre_lcm = 1

Iteration 1 (i = 0, removing 4):

  • Remaining elements: [6, 12]
  • GCD = gcd(pre_gcd=0, suf_gcd[1]=6) = 6
  • LCM = lcm(pre_lcm=1, suf_lcm[1]=12) = 12
  • Factor score = 6 × 12 = 72
  • Update: ans = max(24, 72) = 72
  • Update prefix: pre_gcd = gcd(0, 4) = 4, pre_lcm = lcm(1, 4) = 4

Iteration 2 (i = 1, removing 6):

  • Remaining elements: [4, 12]
  • GCD = gcd(pre_gcd=4, suf_gcd[2]=12) = 4
  • LCM = lcm(pre_lcm=4, suf_lcm[2]=12) = 12
  • Factor score = 4 × 12 = 48
  • Update: ans = max(72, 48) = 72
  • Update prefix: pre_gcd = gcd(4, 6) = 2, pre_lcm = lcm(4, 6) = 12

Iteration 3 (i = 2, removing 12):

  • Remaining elements: [4, 6]
  • GCD = gcd(pre_gcd=2, suf_gcd[3]=0) = 2
  • LCM = lcm(pre_lcm=12, suf_lcm[3]=1) = 12
  • Factor score = 2 × 12 = 24
  • Update: ans = max(72, 24) = 72

Final Answer: 72 (achieved by removing element 4)

The solution efficiently considers all scenarios:

  • Keep all: factor score = 24
  • Remove 4: factor score = 72 ✓ (maximum)
  • Remove 6: factor score = 48
  • Remove 12: factor score = 24

Solution Implementation

1class Solution:
2    def maxScore(self, nums: List[int]) -> int:
3        from math import gcd
4      
5        def lcm(a: int, b: int) -> int:
6            """Calculate the least common multiple of two numbers."""
7            if a == 0 or b == 0:
8                return 0
9            return abs(a * b) // gcd(a, b)
10      
11        n = len(nums)
12      
13        # Build suffix arrays for GCD and LCM values from right to left
14        suffix_gcd = [0] * (n + 1)  # suffix_gcd[i] = GCD of nums[i:]
15        suffix_lcm = [1] * (n + 1)  # suffix_lcm[i] = LCM of nums[i:]
16      
17        # Calculate suffix GCD and LCM for each position
18        for i in range(n - 1, -1, -1):
19            suffix_gcd[i] = gcd(suffix_gcd[i + 1], nums[i])
20            suffix_lcm[i] = lcm(suffix_lcm[i + 1], nums[i])
21      
22        # Initialize answer with score using all elements
23        max_score = suffix_gcd[0] * suffix_lcm[0]
24      
25        # Track prefix GCD and LCM as we iterate through the array
26        prefix_gcd = 0
27        prefix_lcm = 1
28      
29        # Try removing each element and calculate the score
30        for i in range(n):
31            # Score when removing nums[i]: combine prefix (before i) and suffix (after i)
32            score_without_current = gcd(prefix_gcd, suffix_gcd[i + 1]) * lcm(prefix_lcm, suffix_lcm[i + 1])
33            max_score = max(max_score, score_without_current)
34          
35            # Update prefix values to include current element
36            prefix_gcd = gcd(prefix_gcd, nums[i])
37            prefix_lcm = lcm(prefix_lcm, nums[i])
38      
39        return max_score
40
1class Solution {
2    /**
3     * Calculates the maximum score by finding the optimal product of GCD and LCM
4     * after potentially removing one element from the array.
5     * Score = GCD of remaining elements * LCM of remaining elements
6     * 
7     * @param nums the input array of integers
8     * @return the maximum possible score
9     */
10    public long maxScore(int[] nums) {
11        int n = nums.length;
12      
13        // Arrays to store suffix GCD and LCM values
14        // suffixGcd[i] = GCD of all elements from index i to n-1
15        // suffixLcm[i] = LCM of all elements from index i to n-1
16        long[] suffixGcd = new long[n + 1];
17        long[] suffixLcm = new long[n + 1];
18      
19        // Initialize base case: empty suffix has LCM of 1 (identity for LCM)
20        suffixLcm[n] = 1;
21      
22        // Build suffix arrays from right to left
23        for (int i = n - 1; i >= 0; i--) {
24            suffixGcd[i] = gcd(suffixGcd[i + 1], nums[i]);
25            suffixLcm[i] = lcm(suffixLcm[i + 1], nums[i]);
26        }
27      
28        // Calculate score using all elements (no removal)
29        long maxScore = suffixGcd[0] * suffixLcm[0];
30      
31        // Track prefix GCD and LCM as we iterate
32        long prefixGcd = 0;  // Identity for GCD is 0
33        long prefixLcm = 1;  // Identity for LCM is 1
34      
35        // Try removing each element and calculate the score
36        for (int i = 0; i < n; i++) {
37            // Score when removing element at index i:
38            // GCD of (prefix before i) and (suffix after i) * LCM of (prefix before i) and (suffix after i)
39            long currentScore = gcd(prefixGcd, suffixGcd[i + 1]) * lcm(prefixLcm, suffixLcm[i + 1]);
40            maxScore = Math.max(maxScore, currentScore);
41          
42            // Update prefix values to include current element
43            prefixGcd = gcd(prefixGcd, nums[i]);
44            prefixLcm = lcm(prefixLcm, nums[i]);
45        }
46      
47        return maxScore;
48    }
49
50    /**
51     * Calculates the Greatest Common Divisor (GCD) using Euclidean algorithm
52     * 
53     * @param a first number
54     * @param b second number
55     * @return GCD of a and b
56     */
57    private long gcd(long a, long b) {
58        return b == 0 ? a : gcd(b, a % b);
59    }
60
61    /**
62     * Calculates the Least Common Multiple (LCM) using the formula:
63     * LCM(a, b) = (a * b) / GCD(a, b)
64     * Division is performed before multiplication to avoid overflow
65     * 
66     * @param a first number
67     * @param b second number
68     * @return LCM of a and b
69     */
70    private long lcm(long a, long b) {
71        return a / gcd(a, b) * b;
72    }
73}
74
1class Solution {
2public:
3    long long maxScore(vector<int>& nums) {
4        int n = nums.size();
5      
6        // suffix[i] stores the GCD of all elements from index i to n-1
7        vector<long long> suffixGcd(n + 1, 0);
8        // suffix[i] stores the LCM of all elements from index i to n-1
9        vector<long long> suffixLcm(n + 1, 1);
10      
11        // Build suffix arrays from right to left
12        for (int i = n - 1; i >= 0; --i) {
13            suffixGcd[i] = gcd(suffixGcd[i + 1], nums[i]);
14            suffixLcm[i] = lcm(suffixLcm[i + 1], nums[i]);
15        }
16      
17        // Initialize answer with score when no element is removed (all elements included)
18        long long maxResult = suffixGcd[0] * suffixLcm[0];
19      
20        // Track prefix GCD and LCM as we iterate
21        long long prefixGcd = 0;
22        long long prefixLcm = 1;
23      
24        // Try removing each element and calculate the score
25        for (int i = 0; i < n; ++i) {
26            // Score when removing element at index i:
27            // GCD of (prefix[0...i-1] and suffix[i+1...n-1]) * LCM of (prefix[0...i-1] and suffix[i+1...n-1])
28            long long currentScore = gcd(prefixGcd, suffixGcd[i + 1]) * lcm(prefixLcm, suffixLcm[i + 1]);
29            maxResult = max(maxResult, currentScore);
30          
31            // Update prefix values for next iteration
32            prefixGcd = gcd(prefixGcd, nums[i]);
33            prefixLcm = lcm(prefixLcm, nums[i]);
34        }
35      
36        return maxResult;
37    }
38};
39
1/**
2 * Calculates the maximum score by finding the optimal GCD * LCM value
3 * when potentially removing one element from the array
4 * @param nums - The input array of numbers
5 * @returns The maximum score (GCD * LCM)
6 */
7function maxScore(nums: number[]): number {
8    const n: number = nums.length;
9  
10    // Suffix arrays to store GCD and LCM values from right to left
11    const suffixGcd: number[] = Array(n + 1).fill(0);
12    const suffixLcm: number[] = Array(n + 1).fill(1);
13  
14    // Build suffix GCD and LCM arrays
15    // suffixGcd[i] = GCD of all elements from index i to end
16    // suffixLcm[i] = LCM of all elements from index i to end
17    for (let i: number = n - 1; i >= 0; i--) {
18        suffixGcd[i] = gcd(suffixGcd[i + 1], nums[i]);
19        suffixLcm[i] = lcm(suffixLcm[i + 1], nums[i]);
20    }
21  
22    // Initialize answer with the score using all elements
23    let maxResult: number = suffixGcd[0] * suffixLcm[0];
24  
25    // Prefix values to track GCD and LCM from left
26    let prefixGcd: number = 0;
27    let prefixLcm: number = 1;
28  
29    // Try removing each element and calculate the score
30    for (let i: number = 0; i < n; i++) {
31        // Calculate score when removing element at index i
32        // by combining prefix (before i) and suffix (after i)
33        const currentScore: number = gcd(prefixGcd, suffixGcd[i + 1]) * lcm(prefixLcm, suffixLcm[i + 1]);
34        maxResult = Math.max(maxResult, currentScore);
35      
36        // Update prefix values for next iteration
37        prefixGcd = gcd(prefixGcd, nums[i]);
38        prefixLcm = lcm(prefixLcm, nums[i]);
39    }
40  
41    return maxResult;
42}
43
44/**
45 * Calculates the Greatest Common Divisor (GCD) of two numbers
46 * using Euclidean algorithm
47 * @param a - First number
48 * @param b - Second number
49 * @returns The GCD of a and b
50 */
51function gcd(a: number, b: number): number {
52    return b === 0 ? a : gcd(b, a % b);
53}
54
55/**
56 * Calculates the Least Common Multiple (LCM) of two numbers
57 * using the formula: LCM(a,b) = (a * b) / GCD(a,b)
58 * @param a - First number
59 * @param b - Second number
60 * @returns The LCM of a and b
61 */
62function lcm(a: number, b: number): number {
63    return (a / gcd(a, b)) * b;
64}
65

Time and Space Complexity

Time Complexity: O(n * log(max(nums)))

The algorithm consists of three main parts:

  1. Preprocessing suffix arrays (O(n * log(max(nums)))):

    • The loop runs n times
    • Each iteration computes gcd(suf_gcd[i + 1], nums[i]) and lcm(suf_lcm[i + 1], nums[i])
    • Both GCD and LCM operations have time complexity O(log(max(nums))) using Euclidean algorithm
    • Total: O(n * log(max(nums)))
  2. Main loop to find maximum score (O(n * log(max(nums)))):

    • The loop runs n times
    • Each iteration computes:
      • gcd(pre_gcd, suf_gcd[i + 1]) - O(log(max(nums)))
      • lcm(pre_lcm, suf_lcm[i + 1]) - O(log(max(nums)))
      • gcd(pre_gcd, x) - O(log(max(nums)))
      • lcm(pre_lcm, x) - O(log(max(nums)))
    • Total: O(n * log(max(nums)))

Overall time complexity: O(n * log(max(nums)))

Space Complexity: O(n)

The algorithm uses:

  • suf_gcd array of size n + 1: O(n)
  • suf_lcm array of size n + 1: O(n)
  • pre_gcd and pre_lcm variables: O(1)
  • Other variables (ans, i, x): O(1)

Total space complexity: O(n)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Integer Overflow in LCM Calculation

The most critical pitfall in this solution is potential integer overflow when calculating the LCM. The LCM of two numbers can grow very quickly, and multiplying it with the GCD can easily exceed integer limits.

Problem Example:

# Consider nums = [1000000, 999999, 999998]
# LCM can become extremely large, causing overflow
# Even with Python's arbitrary precision, the multiplication a * b in lcm() 
# could be problematic in other languages

Solution:

  • In Python, this is less of an issue due to arbitrary precision integers, but be mindful of performance
  • For other languages, consider using long long or BigInteger types
  • Add overflow checking if needed:
def lcm(a: int, b: int) -> int:
    if a == 0 or b == 0:
        return 0
    g = gcd(a, b)
    # Check for potential overflow before multiplication
    result = (a // g) * b  # Divide first to reduce overflow risk
    return result

2. Incorrect Identity Values for GCD

Using the wrong identity value for GCD operations can lead to incorrect results.

Problem:

# Wrong: Using 1 as identity for GCD
prefix_gcd = 1  # This would make gcd(1, x) = 1 for any x

# Correct: Using 0 as identity for GCD
prefix_gcd = 0  # This makes gcd(0, x) = x

Solution: Always use 0 as the identity for GCD operations, as gcd(0, x) = x for any positive integer x.

3. Edge Case: Single Element Array

When the array has only one element, removing it would leave an empty array, which should return 0.

Problem:

# If nums = [5], removing the only element should give score = 0
# But suffix_gcd[1] and suffix_lcm[1] might not handle this correctly

Solution: The current implementation handles this correctly by initializing:

  • suffix_gcd[n] = 0 (GCD of empty set)
  • suffix_lcm[n] = 1 (LCM of empty set)
  • When both are combined: gcd(0, 0) * lcm(1, 1) = 0 * 1 = 0

4. LCM of Zero Values

If the array contains zeros, special handling is needed.

Problem:

# If nums contains 0, lcm(0, x) should be 0
# But the formula a * b // gcd(a, b) could cause division issues

Solution: The current implementation correctly handles this with the guard clause:

if a == 0 or b == 0:
    return 0

5. Not Considering the "No Removal" Case

A common mistake is forgetting to include the original array's score (without removing any element).

Problem:

# Wrong: Only considering cases where we remove an element
max_score = 0
for i in range(n):
    # Only calculating scores with removal
    ...

# Correct: Initialize with the full array's score
max_score = suffix_gcd[0] * suffix_lcm[0]

Solution: Always initialize the answer with the factor score of the complete array before checking removal scenarios.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using input 56?

1KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13    def dfs(path, res):
14        if len(path) == len(digits):
15            res.append(''.join(path))
16            return
17
18        next_number = digits[len(path)]
19        for letter in KEYBOARD[next_number]:
20            path.append(letter)
21            dfs(path, res)
22            path.pop()
23
24    res = []
25    dfs([], res)
26    return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2    '2', "abc".toCharArray(),
3    '3', "def".toCharArray(),
4    '4', "ghi".toCharArray(),
5    '5', "jkl".toCharArray(),
6    '6', "mno".toCharArray(),
7    '7', "pqrs".toCharArray(),
8    '8', "tuv".toCharArray(),
9    '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13    List<String> res = new ArrayList<>();
14    dfs(new StringBuilder(), res, digits.toCharArray());
15    return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19    if (path.length() == digits.length) {
20        res.add(path.toString());
21        return;
22    }
23    char next_digit = digits[path.length()];
24    for (char letter : KEYBOARD.get(next_digit)) {
25        path.append(letter);
26        dfs(path, res, digits);
27        path.deleteCharAt(path.length() - 1);
28    }
29}
30
1const KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13    let res = [];
14    dfs(digits, [], res);
15    return res;
16}
17
18function dfs(digits, path, res) {
19    if (path.length === digits.length) {
20        res.push(path.join(''));
21        return;
22    }
23    let next_number = digits.charAt(path.length);
24    for (let letter of KEYBOARD[next_number]) {
25        path.push(letter);
26        dfs(digits, path, res);
27        path.pop();
28    }
29}
30

Recommended Readings

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

Load More