14 min read

Row with Maximum 1’s in C++

Row with Maximum 1’s in C++ is a foundational 2D matrix algorithmic problem frequently featured in technical interviews at top-tier companies such as Amazon, Microsoft, Adobe, Flipkart, and Goldman Sachs. You are given a 2D boolean matrix of dimensions M × N where every individual row is sorted in non-decreasing order (meaning all 0s appear strictly before all 1s). The objective is to identify and return the 0-based index of the row that contains the highest count of 1s. If multiple rows share the same maximum number of 1s, return the smallest (first) row index; if no row contains any 1s, return -1. In this comprehensive masterclass, we will trace the complete algorithmic journey: starting from an O(M × N) Nested Brute Force Scan, accelerating each row using O(M · log N) Row-wise Binary Search, and mastering the Optimal O(M + N) Top-Right Staircase Saddleback Walk. We will also examine row-major memory layouts, cache locality, SIMD vectorization, run a 25,000,000-cell benchmark lab, and provide hands-on “Try it yourself” challenges after every section.

🔹 Problem Geometry & The Monotonic First-One Invariant

Consider a 4 × 4 binary matrix where each row is independently sorted in non-decreasing order:

Matrix Coordinates (M = 4, N = 4):
           Col 0   Col 1   Col 2   Col 3    First '1' Col    Count of 1s
Row 0:   [   0,      1,      1,      1   ]   → Col 1           3
Row 1:   [   0,      0,      1,      1   ]   → Col 2           2
Row 2:   [   1,      1,      1,      1   ]   → Col 0           4  ← MAXIMUM 1s (Row 2)
Row 3:   [   0,      0,      0,      0   ]   → None (N/A)      0

From this structure, we observe two foundational mathematical invariants:

  • The First-One Invariant: Because every row is sorted, all 0s precede all 1s. If the first 1 in row r occurs at column index c, then every subsequent cell mat[r][k] (for c ≤ k < N) is guaranteed to be 1. Therefore, the number of 1s in row r is exactly:
    ones_in_row(r) = N - first_one_col(r)
  • The Leftmost-One Equivalence: Maximizing the count of 1s across the entire matrix is mathematically equivalent to finding the row with the minimum (leftmost) column index of its first 1.
  • Tie Breaking Order: If two rows have their first 1 at the same column index, the row with the smaller index (encountered earlier) takes precedence.

Try it yourself

  • If a row of length N = 10 has its first 1 at column 4, how many 1s are in that row?
  • What should the algorithm return if all entries in the matrix are 0?

🔹 Approach 1: Brute Force (Nested Linear Scan: O(M × N))

Intuition & Reasoning: Iterate through every row r from 0 to M – 1. For each row, perform a linear scan from left to right. As soon as the first 1 is found at column c, calculate count = N - c and break out of the inner loop. Maintain variables to track the maximum 1s found so far (max_ones) and the best row index (best_row).

#include <iostream>
#include <vector>
#include <cassert>

using namespace std;

// Approach 1: Brute Force Linear Scan
// Time Complexity: O(M * N), Space Complexity: O(1)
int row_with_max_1s_brute(const vector<vector<int>>& mat) {
    if (mat.empty() || mat[0].empty()) return -1;

    const int m = static_cast<int>(mat.size());
    const int n = static_cast<int>(mat[0].size());

    int max_ones = 0;
    int best_row = -1;

    for (int r = 0; r < m; ++r) {
        int count_ones = 0;
        for (int c = 0; c < n; ++c) {
            if (mat[r][c] == 1) {
                count_ones = n - c; // Remaining elements in this sorted row are all 1s
                break;
            }
        }

        if (count_ones > max_ones) {
            max_ones = count_ones;
            best_row = r;
        }
    }

    return best_row;
}

int main() {
    vector<vector<int>> mat1 = {
        {0, 1, 1, 1},
        {0, 0, 1, 1},
        {1, 1, 1, 1},
        {0, 0, 0, 0}
    };
    vector<vector<int>> mat2 = {
        {0, 0},
        {0, 0}
    };

    cout << "--- Approach 1: Brute Force Linear Scan ---\n";
    cout << "Matrix 1 Max 1s Row: " << row_with_max_1s_brute(mat1) << "\n";
    cout << "Matrix 2 Max 1s Row: " << row_with_max_1s_brute(mat2) << "\n";

    assert(row_with_max_1s_brute(mat1) == 2);
    assert(row_with_max_1s_brute(mat2) == -1);
    cout << "All assertions passed successfully!\n";
    return 0;
}

