A clear explanation of maximizing the sightseeing score by tracking the best left value seen so far in a single pass.
Problem Restatement
We are given an integer array values of length n.
The score of a pair (i, j) where i < j is:
values[i] + values[j] + i - jWe need to return the maximum score over all valid pairs.
The official constraints state that 2 <= values.length <= 5 * 10^4 and 1 <= values[i] <= 1000.
Input and Output
| Item | Meaning |
|---|---|
| Input | Array values |
| Output | Maximum score of any pair |
| Score formula | values[i] + values[j] + i - j |
Function shape:
def maxScoreSightseeingPair(values: list[int]) -> int:
...Examples
Example 1:
values = [8, 1, 5, 2, 6]Best pair: (0, 2): 8 + 5 + 0 - 2 = 11.
Answer:
11Example 2:
values = [1, 2]Only pair: (0, 1): 1 + 2 + 0 - 1 = 2.
Answer:
2Key Insight
Rewrite the score as:
(values[i] + i) + (values[j] - j)For a fixed j, the right term (values[j] - j) is fixed.
We want to maximize the left term (values[i] + i) over all i < j.
So we can scan from left to right, maintaining the best (values[i] + i) seen so far.
Algorithm
- Initialize
best_left = values[0] + 0. - Initialize
result = 0. - For each
jfrom1ton - 1:- Update
result = max(result, best_left + values[j] - j). - Update
best_left = max(best_left, values[j] + j).
- Update
- Return
result.
The update order matters: we update result before updating best_left to ensure i < j.
Correctness
At each position j, best_left holds the maximum (values[i] + i) for all i < j.
The current score for the best i and the current j is best_left + values[j] - j.
We track the maximum such score across all j.
After computing the score, we update best_left to include position j as a potential left index for future pairs.
Edge Cases
- Initialize the base states explicitly; most wrong answers come from an implicit zero that should not be allowed.
- Confirm the iteration order matches the recurrence dependencies.
- For optimization DP, separate impossible states from valid states with value
0.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | Single pass |
| Space | O(1) | Constant extra space |
Common Pitfalls
- Do not overwrite a state before all transitions that still need the old value have used it.
- Use the problem constraints to choose between full tables and compressed state.
Implementation
class Solution:
def maxScoreSightseeingPair(self, values: list[int]) -> int:
best_left = values[0] + 0
result = 0
for j in range(1, len(values)):
result = max(result, best_left + values[j] - j)
best_left = max(best_left, values[j] + j)
return resultCode Explanation
best_left tracks the maximum values[i] + i seen so far:
best_left = values[0] + 0For each j, compute the best pair score using this left:
result = max(result, best_left + values[j] - j)Then update best_left to include position j as a future left:
best_left = max(best_left, values[j] + j)The update to result must come before the update to best_left to preserve the i < j constraint.
Testing
def run_tests():
s = Solution()
assert s.maxScoreSightseeingPair([8, 1, 5, 2, 6]) == 11
assert s.maxScoreSightseeingPair([1, 2]) == 2
assert s.maxScoreSightseeingPair([1, 3, 5]) == 7
assert s.maxScoreSightseeingPair([10, 1, 1]) == 10
print("all tests passed")
run_tests()| Test | Expected | Why |
|---|---|---|
[8,1,5,2,6] | 11 | Best pair is (0,2) |
[1,2] | 2 | Only pair |
[1,3,5] | 7 | Pair (1,2): 3+5+1-2=7 |
[10,1,1] | 10 | Best pair (0,1): 10+1+0-1=10 |