Facebook Pixel

1292. Maximum Side Length of a Square with Sum Less than or Equal to Threshold

Problem Description

You are given a m x n matrix mat containing integers and an integer value threshold. Your task is to find the maximum side-length of a square submatrix within mat such that the sum of all elements in that square is less than or equal to threshold.

The problem asks you to:

  1. Consider all possible square submatrices within the given matrix
  2. Calculate the sum of elements for each square
  3. Find the largest square whose sum doesn't exceed the threshold
  4. Return the side-length of this largest valid square

If no square exists with a sum less than or equal to threshold (including 1x1 squares), return 0.

For example, if you have a 3x3 matrix and threshold of 8, you would check:

  • All 1x1 squares (individual elements)
  • All 2x2 squares that can fit in the matrix
  • All 3x3 squares (in this case, just the entire matrix)

The solution uses:

  • Prefix sum array (s): A 2D prefix sum array where s[i][j] represents the sum of all elements in the rectangle from (0,0) to (i-1,j-1). This allows calculating any rectangular sum in O(1) time using the formula: sum = s[i+k][j+k] - s[i][j+k] - s[i+k][j] + s[i][j] for a k×k square starting at (i,j).

  • Binary search: Since we're looking for the maximum side-length, binary search is used on the possible side-lengths (from 0 to min(m,n)). The check(k) function verifies if there exists at least one k×k square with sum ≤ threshold.

The algorithm efficiently finds the answer by combining prefix sums for quick sum calculations with binary search to minimize the number of side-lengths to check.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing that this problem has a monotonic property: if a square of side-length k exists with sum ≤ threshold, then all squares with side-length less than k will also have valid squares (we can always find smaller squares within the larger valid square). Conversely, if no square of side-length k exists with sum ≤ threshold, then no larger squares will work either.

This monotonic property immediately suggests binary search on the answer. Instead of checking every possible side-length from 1 to min(m,n), we can use binary search to find the maximum valid side-length in O(log(min(m,n))) iterations.

The next challenge is: how do we efficiently check if a square of given side-length k exists with sum ≤ threshold?

A naive approach would calculate the sum for each k×k square by iterating through all its elements, taking O(k²) time per square. With potentially O(m*n) squares to check, this becomes too slow.

This is where 2D prefix sums come in. By preprocessing the matrix to build a prefix sum array, we can calculate the sum of any rectangular region in O(1) time. The formula for a rectangle from (i,j) to (i+k-1, j+k-1) becomes: sum = s[i+k][j+k] - s[i][j+k] - s[i+k][j] + s[i][j]

This works because:

  • s[i+k][j+k] gives us the sum from origin to the bottom-right corner
  • We subtract s[i][j+k] to remove the region above our square
  • We subtract s[i+k][j] to remove the region to the left of our square
  • We add back s[i][j] because we subtracted it twice in the previous two operations

Combining these two techniques - binary search for the side-length and prefix sums for quick validation - gives us an efficient solution that runs in O(m*n*log(min(m,n))) time.

Learn more about Binary Search and Prefix Sum patterns.

Solution Implementation

