055 · Final Prices With a Special Discount in a Shop

algorithm
Published

July 8, 2026

My First Thoughts

嗯,这题是上题的衍生版本吧,对不对。

这里就一个数组,也就是 prices

prices = [1, 2, 3, 4, 5]

肯定是需要遍历的:

for item in prices:

然后实际的解题逻辑是什么呢?

对于每个商品,最终价格由它右边的第一个更小值决定,而且这里的“更小”包含等于。

也就是:如果右边第一个满足条件的价格是 discount,那么当前商品的最终价格就是:

price - discount

首先想到需要一个 stack,这个也很自然。

如果栈是空的,就需要把商品放进去,比如第一个:

if not stack:
    stack.append(item)

然后到了第二个商品,stack 里面已经有一个值了。这个时候就需要判断当前商品是不是比栈最后一个商品更便宜,或者说是不是小于等于栈顶:

if item <= stack[-1]:

如果当前商品更便宜,那么栈最后一个数就需要减去当前商品价格:

stack[-1] - item

这就是那个商品的最终价格,可以放到 result 里面。

但是这里马上有一个问题:应该怎么放进 result 呢?

因为答案要保持原来的顺序。如果栈里面的元素被弹出,或者一次处理了多个旧商品,那么 result 的顺序就不太好维护。

所以会想到:是不是栈里不应该记录价格本身,而应该记录 index

这样每个 index 都能回到原数组里的准确位置。当前价格 prices[i] 可以和 prices[stack[-1]] 比较。如果当前价格可以给栈顶商品打折,就直接修改那个位置:

prices[stack[-1]] -= prices[i]

然后把这个下标弹出来。

这样代码大概就是:

stack = []

length = len(prices)

for i in range(0, length):
    while stack and prices[i] <= prices[stack[-1]]:
        prices[stack[-1]] -= prices[i]
        stack.pop()

    stack.append(i)

return prices

最后还留在 stack 里的商品,说明右边没有小于或等于它的价格。它们本来就没有折扣,所以原价不用再处理。


Why That Is Not Enough

这个思路其实已经是正解了。

真正需要补清楚的地方不是算法方向,而是状态含义:

stack 里到底保存什么?

一开始如果保存价格本身,比如:

stack.append(item)

比较会很方便,但更新答案会麻烦。因为题目要求返回一个和原数组顺序一致的数组,而只保存价格时,我们不知道应该修改原数组里的哪个位置。

所以关键调整是:

stack.append(i)

也就是栈里保存下标,而不是价格。

这样比较时用:

prices[i] <= prices[stack[-1]]

更新时用:

prices[stack[-1]] -= prices[i]

这个状态一旦明确,后面的逻辑就顺了。


Final Idea

用一个栈 stack 保存:

已经看过,但是还没有找到折扣的商品下标。

从左到右遍历 prices

当走到位置 i 时,当前价格 prices[i] 在栈里所有商品的右边。

如果当前价格小于或等于栈顶商品的价格:

prices[i] <= prices[stack[-1]]

说明当前商品就是栈顶商品正在等待的折扣。

于是可以直接更新栈顶商品的最终价格:

prices[stack[-1]] -= prices[i]

然后把这个下标弹出:

stack.pop()

这里要用 while,不是 if

因为同一个当前价格可能同时给前面多个商品打折。

比如:

prices = [8, 6, 2]

看到 2 时,它既可以给 6 打折,也可以继续给 8 打折。

当当前价格不能继续解决栈顶商品时,就把当前下标放入栈里,等待右边之后的商品:

stack.append(i)

遍历结束以后,栈里剩下的下标都没有找到折扣。它们保持原价即可,所以不用额外处理。


Why It Works

栈里保存的是“还没有找到右边第一个小于或等于自己的价格”的商品下标。

从左到右遍历时,当前商品一定在栈里那些商品的右边。

如果当前价格 prices[i] 小于或等于栈顶商品 prices[stack[-1]],那么当前商品可以作为栈顶商品的折扣。

为什么它是“第一个”满足条件的折扣?

因为栈顶商品从入栈到现在,中间经过的商品都没有把它弹出来。也就是说,中间那些商品都没有满足“小于或等于它”的条件。现在第一次遇到满足条件的 prices[i],所以它就是右边第一个可用折扣。

弹出栈顶以后,还要继续检查新的栈顶。

这是因为当前商品可能也能给更早的商品打折。

比如:

prices = [8, 4, 6, 2, 3]

过程可以理解成:

看到 8:还没有折扣,stack = [0]
看到 4:4 <= 8,所以 8 的最终价格变成 4,stack = [],再放入 4 的下标
看到 6:6 不能给 4 打折,stack = [1, 2]
看到 2:2 <= 6,先给 6 打折;2 <= 4,再给 4 打折;最后放入 2 的下标
看到 3:3 不能给 2 打折,放入 3 的下标

最后栈里剩下的是价格 23 的下标。它们右边没有可以用的折扣,所以保持原价。

得到:

[4, 2, 4, 2, 3]

Code

def finalPrices(prices):
    stack = []

    for i in range(len(prices)):
        while stack and prices[i] <= prices[stack[-1]]:
            prices[stack[-1]] -= prices[i]
            stack.pop()

        stack.append(i)

    return prices

Complexity

Time \(O(n)\) - nprices 的长度;每个下标最多入栈一次、出栈一次
Space \(O(n)\) - 最坏情况下所有商品都找不到折扣,栈会保存所有下标

Takeaway

当题目要求给原数组里的某些位置回填答案时,栈里通常保存下标比保存值更灵活。值用来比较,下标用来准确更新答案位置。


Quiz