221. Maximal Square - Explanation

Problem Link

Description

You are given an m x n binary matrix filled with 0's and 1's, find the largest square containing only 1's and return its area.

Example 1:

Input: matrix = [
    ["1","0","1","0","0"],
    ["1","0","1","1","1"],
    ["1","1","1","1","1"],
    ["1","0","0","1","0"]
]

Output: 4

Example 2:

Input: matrix = [
    ["0","1"],
    ["1","0"]
]

Output: 1

Constraints:

  • 1 <= matrix.length, matrix[i].length <= 300
  • matrix[i][j] is '0' or '1'.


Topics

Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • 2D Arrays/Matrices - Traversing and manipulating elements in a grid structure
  • Dynamic Programming - Building solutions from smaller subproblems and using recurrence relations
  • Space Optimization - Reducing space complexity by recognizing that only previous row/column values are needed

1. Brute Force

Intuition

For each cell containing a '1', we can try to expand a square outward as far as possible. Starting with a 1x1 square, we incrementally check if we can form a 2x2, 3x3, and so on. For each size, we only need to verify the new rightmost column and bottommost row that would be added. If any cell in those edges is '0', we cannot expand further. This approach checks all potential squares explicitly.

Algorithm

  1. Iterate through each cell (r, c) in the matrix.
  2. If matrix[r][c] is '0', skip it.
  3. Otherwise, try expanding a square of side k starting at 1:
    • Check if r + k and c + k are within bounds.
    • Verify all cells in the new right column and bottom row are '1'.
    • If any cell is '0', stop expanding.
    • Update the result with k * k if the square is valid.
  4. Return the maximum area found.
class Solution:
    def maximalSquare(self, matrix: List[List[str]]) -> int:
        m, n = len(matrix), len(matrix[0])
        res = 0

        for r in range(m):
            for c in range(n):
                if matrix[r][c] == "0":
                    continue
                k = 1
                while True:
                    if r + k > m or c + k > n:
                        break
                    flag = True

                    for i in range(r, r + k):
                        if matrix[i][c + k - 1] == "0":
                            flag = False
                            break
                    for j in range(c, c + k):
                        if matrix[r + k - 1][j] == "0":
                            flag = False
                            break

                    if not flag:
                        break
                    res = max(res, k * k)
                    k += 1

        return res

Time & Space Complexity

  • Time complexity: O((mn)2)O((m * n) ^ 2)
  • Space complexity: O(1)O(1)

Where mm is the number of rows and nn is the number columns.


2. Dynamic Programming (Top-Down)

Intuition

We can define a recursive function where dp(r, c) returns the side length of the largest square whose top-left corner is at (r, c). For a cell with '1', the answer depends on how far we can extend to the right, down, and diagonally. The limiting factor is the minimum of these three directions. We memoize results to avoid redundant computation, then scan all cells to find the maximum value.

Algorithm

  1. Create a memoization table initialized to -1 (unvisited).
  2. Define dfs(r, c):
    • If out of bounds, return 0.
    • If already computed, return the cached value.
    • Recursively compute dfs(r+1, c), dfs(r, c+1), and dfs(r+1, c+1).
    • If matrix[r][c] is '1', set dp[r][c] = 1 + min(down, right, diag).
    • Otherwise, set dp[r][c] = 0.
  3. Call dfs(0, 0) to fill the table.
  4. Find the maximum value in the table and return its square.
class Solution:
    def maximalSquare(self, matrix: List[List[str]]) -> int:
        ROWS, COLS = len(matrix), len(matrix[0])
        cache = {}

        def dfs(r, c):
            if r >= ROWS or c >= COLS:
                return 0
            if (r, c) not in cache:
                down = dfs(r + 1, c)
                right = dfs(r, c + 1)
                diag = dfs(r + 1, c + 1)
                cache[(r, c)] = 0
                if matrix[r][c] == "1":
                    cache[(r, c)] = 1 + min(down, right, diag)
            return cache[(r, c)]

        dfs(0, 0)
        return max(cache.values()) ** 2

