1039. Minimum Score Triangulation of Polygon
Problem Description
You are given a convex n-sided polygon where each vertex has an integer value. The vertices are represented by an integer array values
where values[i]
is the value of the i-th vertex in clockwise order.
Your task is to triangulate this polygon. Triangulation means dividing the polygon into triangles such that:
- Each triangle's vertices must be vertices from the original polygon
- Only triangles can be formed (no other shapes allowed)
- The triangulation will create exactly
n - 2
triangles
For each triangle formed during triangulation, its weight is calculated as the product of the values at its three vertices. The total score of a triangulation is the sum of all triangle weights.
You need to find the minimum possible score among all valid triangulations of the polygon.
For example, if you have a quadrilateral with vertices having values [1, 2, 3, 4]
, you can triangulate it in different ways:
- One way might create triangles with vertices
(0, 1, 2)
and(0, 2, 3)
, giving a score of1*2*3 + 1*3*4 = 6 + 12 = 18
- Another way might create triangles with vertices
(0, 1, 3)
and(1, 2, 3)
, giving a score of1*2*4 + 2*3*4 = 8 + 24 = 32
The goal is to find the triangulation that minimizes this total score.
Intuition
When we look at triangulating a polygon, we need to think about how to break down this complex problem into smaller, manageable pieces.
Consider any edge of the polygon, say from vertex i
to vertex j
. When we triangulate the polygon, this edge will eventually be part of some triangle. The key insight is that we can choose any vertex k
between i
and j
to form this triangle. Once we pick vertex k
, we create a triangle with vertices i
, k
, and j
.
Now here's the crucial observation: after forming this triangle, we're left with two smaller polygons:
- One polygon from vertex
i
to vertexk
- Another polygon from vertex
k
to vertexj
Each of these smaller polygons needs to be triangulated independently, and we want the minimum score for each. This naturally suggests a recursive approach where the original problem can be solved by combining solutions to smaller subproblems.
The recursive relationship becomes clear: for any polygon from vertex i
to j
, we try all possible vertices k
between them to form a triangle. The score for choosing k
would be:
- The score of triangulating the left polygon (
i
tok
) - Plus the score of triangulating the right polygon (
k
toj
) - Plus the weight of the current triangle formed by
i
,k
, andj
, which isvalues[i] * values[k] * values[j]
We want to minimize this total score across all possible choices of k
.
The base case is when we have just two vertices (i + 1 = j
), which forms an edge but not a triangle, so the score is 0
.
This recursive structure with overlapping subproblems (the same polygon segments get computed multiple times) makes it perfect for dynamic programming with memoization. We can cache the results of dfs(i, j)
to avoid redundant calculations.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution implements a top-down dynamic programming approach using memoization. We define a recursive function dfs(i, j)
that computes the minimum score for triangulating the polygon from vertex i
to vertex j
.
Implementation Details:
-
Function Definition:
dfs(i, j)
represents the minimum triangulation score for the polygon segment from vertexi
to vertexj
. -
Base Case: When
i + 1 == j
, we have only two consecutive vertices forming an edge. Since we need at least 3 vertices to form a triangle, this returns0
. -
Recursive Case: For vertices
i
toj
wherej - i > 1
:- We iterate through all possible middle vertices
k
wherei < k < j
- For each
k
, we calculate the total score as:dfs(i, k)
: Minimum score for triangulating the left sub-polygondfs(k, j)
: Minimum score for triangulating the right sub-polygonvalues[i] * values[k] * values[j]
: Weight of the current triangle formed by verticesi
,k
, andj
- We take the minimum across all possible values of
k
- We iterate through all possible middle vertices
-
Memoization: The
@cache
decorator automatically memoizes the function results, preventing redundant calculations for the same(i, j)
pairs. -
Final Answer: We call
dfs(0, len(values) - 1)
to get the minimum score for triangulating the entire polygon.
Time Complexity: O(n³)
where n
is the number of vertices. For each of the O(n²)
possible (i, j)
pairs, we iterate through up to n
values of k
.
Space Complexity: O(n²)
for the memoization cache storing results for all possible (i, j)
pairs.
The algorithm elegantly handles the optimal substructure property of the problem - the optimal triangulation of a polygon can be constructed from optimal triangulations of its sub-polygons, making dynamic programming the perfect approach.
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 a small example with a pentagon having values [1, 3, 1, 4, 1]
.
We want to find dfs(0, 4)
- the minimum triangulation score for the entire polygon from vertex 0 to vertex 4.
Step 1: Initial Call - dfs(0, 4)
We need to try all possible middle vertices k where 0 < k < 4. So k can be 1, 2, or 3.
- k = 1: Score = dfs(0, 1) + dfs(1, 4) + (1 × 3 × 1) = 0 + dfs(1, 4) + 3
- k = 2: Score = dfs(0, 2) + dfs(2, 4) + (1 × 1 × 1) = dfs(0, 2) + dfs(2, 4) + 1
- k = 3: Score = dfs(0, 3) + dfs(3, 4) + (1 × 4 × 1) = dfs(0, 3) + 0 + 4
Step 2: Compute Required Subproblems
For k = 1, we need dfs(1, 4):
- Try middle vertices 2 and 3
- k = 2: dfs(1, 2) + dfs(2, 4) + (3 × 1 × 1) = 0 + dfs(2, 4) + 3
- k = 3: dfs(1, 3) + dfs(3, 4) + (3 × 4 × 1) = dfs(1, 3) + 0 + 12
For dfs(2, 4), we try k = 3:
- Score = dfs(2, 3) + dfs(3, 4) + (1 × 4 × 1) = 0 + 0 + 4 = 4
For dfs(1, 3), we try k = 2:
- Score = dfs(1, 2) + dfs(2, 3) + (3 × 1 × 4) = 0 + 0 + 12 = 12
So dfs(1, 4) = min(0 + 4 + 3, 12 + 0 + 12) = min(7, 24) = 7
For k = 2 in original call, we need dfs(0, 2):
- Try k = 1: dfs(0, 1) + dfs(1, 2) + (1 × 3 × 1) = 0 + 0 + 3 = 3
For k = 3 in original call, we need dfs(0, 3):
- Try k = 1: dfs(0, 1) + dfs(1, 3) + (1 × 3 × 4) = 0 + 12 + 12 = 24
- Try k = 2: dfs(0, 2) + dfs(2, 3) + (1 × 1 × 4) = 3 + 0 + 4 = 7
- So dfs(0, 3) = min(24, 7) = 7
Step 3: Calculate Final Answer
Now we can complete dfs(0, 4):
- k = 1: 0 + 7 + 3 = 10
- k = 2: 3 + 4 + 1 = 8
- k = 3: 7 + 0 + 4 = 11
Therefore, dfs(0, 4) = min(10, 8, 11) = 8
The optimal triangulation creates triangles:
- (0, 1, 2) with weight 1 × 3 × 1 = 3
- (2, 3, 4) with weight 1 × 4 × 1 = 4
- (0, 2, 4) with weight 1 × 1 × 1 = 1
Total score = 3 + 4 + 1 = 8
Solution Implementation
1class Solution:
2 def minScoreTriangulation(self, values: List[int]) -> int:
3 """
4 Find the minimum score triangulation of a polygon.
5
6 The polygon is represented as a list of vertex values.
7 The score of a triangle is the product of its three vertex values.
8
9 Args:
10 values: List of integers representing vertex values of the polygon
11
12 Returns:
13 The minimum score achievable by triangulating the polygon
14 """
15 from functools import cache
16
17 @cache
18 def dfs(left: int, right: int) -> int:
19 """
20 Calculate minimum triangulation score for polygon segment.
21
22 Uses dynamic programming to find the minimum score for triangulating
23 the polygon segment from vertex 'left' to vertex 'right'.
24
25 Args:
26 left: Starting vertex index of the polygon segment
27 right: Ending vertex index of the polygon segment
28
29 Returns:
30 Minimum triangulation score for the segment [left, right]
31 """
32 # Base case: adjacent vertices form an edge, not a triangle
33 if left + 1 == right:
34 return 0
35
36 # Try all possible vertices k to form a triangle with vertices left and right
37 # The triangle (left, k, right) divides the polygon into:
38 # 1. Sub-polygon from left to k
39 # 2. The triangle itself with score: values[left] * values[k] * values[right]
40 # 3. Sub-polygon from k to right
41 min_score = float('inf')
42 for k in range(left + 1, right):
43 current_score = (dfs(left, k) +
44 dfs(k, right) +
45 values[left] * values[k] * values[right])
46 min_score = min(min_score, current_score)
47
48 return min_score
49
50 # Start with the entire polygon from vertex 0 to vertex n-1
51 return dfs(0, len(values) - 1)
52
1class Solution {
2 // Number of vertices in the polygon
3 private int n;
4 // Array storing the values of each vertex
5 private int[] values;
6 // Memoization table for dynamic programming
7 // memo[i][j] stores the minimum score for triangulating polygon from vertex i to j
8 private Integer[][] memo;
9
10 /**
11 * Finds the minimum score of triangulation for a convex polygon.
12 * The score of a triangle is the product of its three vertices' values.
13 *
14 * @param values Array of vertex values representing the polygon
15 * @return Minimum total score of triangulation
16 */
17 public int minScoreTriangulation(int[] values) {
18 this.n = values.length;
19 this.values = values;
20 this.memo = new Integer[n][n];
21
22 // Start DFS from the entire polygon (vertex 0 to vertex n-1)
23 return dfs(0, n - 1);
24 }
25
26 /**
27 * Recursively calculates the minimum triangulation score for a sub-polygon.
28 * Uses memoization to avoid redundant calculations.
29 *
30 * @param i Starting vertex of the sub-polygon
31 * @param j Ending vertex of the sub-polygon
32 * @return Minimum score to triangulate the sub-polygon from vertex i to j
33 */
34 private int dfs(int i, int j) {
35 // Base case: adjacent vertices form an edge, not a polygon
36 // No triangulation needed, score is 0
37 if (i + 1 == j) {
38 return 0;
39 }
40
41 // Check if result is already computed (memoization)
42 if (memo[i][j] != null) {
43 return memo[i][j];
44 }
45
46 // Initialize minimum score to a large value
47 int minScore = 1 << 30; // Equivalent to 2^30
48
49 // Try all possible vertices k to form a triangle with vertices i and j
50 // Triangle formed: (i, k, j)
51 for (int k = i + 1; k < j; k++) {
52 // Calculate score:
53 // - Score of left sub-polygon (i to k)
54 // - Score of right sub-polygon (k to j)
55 // - Score of current triangle (i, k, j)
56 int currentScore = dfs(i, k) + dfs(k, j) + values[i] * values[k] * values[j];
57 minScore = Math.min(minScore, currentScore);
58 }
59
60 // Store and return the minimum score for this sub-polygon
61 memo[i][j] = minScore;
62 return minScore;
63 }
64}
65
1class Solution {
2public:
3 int minScoreTriangulation(vector<int>& values) {
4 int n = values.size();
5
6 // dp[i][j] stores the minimum score for triangulating the polygon
7 // formed by vertices from index i to j
8 int dp[n][n];
9 memset(dp, 0, sizeof(dp));
10
11 // Recursive function with memoization to find minimum triangulation score
12 // Parameters: i - starting vertex index, j - ending vertex index
13 // Returns: minimum score for triangulating polygon from vertex i to j
14 function<int(int, int)> calculateMinScore = [&](int i, int j) -> int {
15 // Base case: if only two vertices, no triangle can be formed
16 if (i + 1 == j) {
17 return 0;
18 }
19
20 // Return memoized result if already calculated
21 if (dp[i][j] != 0) {
22 return dp[i][j];
23 }
24
25 // Initialize result with a large value
26 int minScore = INT_MAX;
27
28 // Try all possible vertices k between i and j to form a triangle (i, k, j)
29 // The polygon is then divided into three parts:
30 // 1. Polygon from i to k
31 // 2. Triangle (i, k, j)
32 // 3. Polygon from k to j
33 for (int k = i + 1; k < j; ++k) {
34 int currentScore = calculateMinScore(i, k) +
35 calculateMinScore(k, j) +
36 values[i] * values[k] * values[j];
37 minScore = min(minScore, currentScore);
38 }
39
40 // Store and return the minimum score
41 dp[i][j] = minScore;
42 return minScore;
43 };
44
45 // Calculate minimum score for the entire polygon (from vertex 0 to n-1)
46 return calculateMinScore(0, n - 1);
47 }
48};
49
1/**
2 * Finds the minimum score triangulation of a polygon represented by vertex values.
3 * The score of a triangle formed by vertices i, j, k is values[i] * values[j] * values[k].
4 *
5 * @param values - Array of vertex values representing the polygon
6 * @returns The minimum score of triangulation
7 */
8function minScoreTriangulation(values: number[]): number {
9 const polygonSize: number = values.length;
10
11 // Memoization table to store results of subproblems
12 // memo[i][j] represents the minimum score to triangulate vertices from i to j
13 const memo: number[][] = Array.from(
14 { length: polygonSize },
15 () => Array.from({ length: polygonSize }, () => 0)
16 );
17
18 /**
19 * Recursively calculates the minimum triangulation score for vertices from startIndex to endIndex.
20 * Uses dynamic programming with memoization to avoid redundant calculations.
21 *
22 * @param startIndex - Starting vertex index of the polygon segment
23 * @param endIndex - Ending vertex index of the polygon segment
24 * @returns Minimum triangulation score for the given segment
25 */
26 const calculateMinScore = (startIndex: number, endIndex: number): number => {
27 // Base case: adjacent vertices form an edge, not a triangle
28 if (startIndex + 1 === endIndex) {
29 return 0;
30 }
31
32 // Return memoized result if already calculated
33 if (memo[startIndex][endIndex] > 0) {
34 return memo[startIndex][endIndex];
35 }
36
37 // Initialize minimum score with a large value
38 let minScore: number = 1 << 30;
39
40 // Try all possible vertices k to form a triangle with startIndex and endIndex
41 // Triangle vertices: (startIndex, k, endIndex)
42 for (let k: number = startIndex + 1; k < endIndex; ++k) {
43 // Calculate score: left subproblem + right subproblem + current triangle score
44 const currentScore: number = calculateMinScore(startIndex, k) +
45 calculateMinScore(k, endIndex) +
46 values[startIndex] * values[k] * values[endIndex];
47
48 minScore = Math.min(minScore, currentScore);
49 }
50
51 // Store the result in memoization table
52 memo[startIndex][endIndex] = minScore;
53 return minScore;
54 };
55
56 // Calculate minimum score for the entire polygon (from vertex 0 to n-1)
57 return calculateMinScore(0, polygonSize - 1);
58}
59
Time and Space Complexity
The time complexity is O(n^3)
, where n
is the number of vertices in the polygon.
This is because:
- The memoized recursive function
dfs(i, j)
hasO(n^2)
possible states, asi
andj
can each range from0
ton-1
- For each state
(i, j)
, we iterate through all possible values ofk
wherei + 1 ≤ k < j
, which takesO(j - i - 1) = O(n)
time in the worst case - Each state is computed only once due to the
@cache
decorator - Therefore, the total time complexity is
O(n^2) × O(n) = O(n^3)
The space complexity is O(n^2)
, where n
is the number of vertices in the polygon.
This is because:
- The
@cache
decorator stores the results of all computed states, which requiresO(n^2)
space for all possible(i, j)
pairs - The recursion depth can be at most
O(n)
, contributingO(n)
to the call stack space - The dominant factor is the cache storage, giving us
O(n^2)
space complexity
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Base Case Handling
A frequent mistake is misunderstanding when the base case should return 0. Developers often confuse the condition and might write:
Incorrect Implementation:
if right - left <= 1: # Wrong condition! return 0
This is problematic because when right - left == 1
, we have exactly two vertices (e.g., vertices at indices 0 and 1), which should return 0. But when right - left == 2
, we have three vertices forming a triangle, which should NOT return 0.
Correct Implementation:
if left + 1 == right: # or equivalently: right - left == 1 return 0
2. Off-by-One Errors in Loop Boundaries
Another common mistake is using incorrect loop boundaries when iterating through the middle vertex k
:
Incorrect Implementation:
for k in range(left, right + 1): # Wrong! Includes left and right
# This would try to form degenerate triangles
or
for k in range(left + 1, right + 1): # Wrong! Includes right
# This would create invalid sub-problems
Correct Implementation:
for k in range(left + 1, right): # k must be strictly between left and right
# Ensures valid triangles and sub-polygons
3. Forgetting to Import or Use Memoization
Without memoization, the solution becomes exponentially slow due to repeated calculations:
Incorrect Implementation:
def dfs(left, right): # No memoization!
if left + 1 == right:
return 0
# ... rest of the code
This would result in Time Limit Exceeded for larger inputs as the same sub-problems are computed multiple times.
Correct Implementation:
from functools import cache
@cache # Essential for performance
def dfs(left, right):
# ... implementation
4. Confusion About Polygon Representation
Some developers mistakenly think they need to handle the circular nature of the polygon explicitly:
Incorrect Implementation:
def dfs(i, j):
# Wrong! Trying to handle wraparound
if j < i:
j += len(values)
# ... complicated logic to handle circular indices
The correct approach treats the polygon as a linear array from index 0 to n-1. The polygon's circular nature is already encoded in the problem structure - we're finding triangulations that connect vertex 0 to vertex n-1.
5. Integer Overflow in Other Languages
While Python handles large integers automatically, if implementing in languages like Java or C++, the product values[left] * values[k] * values[right]
might overflow:
Solution for Other Languages:
// In C++, use long long long long score = (long long)values[left] * values[k] * values[right];
Complete Corrected Solution Template:
class Solution:
def minScoreTriangulation(self, values: List[int]) -> int:
from functools import cache
@cache
def dfs(left: int, right: int) -> int:
# Correct base case: adjacent vertices
if left + 1 == right:
return 0
min_score = float('inf')
# Correct loop boundaries: k strictly between left and right
for k in range(left + 1, right):
current_score = (dfs(left, k) +
dfs(k, right) +
values[left] * values[k] * values[right])
min_score = min(min_score, current_score)
return min_score
# Process entire polygon
return dfs(0, len(values) - 1)
Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
Recommended Readings
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
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!