1class Solution:
2    def maxSideLength(self, mat: List[List[int]], threshold: int) -> int:
3        """
4        Find the maximum side length of a square with sum <= threshold.
5        Uses binary search template to find first infeasible side, then return answer - 1.
6
7        For "find maximum" problems, we search for first side where NOT feasible.
8        Pattern: true, true, ..., false, false (find first false, answer is index - 1)
9        Reframed: false, false, ..., true, true (find first true where infeasible)
10        """
11        def feasible(side_length: int) -> bool:
12            """Check if there exists a square of given side length with sum <= threshold."""
13            for row in range(rows - side_length + 1):
14                for col in range(cols - side_length + 1):
15                    square_sum = (prefix_sum[row + side_length][col + side_length]
16                                 - prefix_sum[row][col + side_length]
17                                 - prefix_sum[row + side_length][col]
18                                 + prefix_sum[row][col])
19                    if square_sum <= threshold:
20                        return True
21            return False
22
23        rows, cols = len(mat), len(mat[0])
24
25        # Build prefix sum array
26        prefix_sum = [[0] * (cols + 1) for _ in range(rows + 1)]
27        for i, row in enumerate(mat, start=1):
28            for j, value in enumerate(row, start=1):
29                prefix_sum[i][j] = (prefix_sum[i - 1][j] + prefix_sum[i][j - 1]
30                                   - prefix_sum[i - 1][j - 1] + value)
31
32        # Binary search: find first side_length where NOT feasible (infeasible)
33        # Then answer = first_true_index - 1
34        left, right = 1, min(rows, cols) + 1  # +1 to include "no valid square" case
35        first_true_index = min(rows, cols) + 1  # Default: all sizes are feasible
36
37        while left <= right:
38            mid = (left + right) // 2
39            if not feasible(mid):  # Infeasible (this is our "true" condition)
40                first_true_index = mid
41                right = mid - 1  # Look for smaller infeasible
42            else:
43                left = mid + 1
44
45        # Answer is the largest feasible = first_infeasible - 1
46        return first_true_index - 1
47
1class Solution {
2    /**
3     * Find maximum side length using binary search template.
4     * For "find maximum" problems, search for first infeasible size.
5     * Pattern: feasible, feasible, ..., infeasible, infeasible
6     * Reframed: find first true where infeasible, answer = firstTrueIndex - 1
7     */
8    private int rows;
9    private int cols;
10    private int threshold;
11    private int[][] prefixSum;
12
13    public int maxSideLength(int[][] mat, int threshold) {
14        rows = mat.length;
15        cols = mat[0].length;
16        this.threshold = threshold;
17
18        // Build prefix sum array
19        prefixSum = new int[rows + 1][cols + 1];
20        for (int i = 1; i <= rows; i++) {
21            for (int j = 1; j <= cols; j++) {
22                prefixSum[i][j] = prefixSum[i - 1][j] + prefixSum[i][j - 1]
23                                - prefixSum[i - 1][j - 1] + mat[i - 1][j - 1];
24            }
25        }
26
27        // Binary search: find first infeasible size
28        int left = 1;
29        int right = Math.min(rows, cols) + 1;
30        int firstTrueIndex = Math.min(rows, cols) + 1;  // Default: all feasible
31
32        while (left <= right) {
33            int mid = left + (right - left) / 2;
34
35            if (!feasible(mid)) {  // Infeasible is our "true" condition
36                firstTrueIndex = mid;
37                right = mid - 1;
38            } else {
39                left = mid + 1;
40            }
41        }
42
43        // Answer is largest feasible = first_infeasible - 1
44        return firstTrueIndex - 1;
45    }
46
47    private boolean feasible(int k) {
48        for (int i = 0; i <= rows - k; i++) {
49            for (int j = 0; j <= cols - k; j++) {
50                int sum = prefixSum[i + k][j + k] - prefixSum[i][j + k]
51                        - prefixSum[i + k][j] + prefixSum[i][j];
52                if (sum <= threshold) {
53                    return true;
54                }
55            }
56        }
57        return false;
58    }
59}
60
1class Solution {
2public:
3    /**
4     * Find maximum side length using binary search template.
5     * For "find maximum" problems, search for first infeasible size.
6     * Pattern: feasible, feasible, ..., infeasible, infeasible
7     * Reframed: find first true where infeasible, answer = firstTrueIndex - 1
8     */
9    int maxSideLength(vector<vector<int>>& mat, int threshold) {
10        int rows = mat.size();
11        int cols = mat[0].size();
12
13        // Build prefix sum array
14        vector<vector<int>> prefixSum(rows + 1, vector<int>(cols + 1, 0));
15        for (int i = 1; i <= rows; ++i) {
16            for (int j = 1; j <= cols; ++j) {
17                prefixSum[i][j] = prefixSum[i - 1][j] + prefixSum[i][j - 1]
18                                 - prefixSum[i - 1][j - 1] + mat[i - 1][j - 1];
19            }
20        }
21
22        // Lambda to check if side length k is feasible
23        auto feasible = [&](int k) -> bool {
24            for (int i = 0; i <= rows - k; ++i) {
25                for (int j = 0; j <= cols - k; ++j) {
26                    int sum = prefixSum[i + k][j + k] - prefixSum[i][j + k]
27                            - prefixSum[i + k][j] + prefixSum[i][j];
28                    if (sum <= threshold) {
29                        return true;
30                    }
31                }
32            }
33            return false;
34        };
35
36        // Binary search: find first infeasible size
37        int left = 1;
38        int right = min(rows, cols) + 1;
39        int firstTrueIndex = min(rows, cols) + 1;  // Default: all feasible
40
41        while (left <= right) {
42            int mid = left + (right - left) / 2;
43
44            if (!feasible(mid)) {  // Infeasible is our "true" condition
45                firstTrueIndex = mid;
46                right = mid - 1;
47            } else {
48                left = mid + 1;
49            }
50        }
51
52        // Answer is largest feasible = first_infeasible - 1
53        return firstTrueIndex - 1;
54    }
55};
56
1/**
2 * Find maximum side length using binary search template.
3 * For "find maximum" problems, search for first infeasible size.
4 * Pattern: feasible, feasible, ..., infeasible, infeasible
5 * Reframed: find first true where infeasible, answer = firstTrueIndex - 1
6 *
7 * @param mat - The input matrix
8 * @param threshold - The maximum allowed sum for a square
9 * @returns The maximum side length of a valid square
10 */
11function maxSideLength(mat: number[][], threshold: number): number {
12    const rows = mat.length;
13    const cols = mat[0].length;
14
15    // Build prefix sum array
16    const prefixSum: number[][] = Array(rows + 1)
17        .fill(0)
18        .map(() => Array(cols + 1).fill(0));
19
20    for (let row = 1; row <= rows; row++) {
21        for (let col = 1; col <= cols; col++) {
22            prefixSum[row][col] = prefixSum[row - 1][col] + prefixSum[row][col - 1]
23                                 - prefixSum[row - 1][col - 1] + mat[row - 1][col - 1];
24        }
25    }
26
27    // Check if side length k is feasible
28    const feasible = (k: number): boolean => {
29        for (let i = 0; i <= rows - k; i++) {
30            for (let j = 0; j <= cols - k; j++) {
31                const sum = prefixSum[i + k][j + k] - prefixSum[i][j + k]
32                          - prefixSum[i + k][j] + prefixSum[i][j];
33                if (sum <= threshold) {
34                    return true;
35                }
36            }
37        }
38        return false;
39    };
40
41    // Binary search: find first infeasible size
42    let left = 1;
43    let right = Math.min(rows, cols) + 1;
44    let firstTrueIndex = Math.min(rows, cols) + 1;  // Default: all feasible
45
46    while (left <= right) {
47        const mid = Math.floor((left + right) / 2);
48
49        if (!feasible(mid)) {  // Infeasible is our "true" condition
50            firstTrueIndex = mid;
51            right = mid - 1;
52        } else {
53            left = mid + 1;
54        }
55    }
56
57    // Answer is largest feasible = first_infeasible - 1
58    return firstTrueIndex - 1;
59}
60

