LeetCode 2971 - Find Polygon With the Largest Perimeter
The problem gives us an array nums containing positive integers. Each integer represents a potential side length that we may use when constructing a polygon. A polygon must have at least three sides.
Difficulty: 🟡 Medium
Topics: Array, Greedy, Sorting, Prefix Sum
Solution
LeetCode 2971 - Find Polygon With the Largest Perimeter
Problem Understanding
The problem gives us an array nums containing positive integers. Each integer represents a potential side length that we may use when constructing a polygon.
A polygon must have at least three sides. The key geometric property we are given is that if the side lengths are sorted and the largest side is smaller than the sum of all other sides, then a valid polygon can always be formed. Therefore, for any selected set of sides:
sum(all sides except largest) > largest
must hold.
Our goal is to select some subset of the given numbers, containing at least three elements, such that:
- The selected sides can form a valid polygon.
- The perimeter, which is the sum of all selected sides, is as large as possible.
If no valid polygon can be formed, we return -1.
The constraints are important:
3 <= n <= 1000001 <= nums[i] <= 1000000000
Since n can be as large as 100,000, any exponential or quadratic solution is far too slow. We need something close to O(n log n).
Several edge cases are worth noticing immediately:
- The array may contain exactly three numbers.
- The largest value may be much larger than all others combined.
- A valid polygon might require discarding some large numbers.
- The answer can exceed 32-bit integer range because there may be up to
100,000numbers each as large as10^9, so the perimeter may reach10^14. We must use 64-bit integers. - Since all values are positive, adding more sides generally increases perimeter, provided the polygon condition remains valid.
Approaches
Brute Force
A brute-force solution would examine every possible subset of the array that contains at least three elements.
For each subset, we would:
- Sort the chosen side lengths.
- Check whether the largest side is smaller than the sum of the remaining sides.
- If valid, compute the perimeter.
- Track the maximum perimeter found.
This approach is correct because it explicitly checks every possible polygon that can be formed.
However, there are 2^n possible subsets. Even for n = 50, this becomes completely infeasible. With n = 100000, such an approach is impossible.
Key Insight
First sort the array in ascending order.
Suppose the sorted array is:
a0 <= a1 <= a2 <= ... <= an-1
Let the total sum of the first i + 1 elements be a prefix sum.
Consider using all elements from index 0 through i as the polygon sides. Since the array is sorted, nums[i] is the largest side.
The polygon condition becomes:
prefixSum(i) - nums[i] > nums[i]
or equivalently:
prefixSum(i) > 2 * nums[i]
Now comes the crucial observation.
Because all numbers are positive, if a prefix ending at index i forms a valid polygon, then its perimeter is exactly prefixSum(i).
To maximize perimeter, we should use as many sides as possible. After sorting, we can process prefixes from left to right and record every valid prefix. The largest valid prefix sum encountered will be the answer.
Why does checking prefixes suffice?
Suppose a valid solution uses some subset ending with largest side nums[i]. Adding additional smaller positive sides only increases the left side of the inequality and cannot make the polygon invalid. Therefore, for any largest side nums[i], the maximum perimeter using that largest side is obtained by including all smaller sides. That means every optimal solution corresponds to some sorted prefix.
This reduces the problem to checking each prefix once.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^n · n log n) | O(n) | Enumerates every subset |
| Optimal | O(n log n) | O(1) extra, excluding sort | Sort once and check each prefix |
Algorithm Walkthrough
- Sort
numsin non-decreasing order.
Sorting allows us to easily identify the largest side in any candidate polygon and efficiently evaluate the polygon condition. 2. Initialize a running prefix sum.
As we iterate through the sorted array, we maintain the sum of all elements seen so far. 3. Start scanning the sorted array from left to right.
For each index i, add nums[i] to the running sum.
4. Ignore prefixes with fewer than three elements.
A polygon must contain at least three sides, so we only start checking when i >= 2.
5. Check the polygon condition.
Let the current prefix sum be currentSum.
The largest side is nums[i].
The polygon condition is:
currentSum - nums[i] > nums[i]
If this holds, then all sides from index 0 through i form a valid polygon.
6. Update the answer.
When a valid polygon is found, its perimeter is exactly currentSum.
Record the maximum perimeter seen so far. 7. Return the answer.
If no valid prefix was ever found, return -1.
Why it works
After sorting, any polygon whose largest side is nums[i] achieves its maximum possible perimeter by including every smaller side. Since all side lengths are positive, adding extra smaller sides increases the perimeter and makes the inequality easier to satisfy. Therefore every optimal solution corresponds to a prefix of the sorted array. Checking each prefix guarantees that we evaluate every possible optimal candidate, and selecting the largest valid prefix sum yields the maximum perimeter.
Python Solution
from typing import List
class Solution:
def largestPerimeter(self, nums: List[int]) -> int:
nums.sort()
prefix_sum = 0
answer = -1
for i, value in enumerate(nums):
prefix_sum += value
if i >= 2 and prefix_sum - value > value:
answer = prefix_sum
return answer
The implementation begins by sorting the array. This guarantees that the current element is always the largest side in the current prefix.
The variable prefix_sum stores the sum of all elements processed so far. As we iterate through the sorted array, we continuously update this value.
Once at least three elements have been included, we test the polygon condition:
prefix_sum - value > value
where value is the largest side of the current prefix.
Whenever the condition is satisfied, the entire prefix forms a valid polygon. Since prefix_sum is the perimeter of that polygon, we store it as the current answer.
Because we process prefixes in increasing order of size and all values are positive, later valid prefixes have larger perimeters. Storing the latest valid perimeter naturally gives the maximum result.
Go Solution
import "sort"
func largestPerimeter(nums []int) int64 {
sort.Ints(nums)
var prefixSum int64
var answer int64 = -1
for i, value := range nums {
prefixSum += int64(value)
if i >= 2 && prefixSum-int64(value) > int64(value) {
answer = prefixSum
}
}
return answer
}
The Go solution follows exactly the same logic as the Python version.
The main implementation difference is the use of int64 for both prefixSum and answer. The perimeter can be as large as:
100000 × 1000000000 = 100000000000000
which exceeds 32-bit integer limits. Using int64 prevents overflow.
Worked Examples
Example 1
Input:
[5,5,5]
Sorted array:
[5,5,5]
| i | value | prefixSum | Check | Valid? | answer |
|---|---|---|---|---|---|
| 0 | 5 | 5 | fewer than 3 sides | No | -1 |
| 1 | 5 | 10 | fewer than 3 sides | No | -1 |
| 2 | 5 | 15 | 15 - 5 > 5 | Yes | 15 |
Final answer:
15
Example 2
Input:
[1,12,1,2,5,50,3]
Sorted array:
[1,1,2,3,5,12,50]
| i | value | prefixSum | Condition | Valid? | answer |
|---|---|---|---|---|---|
| 0 | 1 | 1 | N/A | No | -1 |
| 1 | 1 | 2 | N/A | No | -1 |
| 2 | 2 | 4 | 2 > 2 | No | -1 |
| 3 | 3 | 7 | 4 > 3 | Yes | 7 |
| 4 | 5 | 12 | 7 > 5 | Yes | 12 |
| 5 | 12 | 24 | 12 > 12 | No | 12 |
| 6 | 50 | 74 | 24 > 50 | No | 12 |
Final answer:
12
Example 3
Input:
[5,5,50]
Sorted array:
[5,5,50]
| i | value | prefixSum | Condition | Valid? |
|---|---|---|---|---|
| 0 | 5 | 5 | N/A | No |
| 1 | 5 | 10 | N/A | No |
| 2 | 50 | 60 | 10 > 50 | No |
No valid polygon exists.
Final answer:
-1
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n log n) | Sorting dominates the runtime |
| Space | O(1) extra, excluding sort | Only a few variables are maintained |
The scan after sorting is linear, requiring only one pass through the array. The sorting step costs O(n log n), which dominates the overall complexity. Aside from the sort implementation itself, the algorithm uses only constant additional memory.
Test Cases
sol = Solution()
assert sol.largestPerimeter([5, 5, 5]) == 15 # example 1
assert sol.largestPerimeter([1, 12, 1, 2, 5, 50, 3]) == 12 # example 2
assert sol.largestPerimeter([5, 5, 50]) == -1 # example 3
assert sol.largestPerimeter([2, 3, 4]) == 9 # simple triangle
assert sol.largestPerimeter([1, 1, 2]) == -1 # degenerate triangle
assert sol.largestPerimeter([1, 1, 2, 3]) == -1 # no valid polygon
assert sol.largestPerimeter([1, 2, 2, 3]) == 8 # valid quadrilateral
assert sol.largestPerimeter([1, 1, 1, 1]) == 4 # all equal sides
assert sol.largestPerimeter([10, 11, 12, 13]) == 46 # entire array valid
assert sol.largestPerimeter([1, 1, 1, 100]) == 3 # discard large outlier
assert sol.largestPerimeter([1000000000, 1000000000, 1000000000]) == 3000000000 # large values
assert sol.largestPerimeter([3, 4, 5, 100]) == 12 # best polygon excludes largest
assert sol.largestPerimeter([2, 2, 3, 4, 10]) == 11 # largest element unusable
assert sol.largestPerimeter([1, 2, 3, 4, 5, 6]) == 21 # large valid prefix
assert sol.largestPerimeter([1, 1, 1]) == 3 # minimum size valid case
Test Summary
| Test | Why |
|---|---|
[5,5,5] |
Provided example, valid polygon |
[1,12,1,2,5,50,3] |
Provided example, requires excluding large values |
[5,5,50] |
Provided example, impossible case |
[2,3,4] |
Basic valid triangle |
[1,1,2] |
Boundary where equality fails |
[1,1,2,3] |
No valid polygon despite four numbers |
[1,2,2,3] |
Valid quadrilateral |
[1,1,1,1] |
Uniform side lengths |
[10,11,12,13] |
Entire array forms polygon |
[1,1,1,100] |
Large outlier must be ignored |
Large 10^9 values |
Verifies 64-bit perimeter handling |
[3,4,5,100] |
Optimal answer excludes largest element |
[2,2,3,4,10] |
Later element breaks validity |
[1,2,3,4,5,6] |
Large valid prefix |
[1,1,1] |
Smallest valid input size |
Edge Cases
Equality Does Not Count
A common mistake is using >= instead of >.
Consider:
[1,1,2]
The condition becomes:
1 + 1 = 2
A valid polygon requires the sum of the smaller sides to be strictly greater than the largest side. Equality is not sufficient. The implementation correctly uses:
prefix_sum - value > value
which rejects this case.
Very Large Side Lengths
The side lengths may be as large as 10^9, and there may be up to 100000 of them.
The perimeter can therefore reach:
100000 × 10^9 = 10^14
This exceeds the range of a 32-bit integer. The Go solution explicitly uses int64, and Python automatically supports arbitrarily large integers.
The Largest Numbers May Need to Be Excluded
Consider:
[3,4,5,100]
Using all four sides fails because:
3 + 4 + 5 = 12 < 100
However, the first three sides form a valid triangle with perimeter 12.
A naive strategy that only checks the entire array would incorrectly return -1. The prefix-based approach evaluates every possible largest side and therefore correctly finds the best valid perimeter.
No Valid Polygon Exists
Consider:
[1,1,100]
The largest side is much larger than the sum of the others.
Every possible polygon candidate fails the polygon inequality. The answer variable remains -1, and the implementation correctly returns -1 when no valid prefix is found.