Builds on Linear DP, which you haven’t finished yet.
Best Left Half Carry (Best Sightseeing Pair)
best_left, is the whole state.You get an integer array values. Each position is one sightseeing spot along a road, and values[i] is how good spot i is. A pair is two different spots i and j with i < j.
The score of a pair is the two values added together, minus the distance between the spots: values[i] + values[j] - (j - i). Return the highest score that any pair can have.
8 + 5 - 2 = 11. Every other pair scores less.Best Sightseeing Pair
i < j of an array to get the highest score, where the score adds one term for i and one term for j, and the distance j - i is part of it, for example values[i] + values[j] - (j - i).| 1 | def max_score_sightseeing_pair(values: list[int]) -> int: |
| 2 | """Best Sightseeing Pair: carry the best left spot, values[i] + i, and score every spot j against it. |
| 3 | Time: O(N) | Space: O(1) |
| 4 | """ |
| 5 | # best_left: the largest values[i] + i over the spots before j; best: the largest pair score found so far |
| 6 | best_left = values[0] |
| 7 | best = float('-inf') |
| 8 | for j in range(1, len(values)): |
| 9 | # 1. Score Spot j As The Right End: |
| 10 | # The score is (values[i] + i) + (values[j] - j), so the best partner for j is the spot with the largest values[i] + i before j. |
| 11 | best = max(best, best_left + values[j] - j) |
| 12 | Â |
| 13 | # 2. Update best_left With Spot j: |
| 14 | # This runs after the score, so spot j is never paired with itself. |
| 15 | best_left = max(best_left, values[j] + j) |
| 16 | Â |
| 17 | return best |
j is scored, best_left is the largest values[i] + i over all spots i < j, and best is the largest score of any pair that ends before j.The score values[i] + values[j] + i - j splits into a part that needs only i, which is values[i] + i, and a part that needs only j, which is values[j] - j. So for each j the best partner is the earlier spot with the largest values[i] + i, and one carried value, best_left, holds it. best_left is the maximum over every earlier spot, so no pair is missed, and each spot is read once. The trap is the order inside the loop. Score j first with best = max(best, best_left + values[j] - j), and only then update best_left with j, using best_left = max(best_left, values[j] + j). If best_left changes first, spot j is paired with itself and scores 2 * values[j].
See it run
Best Sightseeing Pair, step by step
best_left 8best -inf
Each column is one spot, numbered from 0.
jthe spot being scoredbest_leftthe earlier spot with the largest values[i] + i
Start: best_left = values[0] + 0 = 8. Spot 0 is the only spot before spot 1.
Gotcha 1 of 4
- The trap
Gotcha 1: Updating
best_leftwith spotjbefore scoringjMistakebest_left = max(best_left, values[j] + j) best = max(best, best_left + values[j] - j)Fixbest = max(best, best_left + values[j] - j) best_left = max(best_left, values[j] + j)What happens:
best_leftmust hold only the spots beforej. Whenbest_leftis updated first, it already holdsvalues[j] + j. The score then uses spotjat both ends of the pair. That score is(values[j] + j) + (values[j] - j), which is2 * values[j]. Atj = 4in the example, the pair scores 12, which is 2 * 6, and no real pair scores that high.Example: Best Sightseeing Pair (LC 1014): values = [8, 1, 5, 2, 6]Mistake: returns 12Fix: returns 11
- The score adds up every number between the two ends, as in Maximum Subarray (LC 53), instead of only the two end values and the distance between them, so the carried value is a running sum, not a best left end. See Kadane's Algorithm
- You may buy and sell many times, as in Best Time to Buy and Sell Stock II (LC 122), so every day has a state (holding a share or not), not one best left end for a single pair. See State Machine DP
- Every position needs the best value on its left and also the best value on its right, as in Trapping Rain Water (LC 42), so one pass from the left is not enough. See Prefix-Suffix Scan
Deep Dive
The observation: split the score into a left part and a right part
A pair of spots i < j scores values[i] + values[j] + i - j. Move the terms: the score is (values[i] + i) + (values[j] - j). The first bracket reads only i. The second bracket reads only j.
Example: values = [8, 1, 5, 2, 6], i = 0 and j = 2. The left part is 8 + 0 = 8. The right part is 5 - 2 = 3. The score is 8 + 3 = 11, which is the answer.
For a fixed j, the right part is one number. Only the left part changes with i. So the best partner of j is the spot i < j with the largest values[i] + i.
Example: for j = 4, the left parts of spots 0 to 3 are 8, 2, 7 and 5, and the right part is 6 - 4 = 2. The best left part is 8, so the best score that ends at spot 4 is 10.
Why one variable, best_left, is enough
At the start of the iteration for j, best_left is the largest values[i] + i over all i < j. This is a prefix maximum. One comparison per spot keeps it up to date, so no table is needed.
The best pair that ends at j scores best_left + values[j] - j. Every pair (i, j) is counted under its right end j, and its score is at most the score with the best partner. So best, the maximum over all j, is the best pair.
The brute force scores N * (N - 1) / 2 pairs. For N = 5 * 104 that is 1,249,975,000 pairs. The loop reads each spot once.
What this card adds to Linear DP
Linear DP walks the array once and keeps the state in a few registers. This card keeps that single pass and that rolling register.
The new part is what the register holds. In Linear DP, the state at step i is built from the one or two states just before it. Here the register is a maximum over all earlier spots, and the answer is a separate maximum, best, over all right ends. That is why there are two variables.
This is not Kadane's Algorithm. Kadane's state is the sum of one unbroken run. Here the numbers between i and j are not added. Only the two end values and the distance j - i count.
The order inside the loop, and the same idea with a minimum
The loop scores j first and updates best_left with j second. When best_left is updated first, it already holds spot j, so the score pairs j with itself and equals 2 * values[j]. For [8, 1, 5, 2, 6], the update-first order returns 12 and the correct order returns 11.
Best Time to Buy and Sell Stock (LC 121) uses the same loop. The profit of a trade is prices[j] - prices[i], so the left part is -prices[i], the right part is prices[j], and best_left is the lowest price so far, negated. Positions are not part of that score, so there is no - j.
Time is O(N), because the loop does constant work for each of the N - 1 spots. Space is O(1), because only best_left and best are kept.
60-Second Pitch
"The brute force scores every pair, which is about N squared over two pairs. I split the score into a left part, values i plus i, and a right part, values j minus j. For a fixed j the right part is one number, so the best partner is the earlier spot with the largest left part. I keep that maximum in one variable, best left, which starts as spot 0. For each j from 1, I first score the pair, best left plus values j minus j, and keep the largest score. Only after that I update best left with j. If I did it before, j would pair with itself. That is one pass, O of N time and O of 1 space. For 8, 1, 5, 2, 6 the answer is 11, from spots 0 and 2."
Common Questions
- Why can the loop not keep the largest
values[i]and fix the distance later? The distance depends on which spot is chosen. A spot with a smaller value but a larger index
ican lose less to the distance. The left term must bevalues[i] + i. For[1, 1, 1, 1, 9], the largest value alone gives 6, and the correct answer is 9.- How does the same loop solve Best Time to Buy and Sell Stock (LC 121)?
The profit is
prices[j] - prices[i], so the left term is-prices[i]and the right term isprices[j].best_leftis the lowest price so far, negated. The positions are not part of the score, so there is no- j. A trade is optional, sobeststarts at 0.- How do you return the pair of spots and not only the score?
Keep the index of the spot that gave
best_left, calledleft_index. When the score line improvesbest, save(left_index, j). When the update line improvesbest_left, setleft_index = j. For[8, 1, 5, 2, 6], this returns the spots 0 and 2.- What changes when the score rewards distance, as in
values[i] + values[j] + j - i? The terms move to the other side. The left term is
values[i] - iand the right term isvalues[j] + j. The loop keeps the same shape and the same order: scorejfirst, then updatebest_leftwithj.- Can the time or the space be smaller?
The loop uses
O(N)time andO(1)space. Every value must be read at least once, so no algorithm can take less thanO(N)time. The space is already constant, because onlybest_leftandbestare kept.
SWE Reasoning
Spot it
- The question asks for the best pair of positions
i < j, not for a sum over a run of numbers. - The score adds a term that reads only
iand a term that reads onlyj. The distancej - isplits into+ ifor the left spot and- jfor the right spot. - The brute force scores all
N * (N - 1) / 2pairs. ForN = 5 * 104, that is about 1.25 * 10^9 pairs. - For a fixed
j, only the left term changes withi, so the bestiis a maximum over a prefix of the array.
The invariant, step by step
- Before the loop,
best_left = values[0]andbest = float('-inf'). Spot 0 is the only spot before spot 1, and its left term isvalues[0] + 0. - At the start of the iteration for
j,best_leftis the largestvalues[i] + iover alli < j. - The score line sets
best = max(best, best_left + values[j] - j). This is the best pair that ends atj. - The update line sets
best_left = max(best_left, values[j] + j). It runs after the score line, sobest_leftstill holds only the spots beforejwhenjis scored. Afterwards it holds all spots up toj, which is what the iteration forj + 1needs. - After the last
j, every pair(i, j)was considered through its right endj.bestis the largest score. - Trace for
[8, 1, 5, 2, 6]: afterj = 1, 2, 3, 4,bestis 8, 11, 11, 11 andbest_leftis 8, 8, 8, 10. The answer is 11, from spots 0 and 2.
Traps
- Updating
best_leftwithjbefore scoringj.best_leftalready holds spotj, sojis paired with itself and scores2 * values[j].[8, 1, 5, 2, 6]returns 12 instead of 11. - Carrying
values[i]without+ i. The distance is lost from the left term.[1, 1, 1, 1, 9]returns 6 instead of 9. - Leaving out
- jfrom the score. The distance is never subtracted.[8, 1, 5, 2, 6]returns 14 instead of 11. - Starting the loop at
j = 0. Spot 0 is scored against itself. The same input returns 16. - Starting
bestat minus infinity in Best Time to Buy and Sell Stock (LC 121). A trade is optional there, so the answer is never negative.[7, 6, 4, 3, 1]returns -1 instead of 0. Startbestat 0. - Copying the signs when the score rewards distance. For the score
values[i] + values[j] + j - i, the left term isvalues[i] - iand the right term isvalues[j] + j. Read the signs from the formula, not from this card.
Practice
Builds on Linear DP, which you haven’t finished yet.
Best Left Half Carry (Best Sightseeing Pair)
best_left, is the whole state.You get an integer array values. Each position is one sightseeing spot along a road, and values[i] is how good spot i is. A pair is two different spots i and j with i < j.
The score of a pair is the two values added together, minus the distance between the spots: values[i] + values[j] - (j - i). Return the highest score that any pair can have.
8 + 5 - 2 = 11. Every other pair scores less.Best Sightseeing Pair
i < j of an array to get the highest score, where the score adds one term for i and one term for j, and the distance j - i is part of it, for example values[i] + values[j] - (j - i).| 1 | def max_score_sightseeing_pair(values: list[int]) -> int: |
| 2 | """Best Sightseeing Pair: carry the best left spot, values[i] + i, and score every spot j against it. |
| 3 | Time: O(N) | Space: O(1) |
| 4 | """ |
| 5 | # best_left: the largest values[i] + i over the spots before j; best: the largest pair score found so far |
| 6 | best_left = values[0] |
| 7 | best = float('-inf') |
| 8 | for j in range(1, len(values)): |
| 9 | # 1. Score Spot j As The Right End: |
| 10 | # The score is (values[i] + i) + (values[j] - j), so the best partner for j is the spot with the largest values[i] + i before j. |
| 11 | best = max(best, best_left + values[j] - j) |
| 12 | Â |
| 13 | # 2. Update best_left With Spot j: |
| 14 | # This runs after the score, so spot j is never paired with itself. |
| 15 | best_left = max(best_left, values[j] + j) |
| 16 | Â |
| 17 | return best |
j is scored, best_left is the largest values[i] + i over all spots i < j, and best is the largest score of any pair that ends before j.The score values[i] + values[j] + i - j splits into a part that needs only i, which is values[i] + i, and a part that needs only j, which is values[j] - j. So for each j the best partner is the earlier spot with the largest values[i] + i, and one carried value, best_left, holds it. best_left is the maximum over every earlier spot, so no pair is missed, and each spot is read once. The trap is the order inside the loop. Score j first with best = max(best, best_left + values[j] - j), and only then update best_left with j, using best_left = max(best_left, values[j] + j). If best_left changes first, spot j is paired with itself and scores 2 * values[j].
See it run
Best Sightseeing Pair, step by step
best_left 8best -inf
Each column is one spot, numbered from 0.
jthe spot being scoredbest_leftthe earlier spot with the largest values[i] + i
Start: best_left = values[0] + 0 = 8. Spot 0 is the only spot before spot 1.
Gotcha 1 of 4
- The trap
Gotcha 1: Updating
best_leftwith spotjbefore scoringjMistakebest_left = max(best_left, values[j] + j) best = max(best, best_left + values[j] - j)Fixbest = max(best, best_left + values[j] - j) best_left = max(best_left, values[j] + j)What happens:
best_leftmust hold only the spots beforej. Whenbest_leftis updated first, it already holdsvalues[j] + j. The score then uses spotjat both ends of the pair. That score is(values[j] + j) + (values[j] - j), which is2 * values[j]. Atj = 4in the example, the pair scores 12, which is 2 * 6, and no real pair scores that high.Example: Best Sightseeing Pair (LC 1014): values = [8, 1, 5, 2, 6]Mistake: returns 12Fix: returns 11
- The score adds up every number between the two ends, as in Maximum Subarray (LC 53), instead of only the two end values and the distance between them, so the carried value is a running sum, not a best left end. See Kadane's Algorithm
- You may buy and sell many times, as in Best Time to Buy and Sell Stock II (LC 122), so every day has a state (holding a share or not), not one best left end for a single pair. See State Machine DP
- Every position needs the best value on its left and also the best value on its right, as in Trapping Rain Water (LC 42), so one pass from the left is not enough. See Prefix-Suffix Scan
Deep Dive
The observation: split the score into a left part and a right part
A pair of spots i < j scores values[i] + values[j] + i - j. Move the terms: the score is (values[i] + i) + (values[j] - j). The first bracket reads only i. The second bracket reads only j.
Example: values = [8, 1, 5, 2, 6], i = 0 and j = 2. The left part is 8 + 0 = 8. The right part is 5 - 2 = 3. The score is 8 + 3 = 11, which is the answer.
For a fixed j, the right part is one number. Only the left part changes with i. So the best partner of j is the spot i < j with the largest values[i] + i.
Example: for j = 4, the left parts of spots 0 to 3 are 8, 2, 7 and 5, and the right part is 6 - 4 = 2. The best left part is 8, so the best score that ends at spot 4 is 10.
Why one variable, best_left, is enough
At the start of the iteration for j, best_left is the largest values[i] + i over all i < j. This is a prefix maximum. One comparison per spot keeps it up to date, so no table is needed.
The best pair that ends at j scores best_left + values[j] - j. Every pair (i, j) is counted under its right end j, and its score is at most the score with the best partner. So best, the maximum over all j, is the best pair.
The brute force scores N * (N - 1) / 2 pairs. For N = 5 * 104 that is 1,249,975,000 pairs. The loop reads each spot once.
What this card adds to Linear DP
Linear DP walks the array once and keeps the state in a few registers. This card keeps that single pass and that rolling register.
The new part is what the register holds. In Linear DP, the state at step i is built from the one or two states just before it. Here the register is a maximum over all earlier spots, and the answer is a separate maximum, best, over all right ends. That is why there are two variables.
This is not Kadane's Algorithm. Kadane's state is the sum of one unbroken run. Here the numbers between i and j are not added. Only the two end values and the distance j - i count.
The order inside the loop, and the same idea with a minimum
The loop scores j first and updates best_left with j second. When best_left is updated first, it already holds spot j, so the score pairs j with itself and equals 2 * values[j]. For [8, 1, 5, 2, 6], the update-first order returns 12 and the correct order returns 11.
Best Time to Buy and Sell Stock (LC 121) uses the same loop. The profit of a trade is prices[j] - prices[i], so the left part is -prices[i], the right part is prices[j], and best_left is the lowest price so far, negated. Positions are not part of that score, so there is no - j.
Time is O(N), because the loop does constant work for each of the N - 1 spots. Space is O(1), because only best_left and best are kept.
60-Second Pitch
"The brute force scores every pair, which is about N squared over two pairs. I split the score into a left part, values i plus i, and a right part, values j minus j. For a fixed j the right part is one number, so the best partner is the earlier spot with the largest left part. I keep that maximum in one variable, best left, which starts as spot 0. For each j from 1, I first score the pair, best left plus values j minus j, and keep the largest score. Only after that I update best left with j. If I did it before, j would pair with itself. That is one pass, O of N time and O of 1 space. For 8, 1, 5, 2, 6 the answer is 11, from spots 0 and 2."
Common Questions
- Why can the loop not keep the largest
values[i]and fix the distance later? The distance depends on which spot is chosen. A spot with a smaller value but a larger index
ican lose less to the distance. The left term must bevalues[i] + i. For[1, 1, 1, 1, 9], the largest value alone gives 6, and the correct answer is 9.- How does the same loop solve Best Time to Buy and Sell Stock (LC 121)?
The profit is
prices[j] - prices[i], so the left term is-prices[i]and the right term isprices[j].best_leftis the lowest price so far, negated. The positions are not part of the score, so there is no- j. A trade is optional, sobeststarts at 0.- How do you return the pair of spots and not only the score?
Keep the index of the spot that gave
best_left, calledleft_index. When the score line improvesbest, save(left_index, j). When the update line improvesbest_left, setleft_index = j. For[8, 1, 5, 2, 6], this returns the spots 0 and 2.- What changes when the score rewards distance, as in
values[i] + values[j] + j - i? The terms move to the other side. The left term is
values[i] - iand the right term isvalues[j] + j. The loop keeps the same shape and the same order: scorejfirst, then updatebest_leftwithj.- Can the time or the space be smaller?
The loop uses
O(N)time andO(1)space. Every value must be read at least once, so no algorithm can take less thanO(N)time. The space is already constant, because onlybest_leftandbestare kept.
SWE Reasoning
Spot it
- The question asks for the best pair of positions
i < j, not for a sum over a run of numbers. - The score adds a term that reads only
iand a term that reads onlyj. The distancej - isplits into+ ifor the left spot and- jfor the right spot. - The brute force scores all
N * (N - 1) / 2pairs. ForN = 5 * 104, that is about 1.25 * 10^9 pairs. - For a fixed
j, only the left term changes withi, so the bestiis a maximum over a prefix of the array.
The invariant, step by step
- Before the loop,
best_left = values[0]andbest = float('-inf'). Spot 0 is the only spot before spot 1, and its left term isvalues[0] + 0. - At the start of the iteration for
j,best_leftis the largestvalues[i] + iover alli < j. - The score line sets
best = max(best, best_left + values[j] - j). This is the best pair that ends atj. - The update line sets
best_left = max(best_left, values[j] + j). It runs after the score line, sobest_leftstill holds only the spots beforejwhenjis scored. Afterwards it holds all spots up toj, which is what the iteration forj + 1needs. - After the last
j, every pair(i, j)was considered through its right endj.bestis the largest score. - Trace for
[8, 1, 5, 2, 6]: afterj = 1, 2, 3, 4,bestis 8, 11, 11, 11 andbest_leftis 8, 8, 8, 10. The answer is 11, from spots 0 and 2.
Traps
- Updating
best_leftwithjbefore scoringj.best_leftalready holds spotj, sojis paired with itself and scores2 * values[j].[8, 1, 5, 2, 6]returns 12 instead of 11. - Carrying
values[i]without+ i. The distance is lost from the left term.[1, 1, 1, 1, 9]returns 6 instead of 9. - Leaving out
- jfrom the score. The distance is never subtracted.[8, 1, 5, 2, 6]returns 14 instead of 11. - Starting the loop at
j = 0. Spot 0 is scored against itself. The same input returns 16. - Starting
bestat minus infinity in Best Time to Buy and Sell Stock (LC 121). A trade is optional there, so the answer is never negative.[7, 6, 4, 3, 1]returns -1 instead of 0. Startbestat 0. - Copying the signs when the score rewards distance. For the score
values[i] + values[j] + j - i, the left term isvalues[i] - iand the right term isvalues[j] + j. Read the signs from the formula, not from this card.