1. 程式人生 > 實用技巧 >LeetCode 85 | 如何從矩陣當中找到數字圍成的最大矩形的面積?(轉載)

LeetCode 85 | 如何從矩陣當中找到數字圍成的最大矩形的面積?(轉載)

本文始發於個人公眾號:TechFlow,原創不易,求個關注

今天是LeetCode專題53篇文章,我們一起來看看LeetCode中的85題,Maximal Rectangle(最大面積矩形)。

今天的這道題目和上一篇文章講的Largest Rectangle in Histogram這題有一定的相似,所以如果沒有看過上一篇文章的同學,建議先移步觀看一下上一篇。

LeetCode 84 | 單調棧解決最大矩形問題

85題的官方難度是Hard,點贊2757,反對69,通過率37.2%左右。它的情況和84題非常相似,點贊比很高,然後通過率也差不多。雖然它是84題的變形題,但是整體的題目質量還是很高的,沒有因為這一點被詬病。那麼和84題相比,究竟它的變動在哪裡呢,讓我們一起來看題目吧。

題意

給定一個只包含0和1的數字矩陣,要求在這個矩陣當中找到一個由1組成的最大面積的矩形,返回這個面積。

我們來看個樣例:

Input:
[
  ["1","0","1","0","0"],
  ["1","0","1","1","1"],
  ["1","1","1","1","1"],
  ["1","0","0","1","0"]
]
Output: 6

答案是6,是這一塊1圍成的矩形:

題解

還是老規矩,我們從最簡單的方法入手,一點點推匯出最佳的思路。

暴力

首先最簡單的當然是暴力,這題讓我們尋找一個矩形,直接尋找矩形是有點麻煩的。計算機程式不像人眼,可以直接獲取到圖形相關的資訊,計算機不行,只能獲得單個位置的資訊。所以我們讓程式直接判斷矩形是不現實的,但我們可以通過特徵點來鎖定矩形,這個也是業內常用的套路。

鎖定一個矩形的方法一般有兩種,第一種是用矩形的中心點和長寬來確定。這一種在各種影象識別和目標檢測演算法當中經常用到,模型預測的結果就是影象中心點的座標以及長寬的長度。

第二種方法可以通過矩形的對角線上的兩個點來確定,這種方法只適用於和座標軸平行的矩形。比如下圖當中,無論我們知道了(x2, y2), (x3, y3)還是(x1, y1), (x4, y4),我們都可以將這個矩形確定下來。

有了確定矩形的方法之後,我們通過暴力法來求解就簡單了。我們通過這些值來列舉所有可能構成的矩形,然後依次遍歷矩形中的每一個元素,來判斷它們是否全是1,如果是否的話,那麼就排除,否則則用來更新答案。

這種方法固然可行,但是估算一下,差不多應該是的規模,顯然是我們不能接受的。

分析問題

在暴力解法當中我們遇到了時間複雜度的困難,我們想要優化就必須要解決複雜度的問題,複雜度的問題怎麼解決呢?幹想肯定是不行的,我們需要轉變一下思路,尋找一下突破口。

我們列舉的複雜度規模這麼高是因為我們遍歷了所有矩形,遍歷矩形本身就是一個時間複雜度開銷非常大的舉動。如果不想遍歷矩形,還有什麼方法可以得出最大面積呢?如果我們聯想一下上一題很容易得出答案。

在上一題84題當中,題目給出的是一個個豎直型別的矩形,要求這些矩形組合當中能夠找到的最大面積。

在這題當中我們可以對01的數字矩陣也做這麼一個類似的變形,將從底部開始連續延伸的1的數量看成是豎直襬放的矩形的高度,這樣我們這題就可以使用上一題的思路進行求解了。

  ["1","0","1","0","0"],
  ["1","0","1","1","1"],
  ["1","1","1","1","1"],
  ["1","0","0","1","0"]

比如說上面這個矩陣就可以轉變為[4, 0, 0, 3, 0],其實就是我們一列一列看,從最低處往上連續的1的數量。但是這樣找到的面積最大值是4,並不是答案的6,原因是因為我們尋找的底層不對,並不一定以最後一行作為底面得到的面積最大。所以我們需要遍歷作為底層的行,然後用這種方法尋找最大面積,全域性當中找到的最大面積就是答案。

在上一題我們計算矩形面積的時候用到了兩個單調棧,分別計算了某一個高度向左、向右能夠延伸到的最遠距離,其實這並沒有必要。因為我們用一個棧也可以同時計算出兩邊的邊界。舉個例子:[1, 3, 6, 7],當前元素是5。我們需要把6,7出棧,5入棧。我們知道了5的左邊界是3,但仔細想一想,對於7來說,我們知道了它的左右邊界。7的左邊界是6,右邊界是5。也就是說對於棧頂的元素而言,它的左邊界是stack[top-1],右邊界是當前的位置i,寬就是i - stack[top-1] - 1。

class Solution:
    def maximalRectangle(self, matrix: List[List[str]]) -> int:
        # 求出行數n和列數m
        n = len(matrix)
        if n == 0:
            return 0

        m = len(matrix[0])
  # 儲存每一層的高度
        height = [0 for _ in range(m+1)]
        res = 0
        # 遍歷以哪一層作為底層
        for i in range(n):
            sk = [-1]
            for j in range(m+1):
                # 計算j位置的高度,如果遇到0則置為0,否則遞增
                h = 0 if j == m or matrix[i][j] == '0' else height[j] + 1
                height[j] = h
                # 單調棧維護長度
                while len(sk) > 1 and h < height[sk[-1]]:
                    res = max(res, (j-sk[-2]-1) * height[sk[-1]])
                    sk.pop()
                sk.append(j)
        return res

總結

乍一看這道題好像非常複雜,但是當我們對它進行分析和變形之後,它又變回了普通的單調棧的應用題。在單調棧的使用當中,有兩個細節,一個細節是棧在初始化的時候插入了-1,插入-1是作為一個標兵,也就是所有情況能夠達到的最左側的邊界。另一個細節是維護結束的時候插入了0,插入0的目的是為了彈出棧內所有的元素,因為只有出棧的元素會計算構成的面積,這樣可以保證不會遺漏情況。

除了上面提到的之外,還有其他的一些細節,比如陣列的建立的長度,還有矩形面積的計算公式等等。很多時候演算法之所以難以實現,也正是因為需要考慮的細節很多,整體的邏輯不是非常清楚,需要我們進行大量的思考。總體來說,這是一道非常優秀的問題,值得大家仔細鑽研。

今天的文章到這裡就結束了,如果喜歡本文的話,請來一波素質三連,給我一點支援吧(關注、在看、點贊)。

原文連結:https://www.cnblogs.com/techflow/p/13359851.html