Output

--- Approach 1: Brute Force Linear Scan ---
Matrix 1 Max 1s Row: 2
Matrix 2 Max 1s Row: -1
All assertions passed successfully!

Step by Step State Trace

Tracing row_with_max_1s_brute on mat1 (4 × 4):

Initial: max_ones = 0, best_row = -1
Row 0: c=0 (val 0), c=1 (val 1 → count = 4 - 1 = 3). 3 > 0 → max_ones = 3, best_row = 0
Row 1: c=0 (val 0), c=1 (val 0), c=2 (val 1 → count = 4 - 2 = 2). 2 > 3 is FALSE.
Row 2: c=0 (val 1 → count = 4 - 0 = 4). 4 > 3 → max_ones = 4, best_row = 2
Row 3: c=0..3 (all 0s → count = 0). 0 > 4 is FALSE.
Final Result: best_row = 2 (with 4 ones).

Complexity Analysis

  • Time Complexity (TC): O(M × N): In the worst case (e.g. all 0s), the algorithm visits every one of the M × N matrix cells.
  • Space Complexity (SC): O(1): Only a few integer counters are stored on the stack.
  • Algorithmic Inefficiency: Scanning linearly inside each sorted row ignores the sorted binary property. We can replace the inner linear scan with binary search.

Try it yourself

  • Why does the inner loop terminate immediately with break after finding the first 1?
  • How would you modify the code to return the total number of 1s in the matrix?

🔹 Approach 2: Row-wise Binary Search via std::lower_bound (O(M · log N))

Intuition & Reasoning: Since every row is sorted, finding the first occurrence of 1 in row r is equivalent to computing std::lower_bound(row.begin(), row.end(), 1). Binary search finds the first 1 in O(log N) time per row, leading to an overall runtime of O(M · log N).

#include <iostream>
#include <vector>
#include <algorithm>
#include <cassert>

using namespace std;

// Helper: Custom Binary Search to find the index of the first '1' in a sorted row
int first_one_binary_search(const vector<int>& row) {
    int low = 0;
    int high = static_cast<int>(row.size()) - 1;
    int first_idx = static_cast<int>(row.size()); // Default: no 1s

    while (low <= high) {
        int mid = low + (high - low) / 2;
        if (row[mid] == 1) {
            first_idx = mid;   // Candidate found, search left for earlier occurrence
            high = mid - 1;
        } else {
            low = mid + 1;    // 0 found, 1 must be to the right
        }
    }
    return first_idx;
}

// Approach 2: Binary Search per Row
// Time Complexity: O(M * log N), Space Complexity: O(1)
int row_with_max_1s_binary_search(const vector<vector<int>>& mat) {
    if (mat.empty() || mat[0].empty()) return -1;

    const int m = static_cast<int>(mat.size());
    const int n = static_cast<int>(mat[0].size());

    int max_ones = 0;
    int best_row = -1;

    for (int r = 0; r < m; ++r) {
        // Alternatively: int first_idx = distance(mat[r].begin(), lower_bound(mat[r].begin(), mat[r].end(), 1));
        int first_idx = first_one_binary_search(mat[r]);
        int count_ones = n - first_idx;

        if (count_ones > max_ones) {
            max_ones = count_ones;
            best_row = r;
        }
    }

    return best_row;
}

int main() {
    vector<vector<int>> mat1 = {
        {0, 1, 1, 1},
        {0, 0, 1, 1},
        {1, 1, 1, 1},
        {0, 0, 0, 0}
    };

    cout << "--- Approach 2: Row-wise Binary Search ---\n";
    cout << "Matrix 1 Max 1s Row: " << row_with_max_1s_binary_search(mat1) << "\n";
    assert(row_with_max_1s_binary_search(mat1) == 2);
    cout << "All assertions passed successfully!\n";
    return 0;
}

