文章目錄
學習資料:
[1] 數據結構系列/單調棧
[2] 單調棧原理及應用 詳解 附各種類型的題目練習
[3] Leetcode 單調棧問題總結(超詳細!!!)
[4] 數據結構——單調棧
單調棧的基本介紹見參考資料 [2].
單調棧的應用有下面2種基本情況:
1、給定一組數,針對每個數,尋找它和它右(左)邊第一個比它大(小)的數之間有多少個數
2、給定一組數,針對每個數,尋找它右(左)邊以最近的數字爲最小值(最大值)的最長單調遞增(遞減)序列有多少個數
這兩種情況的模板是類似的。
題型一
POJ 3250 牛的視野
資料 [1] 中有POJ 3250的參考答案,但是這篇博客對於POJ 3250描述不夠準確,應該改爲:
一羣高度不完全相同的牛從左到右站成一排,每頭牛隻能看見它右邊的比它矮的牛的髮型,若遇到一頭高度大於或等於它的牛,則無法繼續看到這頭牛後面的其他牛(即使它們也比它矮)。給出每頭牛的身高,求每頭牛能看到的牛的總數。
import sys
for n in sys.stdin:
n = int(n)
nums = [int(input().strip()) for _ in range(n)]
nums.append(float('inf')) # 在末尾添加一個無窮高的牛
stack = []
res = [0] * (n+1) # res[i] 表示第 i 頭牛往右能看到多少頭牛
for i in range(n, -1, -1):
while len(stack) > 0 and nums[i] >= nums[stack[-1]]:
stack.pop() # 把nums[i]右邊比它矮的或一樣高的出棧
# 此時如果棧非空,說明棧頂就是右邊第一個比nums[i]高的牛。這之間牛的個數即爲下標之差減一
res[i] = stack[-1] - i -1 if len(stack) > 0 else 0
stack.append(i)
# 如果問每一頭牛能看到多少頭牛,則返回res[:-1]。注意不需要最後一頭無窮高的牛
# 但是本題問所有牛一共能看到多少頭,所以求和。
print(sum(res[:-1]))
美團點評2020校招數據分析方向筆試題——比大小
分析:本題與上一題的不同之處在於:
1、如果一個數右邊沒有比它大的數,則不必記錄差值,保留爲-1即可。故不需要加一個無窮大。
2、根據題意,統計的是 stack[-1] - i
而不是 stack[-1] - i -1
(因爲要包括那個第一個比它高的數)
參考答案:
import sys
for n in sys.stdin:
n = int(n)
nums = [int(input().strip()) for _ in range(n)]
stack = []
res = [-1] * n
for i in range(n - 1, -1, -1): # 不需要加無窮大,從n-1而不是n開始倒序遍歷
while len(stack) > 0 and nums[i] >= nums[stack[-1]]:
stack.pop()
res[i] = stack[-1] - i if len(stack) > 0 else -1 # 下標之差不需要減一
stack.append(i)
print('\n'.join([str(x) for x in res]))
LC739. 每日溫度
題目鏈接:https://leetcode-cn.com/problems/daily-temperatures/
本題基本與前一題相同
class Solution:
def dailyTemperatures(self, T: List[int]) -> List[int]:
n=len(T)
stack=[]
res=[0]*n
for i in range(n-1,-1,-1):
while len(stack)>0 and T[stack[-1]]<=T[i]:
stack.pop()
res[i]=stack[-1]-i if len(stack)>0 else 0
stack.append(i)
return res
環形排列
在上一題的基礎上:如果這些數圍成一個圈,求往順時針方向看多少步纔到達第一個比它高的(如果沒有比它高的,則對應-1)。
思路:
增加了環形屬性後,問題的難點在於:“下一個” 的意義不僅僅是當前元素的右邊了,有可能出現在當前元素的左邊。
解決思路爲:將原始數組“翻倍”,就是在後面再接一個原始數組,這樣的話,按照之前“比身高”的流程,每個元素不僅可以比較自己右邊的元素,而且也可以和左邊的元素比較了。
import sys
for n in sys.stdin:
n = int(n)
nums = [int(input().strip()) for _ in range(n)] * 2 # 再接一個一樣的數組
stack = []
res = [-1] * 2 * n
for i in range(2 * n - 1, -1, -1): # n-1 修改爲 2n-1
while len(stack) > 0 and nums[i] >= nums[stack[-1]]:
stack.pop()
res[i] = stack[-1] - i if len(stack) > 0 else -1
stack.append(i)
print('\n'.join([str(x) for x in res[:n]])) # 注意最後只需要前 n 個的結果
LC84. Largest Rectangle in Histogram
題目鏈接:84. 柱狀圖中最大的矩形
先逆序遍歷,找到向右第一個比它矮的柱子,再順序遍歷,找到向左第一個比它矮的柱子,算出中間有多少柱子即爲寬度,然後寬度乘以高即爲面積。
如果左邊或右邊沒有比它矮的柱子,就沒辦法求出它們中間有多少比它高(或一樣高)的柱子,所以類似於“POJ 3250 牛的視野”,此處需要分別在開頭和末尾插入一個最矮的數0。
class Solution:
def largestRectangleArea(self, heights) -> int:
n = len(heights)
if n==0:return 0
# 開頭和末尾都要插入一個最矮的數
heights.insert(0, 0)
heights.append(0)
stack = []
right_look = [0] * (n + 2)
for i in range(n + 1, -1, -1):
while len(stack) > 0 and heights[i] <= heights[stack[-1]]:
stack.pop()
right_look[i] = stack[-1] - i - 1 if len(stack) > 0 else 0
stack.append(i)
stack.clear()
res = [x for x in right_look]
for i in range(n + 2):
while len(stack) > 0 and heights[i] <= heights[stack[-1]]:
stack.pop()
res[i] += (i - stack[-1] - 1 if len(stack) > 0 else 0) + 1 # 先求出矩形的寬度(往左+往右+自己)
res[i] *= heights[i] # 寬度乘以高
stack.append(i)
return max(res[1:-1]) # 去掉加入的開頭和末尾的0對應的值
求最大區間
題目來自文章開頭寫的參考資料 [3]
本題與上一題LC84類似,但要注意區間的範圍如何用切片來表達。
class Solution:
def largestRectangleArea(self, heights) -> int:
n = len(heights)
heights.insert(0, 0)
heights.append(0)
stack = []
right_look = [0] * (n + 2)
for i in range(n + 1, -1, -1):
while len(stack) > 0 and heights[i] <= heights[stack[-1]]:
stack.pop()
# 區間 [i+1, stack[-1]-1] 的和,即不包括柱子i和柱子stack[-1]
right_look[i] = sum(heights[i + 1:stack[-1]]) if len(stack) > 0 else 0
stack.append(i)
stack.clear()
res = [x for x in right_look]
for i in range(n + 2):
while len(stack) > 0 and heights[i] <= heights[stack[-1]]:
stack.pop()
# 區間 [stack[-1]+1, i-1] 的和,即不包括柱子i和柱子stack[-1]
res[i] += (sum(heights[stack[-1] + 1:i]) if len(stack) > 0 else 0) + heights[i] # 先求出總區間的和(注意加上柱子i)
res[i] *= heights[i] # 區間的和乘以最小值
stack.append(i)
return max(res[1:-1])
題型二
騰訊2020校園招聘編程題——逛街
注意,題型一的棧中需要保存下標,因爲需要在出棧時通過下標相減來得到距離
而在題型二,棧中沒必要保存下標,因爲每個位置對應的結果是 棧的元素個數 而不是 棧頂元素與當前元素的位置差值。
import sys
# 本答案參考自評論區
def parse_nums(nums_str):
return [int(x) for x in nums_str.strip().split()]
for n in sys.stdin:
n = int(n)
nums = parse_nums(input())
assert n == len(nums)
right_look = [0] * n
stack = []
# 從右往左遍歷,統計每個位置往右看能看到多少棟樓(往右的高度單調遞增)
# 注意對比題型一,記錄結果的語句提到了while前,且記錄的是棧的元素個數
for i in range(n - 1, -1, -1):
right_look[i] = len(stack) # 此時棧中的元素個數即爲右邊最長的單調遞增序列的長度
while len(stack) != 0 and nums[i] >= nums[stack[-1]]:
stack.pop()
stack.append(i)
stack.clear()
# 從左往右遍歷,統計每個位置往左看能看到多少棟樓(往左的高度單調遞增)
both_look = [x for x in right_look]
for i in range(n):
both_look[i] += len(stack)+1 # 能看到樓的總數爲往右+往左+自己
while len(stack) != 0 and nums[i] >= nums[stack[-1]]:
stack.pop()
stack.append(i)
both_look = [str(x) for x in both_look]
print(' '.join(both_look))