LeetCode 218. The Skyline Problem
放在待辦清單裡面好久,今天終於拉出來寫。搞了好多種解法,十分快樂。
題目
從定點往遠處看去,所有建築物共同構成的最高點連線稱為天際線。
輸入buildings代表建物的[起點, 終點, 高度],平地高度為0,回傳buildings所構成的天際線。
只有在天際線高度改變時才將[起點, 高度]加入答案陣列中。且多個連續建物為同高度時視為一個輸出,例如[2,3,10],[3,4,10]視為一體,只應將[2,10]加入答案。
解法
雖然底下分類標籤有一堆什麼分治法、樹狀陣列、線段樹、heap之類的,結果我選擇座標壓縮+二分搜。
先遍歷一次buildings,把所有出現的起、終點座標加進集合co中,再將co排序,供後續二分搜使用。
這時co大小為N,建立一個長度同為N的陣列skyline,對應co區段中的天際線。
在遍歷一次buildings,這次分別以起點、終點在co中找到對應位置left和right,對skyline[left:right]的位置更新最大高度。
最後建立ans陣列,遍歷一次天際線高度,並在高度改變時將[原座標, 新高度]加入ans中。
跑了8536ms,有夠久,但至少是有通過。
2023/4/18更新,更新區間其實不用二分搜,直接單純迴圈就快了不少。
在每個點都不重複的情況下,將近每個節點都要更新N次,時間複雜度O(N^2)。空間複雜度O(N)。
class Solution:
def getSkyline(self, buildings: List[List[int]]) -> List[List[int]]:
a=set()
for s,e,_ in buildings:
a.add(s)
a.add(e)
indexes=sorted(a)
mp={x:i for i,x in enumerate(indexes)}
heights=[0]*len(a)
for s,e,h in buildings:
for i in range(mp[s],mp[e]):
if h>heights[i]:
heights[i]=h
ans=[]
prev=-1
for i,h in enumerate(heights):
if h!=prev:
ans.append([indexes[i],h])
prev=h
return ans
return ans
看大部分人都用heap解法,自己試著做了一次。這種應該就是所謂的掃描線。 題目其實有提到,在天際線高度改變時稱為key point。我們先把每棟建物轉成上升的下降的key point,依照發生位置做排序。
heap保存的是進行中的上升key point,以及其結束時間,以高度為鍵值排序。
遍歷keyPoints,如果當前start已經超過heap頂端的結束時間,則此key point已經失效,將其彈出。
若當前的key point為上升,則將其押入heap中。這時heap頂端應該會是所有有效上升key point中高度最大者,檢查其高度是否與答案中上一個key point是否相同,若不同則將其加入答案。
162ms,加速超級多。
class Solution:
def getSkyline(self, buildings: List[List[int]]) -> List[List[int]]:
keyPoints = []
for start, end, h in buildings:
keyPoints.append((start, -h, end))
keyPoints.append((end, 0, 0))
keyPoints.sort()
ans = [(0, 0)]
heap = [(0, math.inf)]
for start, h, end in keyPoints:
while start >= heap[0][1]:
heappop(heap)
if h != 0:
heappush(heap, (h, end))
if ans[-1][1] != -heap[0][0]:
ans.append((start, -heap[0][0]))
return ans[1:]
再來個merge sort解法。136ms,第一種解法已經看不到車尾燈。
把所有建物形成的地平線倆倆合併,最後成為一個答案。
當分割的大小=1時為base case,拆分成上升、下降key point各一個。
否則遞迴拆成左右邊,每次取出起點較早的key point,並更新對應高度;兩端起點相同時則同時取出,更新兩方高度。最後檢查高度是否有變化,若是則加入ans。
class Solution:
def getSkyline(self, buildings: List[List[int]]) -> List[List[int]]:
def merge(l, r):
if l > r:
return []
if l == r: # base case
return [(buildings[l][0], buildings[l][2]), (buildings[l][1], 0)] # up and down
mid = (l+r)//2
q1 = deque(merge(l, mid))
q2 = deque(merge(mid+1, r))
h1 = h2 = 0
ans = []
while q1 or q2:
sl = q1[0][0] if q1 else math.inf
sr = q2[0][0] if q2 else math.inf
if sl < sr:
S, h1 = q1.popleft()
elif sl > sr:
S, h2 = q2.popleft()
else: # equal
S, h1 = q1.popleft()
_, h2 = q2.popleft()
H = max(h1, h2)
if not ans or ans[-1][1] != H:
ans.append((S, H))
return ans
return merge(0, len(buildings)-1)
後來發現付費官方解有並查集,也來練習一下。
這個思路比較少見,幾乎沒有看到這個方式,看來買會員也不算太虧。
這方法一樣要將座標離散化。
主要思路是先處理較高的建築,然後使用並查集紀錄下一步的位置。例如:
有個建築是[1,5,10],代表從1~4高度都是10
座標i範圍1~4,將heights[i]標記為10,然後根節點fa[i]設為5
假設又有一棟建築和[1,5]重疊,但是因為剛才有標記過根節點,所以可以跳過處理過的座標:
後有一棟建築[1,4,2],代表從1~3高度都是2
座標i範圍1~3,但是i=1時就發現已經標記過,所以直接跳到根節點fa[i]
fa[1]=5,已經超出當前建築的右邊界4,不需要任何動作
時間瓶頸在於buildings的排序,時間複雜度O(N log N)。
空間複雜度O(N)。
class Solution:
def getSkyline(self, buildings: List[List[int]]) -> List[List[int]]:
pos=set()
for s,e,_ in buildings:
pos.add(s)
pos.add(e)
N=len(pos)
pos=sorted(pos)
mp={x:i for i,x in enumerate(pos)}
fa=list(range(N))
heights=[0]*N
def find(x):
if fa[x]!=x:
fa[x]=find(fa[x])
return fa[x]
buildings.sort(key=lambda x:-x[2]) # sort by height, desc
for s,e,h in buildings:
left=mp[s]
right=mp[e]
while left<right:
if fa[left]!=left:
left=find(left)
else:
heights[left]=h
fa[left]=right
left+=1
ans=[]
prev=-1
for i,curr in enumerate(heights):
if curr!=prev:
ans.append([pos[i],curr])
prev=curr
return ans