Introduction
I recently solved a classic circular DP problem. It was quite deceptive: I designed a seemingly correct linear DP model that actually ignored the problem’s central circular constraint. This article retraces the entire process, from a 70-point linear solution, through a 90-point incorrect patch, to the final 100-point accepted solution that cuts the circle into a line. Along the way, I will examine the mistakes in my reasoning and the key to modeling the problem correctly.
Problem Analysis
The essential parts of the problem are:
- Layout: A circular arrangement.
- Constraint: Every tree must be taller than both neighbors or shorter than both neighbors, forming a peak or a valley.
- Objective: Maximize the total ornamental value.
This is a dynamic programming problem, and state design is the key. To store the input ornamental values, I used a Plant_Position structure, where pos[i].ten, pos[i].twenty, and pos[i].thirty hold the corresponding values at position i.
To determine whether a planting at position i is valid, we need both the height at i-1 and the direction of the change from i-2 to i-1. Only then can we determine which direction is allowed at i. This calls for a three-dimensional state: dp[i][j][k].
i: We are considering theith tree.j: The height of theith tree. I use1, 2, 3to represent heights10, 20, 30.k: The shape of the sequence at theith tree.k=0: Theith tree is a valley:H[i-1] > H[i], so the change fromi-1toiis downward.k=1: Theith tree is a peak:H[i-1] < H[i], so the change fromi-1toiis upward.
The value of dp[i][j][k] is the maximum ornamental value under these definitions.
Algorithm Design and Implementation
V1.0: A First Attempt with Linear DP (70 Points)
Design
My initial approach completely ignored the circular constraint and treated the garden as a linear sequence from 1 to n. The state dp[i][j][k] above gives us all the valid transition equations.
State transitions in detail:
Target state:
dp[i][1][0](height10, valley/downward)- Reasoning: To descend to height
10, the previous positioni-1must have height20or30. To form a valley,i-1must also be a peak—an upward state withk=1. - Equation:
dp[i][1][0] = max(dp[i-1][2][1], dp[i-1][3][1]) + pos[i].ten
- Reasoning: To descend to height
Target state:
dp[i][2][0](height20, valley/downward)- Reasoning: To descend to height
20, the previous positioni-1must have height30. It must be a peak—an upward state withk=1. - Equation:
dp[i][2][0] = dp[i-1][3][1] + pos[i].twenty
- Reasoning: To descend to height
Target state:
dp[i][2][1](height20, peak/upward)- Reasoning: To ascend to height
20, the previous positioni-1must have height10. It must be a valley—a downward state withk=0. - Equation:
dp[i][2][1] = dp[i-1][1][0] + pos[i].twenty
- Reasoning: To ascend to height
Target state:
dp[i][3][1](height30, peak/upward)- Reasoning: To ascend to height
30, the previous positioni-1may have height10or20. It must be a valley—a downward state withk=0. - Equation:
dp[i][3][1] = max(dp[i-1][1][0], dp[i-1][2][0]) + pos[i].thirty
- Reasoning: To ascend to height
Unreachable states:
dp[i][1][1](ascending to the lowest height) anddp[i][3][0](descending to the highest height) are impossible. Their values should remain at a very small sentinel value and stay out of the calculation.
Figure legend: The state definition labels i as the position, j as the height, and k as the height relationship to the previous tree. k=0 is downward (H[i-1] > H[i]); k=1 is upward (H[i-1] < H[i]). The red and green calculations work through the four transitions on the sample.
Implementation
This implementation simply loops from i=1 to n, then takes the maximum over all valid states in dp[n] as the result.
Expand/collapse the V1.0 70-point code
1 |
|
What Went Wrong
- Root cause:
This computes the optimum for a linear arrangement, but the problem requires a circular arrangement. The states at positionsnand1in the linear optimum may fail the alternating-height constraint, making the solution invalid for the circular problem.
V2.0: An Incorrect “Patch” (90 Points)
Design
After submitting V1.0, I realized that I had missed the circular constraint. I then came up with a flawed correction: compute the linear optimum first, then check whether it satisfies the circular constraint.
My first implementation of this idea was a complicated, cumbersome while loop:
Expand/collapse the while-loop “patch”
1 | ans.push_back(dp[n][1][0]); |
I eventually reduced this complicated while loop to two if statements:
1 | if (pos[1].twenty > pos[1].ten) { |
Although the if version is much shorter, the underlying faulty reasoning is the same. The “patch” tries to take the few optimal candidates produced by linear DP and use a set of checks to infer and verify whether their starting points are valid.
The checks work as follows:
Unrestricted cases: If the linear optimum ends at
dp[n][1][0](H=10, downward) ordp[n][3][1](H=30, upward), accept it directly.- Inference: If
H[n]=10, closing the circle requiresH[1] > 10, soH[1]may be20or30. Because there are two choices, the code assumes that a valid starting point can always be found and no intervention is needed. The same reasoning applies toH[n]=30.
- Inference: If
Restricted case 1: Consider a path ending at
dp[n][2][1](H=20, upward).- Inference: This ending state implies
H[n-1]=10. To close the circle,H[1]must be less thanH[n]=20, soH[1]must be10. - Verification: The code makes a greedy check:
if (pos[1].twenty > pos[1].ten). Its reasoning is: “If planting20at the starting point gives a higher ornamental value than planting10, a genuinely optimal path could not have chosen10at the beginning.” Under this greedy assumption, when the condition holds, the path requiring a starting height of10cannot be optimal, sodp[n][2][1] = 0invalidates it.
- Inference: This ending state implies
Restricted case 2: Consider a path ending at
dp[n][2][0](H=20, downward).- Inference: This ending state implies
H[n-1]=30. To close the circle,H[1]must be greater thanH[n]=20, soH[1]must be30. - Verification: Similarly, the code makes the greedy check
if (pos[1].twenty > pos[1].thirty). If planting20gives a higher value, it assumes that the path requiring a starting height of30cannot be optimal and invalidates it.
- Inference: This ending state implies
Figure legend: The arrows follow the circular order. The circled candidates and the two conditions at the upper right illustrate the attempted endpoint checks: discard dp[n][2][1] or dp[n][2][0] based on the ornamental values at the first position.
Implementation
Expand/collapse the V2.0 code (90 points)
1 |
|
What Went Wrong
Fundamental flaw:
This approach does not fail because theif-statement patch is insufficiently clever. Quite the opposite: it neatly condenses the intention of the earlierwhileloop and is a very good patch in terms of expressing that logic.The real error lies in the fundamental algorithm design. This kind of after-the-fact repair is like beautifully paving a road that goes in the wrong direction: however good the road is, it cannot take you to the right destination.
The central fallacy is the assumption that the circular optimum must be among the linear optima. In reality, the linear portion of the true circular optimum may not be optimal at all. The code checks only a handful of linear-optimal candidates. The actual answer may already have been discarded during the first DP because its score was not high enough on the line, leaving it no chance of reaching the final
ifchecks. This is a classic local-optimum trap.
V3.0: The Correct Solution—Cut the Circle into a Line (AC)
Design
The correct approach is to abandon after-the-fact repair and use the standard pattern for circular problems: cut the circle into a line.
The central idea is to split a difficult circular problem into a few independent linear problems with explicit starting and ending constraints.
Specifically, an outer loop fixes the height of the first tree (第 1 棵树) in turn to each of the three possibilities: H=10, H=20, and H=30. For each case, run an independent linear DP from 2 to n. At the end, use the fixed starting point to filter the states in dp[n] for validity.
Figure legend: The three diagrams fix the first tree to each possible height. The notes marked “take max” identify the eligible ending states for that starting height; the red formulas are the same linear transitions used in each independent run.
Feasibility Analysis
- Time complexity: Each DP run takes
O(n). There are 3 independent runs, so the overall time complexity remainsO(n). - Space complexity:
O(n). - Conclusion: The approach is efficient and feasible.
Implementation
Expand/collapse the V3.0 accepted code
1 |
|
Implementation details
The essence of this approach is to turn the circular constraint from a difficult final check into case analysis built into the design. Once the starting point is fixed, the final filtering of ending states works as follows:Starting height
H[1]=10(valley):- To close the circle,
H[n]must be a peak withH[n] > H[1]. - Valid ending states:
dp[n][2][1](H=20, upward) anddp[n][3][1](H=30, upward). - Candidate answer:
max(dp[n][2][1], dp[n][3][1])
- To close the circle,
Starting height
H[1]=20(peak or valley):- To close the circle, if
H[n]is a peak, it must satisfyH[n] > H[1]; if it is a valley, it must satisfyH[n] < H[1]. - Valid ending states:
dp[n][3][1](H=30, upward) anddp[n][1][0](H=10, downward). - Candidate answer:
max(dp[n][3][1], dp[n][1][0])
- To close the circle, if
Starting height
H[1]=30(peak):- To close the circle,
H[n]must be a valley withH[n] < H[1]. - Valid ending states:
dp[n][1][0](H=10, downward) anddp[n][2][0](H=20, downward). - Candidate answer:
max(dp[n][1][0], dp[n][2][0])
- To close the circle,
The final answer is the largest of these three candidates. This precise filtering ensures that the final solution is valid.
Takeaways
- Modeling is the key: Recognizing and handling the central circular structure is the key to this problem. Applying a linear model directly takes the solution fundamentally away from what the problem asks.
- Be wary of patches: When an algorithm has a flaw, complicated after-the-fact checks are often wrong or incomplete. The right response is to return to the design and consider how to incorporate the constraint into the model from the beginning.
- Master the standard pattern: Cutting a circle into a line is a general and powerful idea for almost all circular DP and circular-array problems. It is a technique worth knowing well.