Output

--- Approach 2: Row-wise Binary Search ---
Matrix 1 Max 1s Row: 2
All assertions passed successfully!

Complexity Analysis

  • Time Complexity (TC): O(M · log N): For each of the M rows, binary search runs in O(log N) steps.
  • Space Complexity (SC): O(1): Operates entirely in place using scalar loop registers.
  • Remaining Inefficiency: Binary searching each row independently treats each row in isolation. If row 0 already has 1s starting at column 2, any subsequent row only matters if it has a 1 at column 1 or 0. We can carry information across rows!

Try it yourself

  • How would you implement this with std::lower_bound in modern C++?
  • If M = 1,000 and N = 1,000,000, how many total comparisons does this approach perform? (Answer: ~20,000 comparisons).

🔹 Approach 3: Optimal Top-Right Staircase Saddleback Walk (O(M + N))

Intuition & Invariant: We can achieve a linear runtime O(M + N) using the Staircase (Saddleback) Walk. We start our traversal at the top-right corner of the matrix: (row = 0, col = N - 1).

Start at Top-Right (row = 0, col = N - 1):
- If mat[row][col] == 1:
    We found a '1'! This row has at least (N - col) ones.
    Record this row as our candidate (best_row = row).
    Move LEFT (col--) to check if this row has even more 1s.
- If mat[row][col] == 0:
    We hit a '0'! Since the row is sorted, all elements to the left in this row are also 0s.
    Therefore, this row cannot beat our current best count!
    Move DOWN (row++) to check the next row at the current best column.

Because row only increases from 0 to M and col only decreases from N – 1 to -1, the total number of steps is strictly bounded by M + N!

#include <iostream>
#include <vector>
#include <cassert>

using namespace std;

// Approach 3: Optimal Top-Right Staircase Saddleback Walk
// Time Complexity: O(M + N), Space Complexity: O(1)
int row_with_max_1s_optimal(const vector<vector<int>>& mat) {
    if (mat.empty() || mat[0].empty()) return -1;

    const int m = static_cast<int>(mat.size());
    const int n = static_cast<int>(mat[0].size());

    int best_row = -1;
    int r = 0;
    int c = n - 1; // Start at the top-right corner

    while (r < m && c >= 0) {
        if (mat[r][c] == 1) {
            best_row = r; // Update candidate row
            --c;          // Try to find more 1s to the left
        } else {
            ++r;          // 0 encountered; move down to the next row
        }
    }

    return best_row;
}

int main() {
    vector<vector<int>> mat1 = {
        {0, 1, 1, 1},
        {0, 0, 1, 1},
        {1, 1, 1, 1},
        {0, 0, 0, 0}
    };
    vector<vector<int>> mat2 = {
        {0, 0, 0},
        {0, 1, 1},
        {0, 0, 1}
    };

    cout << "--- Approach 3: Optimal Top-Right Staircase Walk ---\n";
    cout << "Matrix 1 Max 1s Row: " << row_with_max_1s_optimal(mat1) << "\n";
    cout << "Matrix 2 Max 1s Row: " << row_with_max_1s_optimal(mat2) << "\n";

    assert(row_with_max_1s_optimal(mat1) == 2);
    assert(row_with_max_1s_optimal(mat2) == 1);
    cout << "All assertions passed successfully!\n";
    return 0;
}

Output

--- Approach 3: Optimal Top-Right Staircase Walk ---
Matrix 1 Max 1s Row: 2
Matrix 2 Max 1s Row: 1
All assertions passed successfully!

Step by Step Coordinate Path Trace

Tracing row_with_max_1s_optimal on mat1 (4 × 4):

Start at (r=0, c=3):
- Step 1: mat[0][3] == 1 → best_row = 0, move LEFT → c = 2
- Step 2: mat[0][2] == 1 → best_row = 0, move LEFT → c = 1
- Step 3: mat[0][1] == 1 → best_row = 0, move LEFT → c = 0
- Step 4: mat[0][0] == 0 → move DOWN → r = 1
- Step 5: mat[1][0] == 0 → move DOWN → r = 2
- Step 6: mat[2][0] == 1 → best_row = 2, move LEFT → c = -1 (Out of bounds!)
Loop terminates (c < 0). Total steps = 6 <= (4 + 4 = 8).
Result: best_row = 2!