Time & Space Complexity

  • Time complexity: O(mn)O(m * n)
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number columns.


3. Dynamic Programming (Bottom-Up)

Intuition

Instead of recursion, we can fill the DP table iteratively from bottom-right to top-left. For each '1' cell, the largest square ending there (as the bottom-right corner) is determined by the minimum of the squares ending at its right, bottom, and diagonal neighbors, plus one. This builds up from smaller subproblems to larger ones and avoids recursion overhead.

Algorithm

  1. Create a DP table of size (m+1) x (n+1) initialized to 0.
  2. Iterate from r = m-1 down to 0, and c = n-1 down to 0:
    • If matrix[r][c] is '1', set dp[r][c] = 1 + min(dp[r+1][c], dp[r][c+1], dp[r+1][c+1]).
    • Update maxSquare with dp[r][c].
  3. Return maxSquare * maxSquare.
class Solution:
    def maximalSquare(self, matrix: List[List[str]]) -> int:
        m, n = len(matrix), len(matrix[0])
        dp = [[0] * (n + 1) for _ in range(m + 1)]
        max_square = 0

        for r in range(m - 1, -1, -1):
            for c in range(n - 1, -1, -1):
                if matrix[r][c] == "1":
                    dp[r][c] = 1 + min(dp[r + 1][c], dp[r][c + 1], dp[r + 1][c + 1])
                    max_square = max(max_square, dp[r][c])

        return max_square * max_square

Time & Space Complexity

  • Time complexity: O(mn)O(m * n)
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number columns.


4. Dynamic Programming (Space Optimized)

Intuition

The bottom-up DP only needs the current row and the next row to compute values. We can reduce space by using a single 1D array. As we process each row from right to left, we keep track of the previous diagonal value in a variable prev. This allows us to update the array in place while still having access to all three neighbors needed for the recurrence.

Algorithm

  1. Create a 1D DP array of size n+1 initialized to 0.
  2. Iterate from r = m-1 down to 0:
    • Set prev = 0 (represents the diagonal value from the previous iteration).
    • For each column c from n-1 down to 0:
      • Store dp[c] in temp (this will be the next diagonal).
      • If matrix[r][c] is '1', set dp[c] = 1 + min(dp[c], dp[c+1], prev) and update maxSquare.
      • Otherwise, set dp[c] = 0.
      • Update prev = temp.
  3. Return maxSquare * maxSquare.
class Solution:
    def maximalSquare(self, matrix: List[List[str]]) -> int:
        m, n = len(matrix), len(matrix[0])
        dp = [0] * (n + 1)
        max_square = 0

        for r in range(m - 1, -1, -1):
            prev = 0
            for c in range(n - 1, -1, -1):
                temp = dp[c]
                if matrix[r][c] == "1":
                    dp[c] = 1 + min(dp[c], dp[c + 1], prev)
                    max_square = max(max_square, dp[c])
                else:
                    dp[c] = 0
                prev = temp

        return max_square * max_square

Time & Space Complexity

  • Time complexity: O(mn)O(m * n)
  • Space complexity: O(n)O(n)

Where mm is the number of rows and nn is the number columns.


Common Pitfalls

Returning the Side Length Instead of the Area

The DP table stores the side length of the largest square, but the problem asks for the area. Forgetting to square the maximum side length before returning gives an incorrect answer.

Comparing Characters Instead of Character Values

Matrix elements are characters ('0' and '1'), not integers. Comparing against integer 0 or 1 instead of character '0' or '1' causes the condition to always evaluate incorrectly, resulting in wrong DP values.

Incorrect DP Recurrence Direction

The recurrence dp[r][c] = 1 + min(dp[r+1][c], dp[r][c+1], dp[r+1][c+1]) assumes processing from bottom-right to top-left. Processing in the wrong direction (top-left to bottom-right without adjusting the formula) references uncomputed values and produces incorrect results.