Finding the largest zero submatrix

You are given a matrix with n rows and m columns. Find the largest submatrix consisting of only zeros (a submatrix is a rectangular area of the matrix).

Algorithm

Elements of the matrix will be a[i][j], where i = 0...n - 1, j = 0... m - 1. For simplicity, we will consider all non-zero elements equal to 1.

Step 1: Auxiliary dynamic

First, we calculate the following auxiliary matrix: d[i][j], nearest row that has a 1 above a[i][j]. Formally speaking, d[i][j] is the largest row number (from 0 to i - 1), in which there is a element equal to 1 in the j-th column. While iterating from top-left to bottom-right, when we stand in row i, we know the values from the previous row, so, it is enough to update just the elements with value 1. We can save the values in a simple array d[i], i = 1...m - 1, because in the further algorithm we will process the matrix one row at a time and only need the values of the current row.

vector<int> d(m, -1);
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (a[i][j] == 1) {
d[j] = i;
}
}
}


Step 2: Problem solving

We can solve the problem in $O(n m^2)$ iterating through rows, considering every possible left and right columns for a submatrix. The bottom of the rectangle will be the current row, and using d[i][j] we can find the top row. However, it is possible to go further and significantly improve the complexity of the solution.

It is clear that the desired zero submatrix is bounded on all four sides by some ones, which prevent it from increasing in size and improving the answer. Therefore, we will not miss the answer if we act as follows: for every cell j in row i (the bottom row of a potential zero submatrix) we will have d[i][j] as the top row of the current zero submatrix. It now remains to determine the optimal left and right boundaries of the zero submatrix, i.e. maximally push this submatrix to the left and right of the j-th column.

What does it mean to push the maximum to the left? It means to find an index k1 for which d[i][k1] > d[i][j], and at the same time k1 - the closest one to the left of the index j. It is clear that then k1 + 1 gives the number of the left column of the required zero submatrix. If there is no such index at all, then put k1 = -1(this means that we were able to extend the current zero submatrix to the left all the way to the border of matrix a).

Symmetrically, you can define an index k2 for the right border: this is the closest index to the right of j such that d[i][k2] > d[i][j] (or m, if there is no such index).

So, the indices k1 and k2, if we learn to search for them effectively, will give us all the necessary information about the current zero submatrix. In particular, its area will be equal to (i - d[i][j]) * (k2 - k1 - 1).

How to look for these indexes k1 and k2 effectively with fixed i and j? We can do that in $O(1)$ on average.

To achieve such complexity, you can use the stack as follows. Let's first learn how to search for an index k1, and save its value for each index j within the current row i in matrix d1[i][j]. To do this, we will look through all the columns j from left to right, and we will store in the stack only those columns that have d[][] strictly greater than d[i][j]. It is clear that when moving from a column j to the next column, it is necessary to update the content of the stack. When there is an inappropriate element at the top of the stack (i.e. d[][] <= d[i][j]) pop it. It is easy to understand that it is enough to remove from the stack only from its top, and from none of its other places (because the stack will contain an increasing d sequence of columns).

The value d1[i][j] for each j will be equal to the value lying at that moment on top of the stack.

The dynamics d2[i][j] for finding the indices k2 is considered similar, only you need to view the columns from right to left.

It is clear that since there are exactly m pieces added to the stack on each line, there could not be more deletions either, the sum of complexities will be linear, so the final complexity of the algorithm is $O(nm)$.

It should also be noted that this algorithm consumes $O(m)$ memory (not counting the input data - the matrix a[][]).

Implementation

int zero_matrix(vector<vector<int>> a) {
int n = a.size();
int m = a[0].size();

int ans = 0;
vector<int> d(m, -1), d1(m), d2(m);
stack<int> st;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (a[i][j] == 1)
d[j] = i;
}

for (int j = 0; j < m; ++j) {
while (!st.empty() && d[st.top()] <= d[j])
st.pop();
d1[j] = st.empty() ? -1 : st.top();
st.push(j);
}
while (!st.empty())
st.pop();

for (int j = m - 1; j >= 0; --j) {
while (!st.empty() && d[st.top()] <= d[j])
st.pop();
d2[j] = st.empty() ? m : st.top();
st.push(j);
}
while (!st.empty())
st.pop();

for (int j = 0; j < m; ++j)
ans = max(ans, (i - d[j]) * (d2[j] - d1[j] - 1));
}
return ans;
}