Complexity Analysis & Invariant Proof

  • Time Complexity (TC): O(M + N): In each step, we either increment r or decrement c. The maximum number of increments is M, and the maximum number of decrements is N. Total steps ≤ M + N.
  • Space Complexity (SC): O(1): Only uses two integer index pointers (r and c).
  • Optimality Proof: Any cell to the right of column c has already been verified to contain 1s in a previously examined row. Any cell to the left of column c in rows with a 0 at column c is guaranteed to be 0 by row monotonicity. Thus, no potential maximum row is ever missed.

Try it yourself

  • What happens if the entire first column contains 1s? How many steps does the algorithm take?
  • Can we start from the bottom-left corner instead? How would the traversal directions change?

🔹 Advanced Systems Topic: 2D Matrix Memory Layout & Cache Locality

In high performance systems engineering, standard nested vectors (vector<vector<int>>) introduce pointer chasing and fragmented heap allocations, which degrades L1 cache locality. Flattening the 2D matrix into a contiguous 1D buffer (vector<int> mat(M * N) with index formula r * N + c) enables hardware prefetching and SIMD vectorization.

🔹 Lab: High Performance Benchmark Across 25,000,000 Cells (5,000 × 5,000 Matrix)

The following benchmark executable measures execution time on a 5,000 × 5,000 matrix (25,000,000 integers) using std::chrono::high_resolution_clock:

#include <iostream>
#include <vector>
#include <chrono>
#include <algorithm>

using namespace std;
using namespace std::chrono;

int bench_brute(const vector<vector<int>>& mat) {
    int m = mat.size(), n = mat[0].size(), max_ones = 0, best_row = -1;
    for (int r = 0; r < m; ++r) {
        for (int c = 0; c < n; ++c) {
            if (mat[r][c] == 1) {
                if (n - c > max_ones) { max_ones = n - c; best_row = r; }
                break;
            }
        }
    }
    return best_row;
}

int bench_bs(const vector<vector<int>>& mat) {
    int m = mat.size(), n = mat[0].size(), max_ones = 0, best_row = -1;
    for (int r = 0; r < m; ++r) {
        auto it = lower_bound(mat[r].begin(), mat[r].end(), 1);
        int count = mat[r].end() - it;
        if (count > max_ones) { max_ones = count; best_row = r; }
    }
    return best_row;
}

int bench_staircase(const vector<vector<int>>& mat) {
    int m = mat.size(), n = mat[0].size(), best_row = -1, r = 0, c = n - 1;
    while (r < m && c >= 0) {
        if (mat[r][c] == 1) { best_row = r; --c; }
        else ++r;
    }
    return best_row;
}

int main() {
    const int M = 5000;
    const int N = 5000;
    cout << "Allocating " << M << "x" << N << " (25,000,000 cells) matrix...\n";

    vector<vector<int>> mat(M, vector<int>(N, 0));
    for (int r = 0; r < M; ++r) {
        int first_one = (r == 3750) ? 100 : (2000 + (r % 2500));
        for (int c = first_one; c < N; ++c) mat[r][c] = 1;
    }

    auto t0 = high_resolution_clock::now();
    int r1 = bench_brute(mat);
    auto t1 = high_resolution_clock::now();
    cout << "1. Brute Force O(M*N):       " << duration_cast<milliseconds>(t1 - t0).count() << " ms (Row: " << r1 << ")\n";

    auto t2 = high_resolution_clock::now();
    int r2 = bench_bs(mat);
    auto t3 = high_resolution_clock::now();
    cout << "2. Row Binary Search O(M*logN): " << duration_cast<microseconds>(t3 - t2).count() << " µs (Row: " << r2 << ")\n";

    auto t4 = high_resolution_clock::now();
    int r3 = bench_staircase(mat);
    auto t5 = high_resolution_clock::now();
    cout << "3. Optimal Staircase O(M+N):    " << duration_cast<microseconds>(t5 - t4).count() << " µs (Row: " << r3 << ")\n";

    return 0;
}

Benchmark Results

Allocating 5000x5000 (25,000,000 cells) matrix...
1. Brute Force O(M*N):          18 ms (Row: 3750)
2. Row Binary Search O(M*logN):  850 µs (Row: 3750)
3. Optimal Staircase O(M+N):     12 µs (Row: 3750)  ← ~70x FASTER than Binary Search!

🔹 Comparison Matrix of All Approaches

┌───────────────────────────────────────┬──────────────────┬──────────────────┬─────────────────────────────┬───────────────────────────┐
│ Approach                              │ Time Complexity  │ Space Complexity │ Steps on 5000x5000 Matrix   │ Production Suitability    │
├───────────────────────────────────────┼──────────────────┼──────────────────┼─────────────────────────────┼───────────────────────────┤
│ 1. Nested Linear Scan                 │ O(M * N)         │ O(1)             │ Up to 25,000,000 cells      │ Small matrices only       │
│ 2. Row Binary Search (lower_bound)    │ O(M * log N)     │ O(1)             │ ~65,000 comparisons         │ Good, easy to write       │
│ 3. Optimal Top-Right Staircase Walk   │ O(M + N)         │ O(1)             │ <= 10,000 steps (12 µs)      │ Industry gold standard    │
└───────────────────────────────────────┴──────────────────┴──────────────────┴─────────────────────────────┴───────────────────────────┘

🔹 Frequently Asked Questions (FAQs)

Q1: Why does the Staircase Walk run in O(M + N) time instead of O(M × N)?
In each step of the staircase while loop, either the row index r is incremented by 1 (moving down) or the column index c is decremented by 1 (moving left). Because r can only increase at most M times and c can only decrease at most N times, the total number of cell evaluations across the entire algorithm is strictly bounded by M + N.

Q2: Why does the staircase search start at the top-right corner?
The top-right corner allows unambiguous decisions: if the cell is 1, we move left to seek more 1s in the current row; if it is 0, all cells to the left are also 0 (due to row sorting), so this row cannot exceed the current best count, allowing us to safely move down to the next row.

Q3: Can we start at the bottom-left corner instead?
Yes! Starting at (r = M - 1, c = 0): if mat[r][c] == 0, move right (c++); if mat[r][c] == 1, record the row and move up (r--). This maintains the exact same O(M + N) complexity.

Q4: How do we handle ties when multiple rows have the same maximum count of 1s?
By updating best_row = r only when we discover a 1 at a strictly smaller column index (moving left), rows with identical counts of 1s encountered later will fail the condition and will not overwrite the earlier row index, naturally preserving the tie-break rule.

Q5: What should the function return if the matrix contains no 1s?
If no 1s exist anywhere, the condition mat[r][c] == 1 is never triggered. The variable best_row remains at its initial value -1, correctly indicating that no 1s were present.

Q6: Is Binary Search ever faster than the Staircase Walk?
When M is very large and N is tiny (or vice versa), the difference is negligible. However, if the matrix is heavily sparse (mostly 0s), the staircase walk immediately drops down the right column in M steps, taking O(M) time, which outperforms binary search’s O(M log N).

Q7: How does contiguous memory allocation affect performance in C++?
Standard vector<vector<int>> incurs M separate heap allocations, creating pointer indirection and cache thrashing. A flattened 1D vector<int> mat(M * N) accessed as mat[r * N + c] provides contiguous cache lines, speeding up memory traversal significantly.

Q8: Can SIMD vectorization be used for this problem?
Yes. With AVX-512 or NEON intrinsics, 8 to 16 boolean integers can be compared in parallel using _mm256_cmpeq_epi32 and _mm256_movemask_epi8, accelerating row searches by up to 8x on modern x86-64 CPUs.

🔹 Wrapping Up

Solving Row with Maximum 1’s in C++ demonstrates the power of stateful traversal invariants. By carrying the best column index across rows in the Top-Right Staircase Walk, we reduce the complexity from a naive O(M × N) down to an optimal O(M + N) time and O(1) space. Continue expanding your 2D matrix algorithmic mastery with our guides on Spiral Matrix, Matrix Median, and Search a 2D Matrix.

Leave a Comment