Solution Approach

The implementation consists of three main parts: building the prefix sum array, implementing the validation function, and performing binary search.

Step 1: Building the 2D Prefix Sum Array

We create a 2D array s with dimensions (m+1) × (n+1), padded with zeros to handle boundary cases elegantly:

s = [[0] * (n + 1) for _ in range(m + 1)]
for i, row in enumerate(mat, 1):
    for j, x in enumerate(row, 1):
        s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + x

The formula s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + x builds the prefix sum incrementally:

  • s[i-1][j]: sum of rectangle from origin to row i-1, column j
  • s[i][j-1]: sum of rectangle from origin to row i, column j-1
  • -s[i-1][j-1]: subtract the overlap (counted twice)
  • +x: add the current element mat[i-1][j-1]

Step 2: Validation Function check(k)

This function determines if there exists at least one k×k square with sum ≤ threshold:

def check(k: int) -> bool:
    for i in range(m - k + 1):
        for j in range(n - k + 1):
            v = s[i + k][j + k] - s[i][j + k] - s[i + k][j] + s[i][j]
            if v <= threshold:
                return True
    return False

For each possible k×k square starting at position (i,j):

  • We calculate its sum using the prefix sum array in O(1) time
  • The range m - k + 1 ensures the square fits within matrix boundaries
  • We return True immediately upon finding any valid square

Step 3: Binary Search for Maximum Side-Length

l, r = 0, min(m, n)
while l < r:
    mid = (l + r + 1) >> 1
    if check(mid):
        l = mid
    else:
        r = mid - 1
return l

The binary search works as follows:

  • Initialize search range: l = 0 (minimum possible), r = min(m, n) (maximum possible square that fits)
  • Use mid = (l + r + 1) >> 1 to avoid infinite loops when searching for the maximum value
  • If check(mid) returns True, we know squares of size mid are valid, so search larger sizes: l = mid
  • Otherwise, the current size is too large, search smaller sizes: r = mid - 1
  • The loop terminates when l == r, giving us the maximum valid side-length

Time Complexity: O(m*n*log(min(m,n)))

  • Building prefix sum: O(m*n)
  • Binary search iterations: O(log(min(m,n)))
  • Each check() call: O(m*n) to verify all possible positions

Space Complexity: O(m*n) for the prefix sum array

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to illustrate the solution approach.

Given matrix:

mat = [[1, 1, 3],
       [2, 2, 3],
       [1, 1, 3]]
threshold = 4

Step 1: Build the 2D Prefix Sum Array

We create a prefix sum array s with dimensions 4×4 (padded with zeros):

s = [[0, 0, 0, 0],
     [0, 1, 2, 5],
     [0, 3, 6, 12],
     [0, 4, 8, 17]]

Let's trace how we calculate s[2][2]:

  • s[2][2] = s[1][2] + s[2][1] - s[1][1] + mat[1][1]
  • s[2][2] = 2 + 3 - 1 + 2 = 6

This represents the sum of all elements from (0,0) to (1,1) in the original matrix.

Step 2: Binary Search on Side-Length

Initial range: l = 0, r = min(3, 3) = 3

First iteration: mid = (0 + 3 + 1) >> 1 = 2

  • Check if any 2×2 square has sum ≤ 4:
    • Square at (0,0): sum = s[2][2] - s[0][2] - s[2][0] + s[0][0] = 6 - 0 - 0 + 0 = 6 (> 4, invalid)
    • Square at (0,1): sum = s[2][3] - s[0][3] - s[2][1] + s[0][1] = 12 - 0 - 3 + 0 = 9 (> 4, invalid)
    • Square at (1,0): sum = s[3][2] - s[1][2] - s[3][0] + s[1][0] = 8 - 2 - 0 + 0 = 6 (> 4, invalid)
    • Square at (1,1): sum = s[3][3] - s[1][3] - s[3][1] + s[1][1] = 17 - 5 - 4 + 1 = 9 (> 4, invalid)
  • check(2) returns False, so r = mid - 1 = 1

Second iteration: mid = (0 + 1 + 1) >> 1 = 1

  • Check if any 1×1 square has sum ≤ 4:
    • Square at (0,0): sum = s[1][1] - s[0][1] - s[1][0] + s[0][0] = 1 - 0 - 0 + 0 = 1 (≤ 4, valid!)
  • check(1) returns True, so l = mid = 1

Termination: l = r = 1, return 1

The maximum side-length of a square with sum ≤ 4 is 1.

Verification of the prefix sum formula: Let's verify the 2×2 square at position (0,0) manually:

  • Elements: mat[0][0] + mat[0][1] + mat[1][0] + mat[1][1] = 1 + 1 + 2 + 2 = 6
  • Using prefix sum: s[2][2] - s[0][2] - s[2][0] + s[0][0] = 6 - 0 - 0 + 0 = 6 ✓

The algorithm efficiently found that only 1×1 squares satisfy the threshold constraint, avoiding the need to check all possible combinations exhaustively.

Time and Space Complexity

Time Complexity: O(m * n * min(m, n))

The time complexity is determined by:

  • Building the prefix sum array takes O(m * n) time
  • Binary search runs for O(log(min(m, n))) iterations
  • For each binary search iteration, the check function is called
  • Inside check, there are two nested loops iterating (m - k + 1) * (n - k + 1) times, which in the worst case (when k is small) is approximately O(m * n)
  • Each iteration inside check performs constant time operations using the prefix sum array

Therefore, the overall time complexity is O(m * n * log(min(m, n))). However, since the binary search explores different values of k, and for larger values of k the nested loops in check run fewer times, a tighter bound considering all iterations together gives us O(m * n * min(m, n)).

Space Complexity: O(m * n)

The space complexity is determined by:

  • The prefix sum array s which has dimensions (m + 1) × (n + 1), requiring O(m * n) space
  • Other variables like l, r, mid, and loop counters use O(1) space

Therefore, the overall space complexity is O(m * n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Using Wrong Binary Search Template Variant

This is a "find maximum" problem, not "find minimum". The template must be adapted.

Wrong approach (causes infinite loop):

while left < right:
    mid = (left + right) // 2
    if feasible(mid):
        left = mid  # Infinite loop when left = right - 1
    else:
        right = mid - 1

Correct approach using template:

# Reframe: find first infeasible size, answer = first_infeasible - 1
first_true_index = max_possible + 1
while left <= right:
    mid = (left + right) // 2
    if not feasible(mid):  # Infeasible is our "true"
        first_true_index = mid
        right = mid - 1
    else:
        left = mid + 1
return first_true_index - 1

2. Off-by-One Errors in Prefix Sum Indexing

Pitfall: Confusing the coordinate system between the original matrix (0-indexed) and the prefix sum array (1-indexed with padding).

Solution: Maintain clear understanding that prefix_sum[i][j] represents the sum of all elements from mat[0][0] to mat[i-1][j-1]. When checking a k×k square starting at mat[i][j], use:

square_sum = (prefix_sum[i + k][j + k]
             - prefix_sum[i][j + k]
             - prefix_sum[i + k][j]
             + prefix_sum[i][j])

3. Incorrect Range Calculation for Square Positions

Pitfall: Using range(m - k) instead of range(m - k + 1) when iterating through possible square positions.

Solution: Always use range(m - k + 1) and range(n - k + 1) to include all valid starting positions.

4. Not Handling Edge Cases

Pitfall: Failing to handle matrices where even a 1×1 square exceeds the threshold.

Solution: By searching for first infeasible and returning first_true_index - 1, we naturally handle this case. If all sizes are infeasible (first_true_index = 1), we return 0.

Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the minimum element can be found in:


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More