關於 web service, unity, blogger 等軟體工程筆記

LeetCode #84 Largest Rectangle in Histogram

Edit icon 沒有留言
LeetCode using Golang

Given n non-negative integers representing the histogram's bar height where the width of each bar is 1, find the area of largest rectangle in the histogram.

Above is a histogram where width of each bar is 1, given height = [2,1,5,6,2,3]
The largest rectangle is shown in the shaded area, which has area = 10 unit.

For example, given heights = [2,1,5,6,2,3], return 10.

給予一直方圖 (histogram) 的資料,假設每條柱子 (bar) 寬度都為 1,計算其最大矩形 (rectangle) 面積,可參考其題目示意圖。

解法一:暴力法

直覺的想法,針對直方圖的每一條柱子,計算其高度在鄰近柱子的矩形面積,然後比較回傳最大的矩行面積即可,其計算矩形面積也很容易,分別向左右邊柱子方面尋找,找到其柱子比當前柱子高度小就停止,之後計算其矩形面積,參考示意圖:

計算紅色柱子所在直方圖的矩形面積,向左邊與右邊延展其矩形,直到柱子高度低於紅色柱子高度為止

計算紅色柱子所在直方圖的矩形面積,向左邊與右邊延展其矩形,直到柱子高度低於紅色柱子高度為止

因此寫出以下解法,假設直方圖共有 n 的柱子,其演算法時間複雜度為 O(n2),空間複雜度為 O(1):

func largestRectangleArea(heights []int) int {
   maxArea := 0
   for i := 0; i < len(heights); i++{
      area := heights[i]
      
      // Left side
      for k := i-1; k >= 0; k--{
         if heights[k] >= heights[i]{
            area += heights[i]
         }else{
            break
         }
      }
      
      // Right side
      for k := i+1; k < len(heights); k++{
         if heights[k] >= heights[i]{
            area += heights[i]
         }else{
            break
         }
      }
      
      if area > maxArea{
         maxArea = area
      }
   }
   
   return maxArea
}

解法二:使用 Stack

解法一效率比較差,思考有沒有辦法只要跑一次直方圖 O(n) 就能夠計算完畢的演算法,經由推敲與關鍵字,使用 Stack 先進後出的特性,來計算每個柱子所佔據的矩形面積,然後最後回傳最大的矩形面積。

假設與之前一樣,由直方圖最左邊柱子掃描到最右邊的柱子,先考慮到兩種 Case,第一種是 i 柱子比 i-1 柱子還要高,那表示 i-1 柱子還可以繼續延展向右邊其矩形面積,且 i 柱子也可以開始計算其矩形面積,i 柱子為其矩形最右側的柱子:

i 柱子比 i-1 柱子高的示意圖

i 柱子比 i-1 柱子高的示意圖

反之第二種 Case,是 i 柱子比 i-1 柱子還要矮,那表示 i-1 柱子已經沒有辦法延展其矩形面積,可以計算該柱子的矩形面積,並且之後與最大矩形面積最比較,而 i 柱子一樣是開始計算其矩形面積,但該柱子將不會在是其矩形最右側的柱子:

i 柱子比 i-1 柱子矮的示意圖

i 柱子比 i-1 柱子矮的示意圖

首先先準備一個 stack 紀錄每個柱子的索引 (index),每個未計算其矩形面積的柱子,都必須將其索引 push 到該 stack 中。若該柱子的高度比 stack 末端紀錄 (peek) 的柱子高度還要低的話,便執行第二種 Case 的處理。

將 stack pop 取得其柱子索引以取得該柱子高度,其矩形最右側柱子索引是 i-1,而最左側柱子索引則是看 stack 目前末端紀錄,會是為 stack.peek() + 1,因此其矩形寬度為 (i-1) - (stack.peek() + 1) + 1。

若 stack 已經沒有任何柱子索引資料,那表示會是下圖 i-1 柱的例子,其矩形最左側柱子索引為 0,其寬度會是 (i - 1) - (0) + 1:

計算 i-1 柱矩形面積示意圖,其矩形寬度會是 i (索引由 0 開始計算)

計算 i-1 柱矩形面積示意圖,其矩形寬度會是 i (索引由 0 開始計算)

有矩形高度與寬度便能計算其矩形面積,找到最大的矩形面積回傳即可,完成以下程式碼實作,假設直方圖共有 n 的柱子,其演算法時間複雜度為 O(n2),空間複雜度為 O(n2):

type stack []int

func (s stack) Peek() int {
   return s[len(s)-1]
}

func (s stack) Push(v int) stack {
   return append(s, v)
}

func (s stack) Pop() (stack, int) {
   l := len(s)
   return s[:l-1], s[l-1]
}

func (s stack) Len() int {
   return len(s)
}

func newStack(capacity int) stack {
   return stack(make([]int, 0, capacity))
}

func largestRectangleArea(heights []int) int {
   if heights == nil || len(heights) <= 0{
      return 0;
   }

   maxArea := 0
   stack := newStack(len(heights))
   heights = append(heights, 0)  // dummy end
   
   for i := 0; i < len(heights); i++ {
      ch := heights[i]
      for stack.Len() > 0 && ch <= heights[stack.Peek()] {
         h:= heights[stack.Peek()]
           stack, _ = stack.Pop()
         w := i
         if stack.Len() > 0{
            w = i -1 - stack.Peek()
         }

         if area := h * w; area > maxArea{
            maxArea = area
         }
      }
      
      stack = stack.Push(i)
   }
   
   return maxArea
}

這實作並不是一次到位,先是實作以下的程式碼,在 Leetcode 確定能通過所有測試後,才去尋找其他大神們的解法,最後從頭實作以上的程式碼。

這兩個程式碼在 Leetcode submissions 上顯示的執行時間都差不多,接近 20 ms,但是以上的程式碼相較之下非常簡潔許多,越比較真的是越沒自信呢。

type data struct {
   Height int
   Start  int
}

type stack []data

func (s stack) Peek() data {
   return s[len(s)-1]
}

func (s stack) Push(v data) stack {
   return append(s, v)
}

func (s stack) Pop() (stack, data) {
   l := len(s)
   return s[:l-1], s[l-1]
}

func (s stack) Len() int {
   return len(s)
}

func newStack(capacity int) stack {
   return stack(make([]data, 0, capacity))
}

func largestRectangleArea(heights []int) int {
   maxArea := 0
   stack := newStack(len(heights))
 Add  Dummy
   for i := 0; i <= len(heights); i++ {
      ch := 0
      start := i
      
      if i < len(heights) {
         ch = heights[i]
      }
      
      for stack.Len() > 0 {
         sd := stack.Peek()
         sh := sd.Height
         if ch < sh {
            if area := (i - sd.Start) * sh; area > maxArea {
               maxRectange = rectange
            }
            stack, _ = stack.Pop()
            start = sd.Start
         } else {
            break
         }
      }
      
      stack = stack.Push(data{
         ch,
         start,
      })
   }
   
   return maxRectange
}

沒有留言: