Facebook Pixel

2503. Maximum Number of Points From Grid Queries

Problem Description

You have an m x n matrix called grid filled with integers, and you need to process k queries stored in an array queries.

For each query value queries[i], you start at the top-left corner of the matrix (position [0,0]) and explore the grid following these rules:

  • From your current cell, you can only move to an adjacent cell (up, down, left, or right) if queries[i] is strictly greater than the value in your current cell
  • When you visit a cell for the first time during this query, you earn 1 point
  • If you revisit a cell, you don't earn additional points
  • The exploration stops when you can't move anymore (when queries[i] is not greater than the current cell value)

Your task is to return an array answer where answer[i] represents the maximum number of points you can earn for queries[i].

Key Points:

  • Each query is processed independently - you always start fresh from [0,0]
  • You can visit the same cell multiple times during one query, but only score points on the first visit
  • Movement is only allowed to adjacent cells (4-directional: up, down, left, right)
  • Movement requires the query value to be strictly greater than the current cell's value

Example: If you have a 3x3 grid and a query value of 5, you start at [0,0]. If grid[0][0] = 2, since 5 > 2, you get 1 point and can move to adjacent cells. You continue exploring, earning points for each new cell you visit where the cell value is less than 5.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: The grid can be viewed as a graph where each cell is a node, and adjacent cells (up, down, left, right) are connected by edges. We need to traverse this graph starting from the top-left corner.

Is it a tree?

  • No: The grid structure is not a tree - it can have cycles when we consider the connections between adjacent cells.

Is the problem related to directed acyclic graphs?

  • No: The grid allows movement in all four directions based on value comparisons, and we can revisit cells, so it's not specifically about DAGs.

Is the problem related to shortest paths?

  • Yes: While not explicitly asking for shortest paths, we need to explore all reachable cells from the starting point. The problem involves finding all cells reachable from [0,0] where the cell value is less than the query value. This is similar to finding all nodes within a certain "distance" (value threshold) from the source.

Is the graph Weighted?

  • No: The edges between adjacent cells don't have weights. The cell values act as constraints for traversal rather than edge weights. We either can move to an adjacent cell (if query > cell value) or we cannot.

BFS

  • Yes: Since we have an unweighted graph exploration problem where we need to find all reachable cells from a starting point based on a condition, BFS is the appropriate choice.

Conclusion: The flowchart suggests using BFS (Breadth-First Search) for this problem. The solution uses BFS with a priority queue (min-heap) to efficiently process cells in order of their values, allowing us to handle multiple queries by processing them in sorted order and expanding our explored region incrementally.

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

Intuition

The key insight is recognizing that for each query, we're essentially asking: "How many cells can I reach from [0,0] if I can only step on cells with values less than my query value?"

If we process queries independently, we'd need to run a BFS for each query, which would be inefficient. However, notice that if we can reach a set of cells for query value x, we can definitely reach those same cells (and possibly more) for any query value y where y > x. This monotonic property suggests we can reuse our work.

Instead of starting fresh for each query, what if we process queries in ascending order? For a smaller query value, we explore and count reachable cells. When we move to a larger query value, we can continue from where we left off, expanding our explored region to include cells with values less than the new query threshold.

Think of it like water flooding a terrain: if water level is at height 3, it covers all land below height 3. When water rises to height 5, it keeps everything it already covered and floods additional land between heights 3 and 5.

To implement this efficiently, we use a min-heap to always know the next smallest unvisited cell value. We start from [0,0] and gradually expand our explored region. For each sorted query:

  1. Pop all cells from the heap whose values are less than the current query value
  2. Mark them as visited and increment our count
  3. Add their unvisited neighbors to the heap
  4. The current count is the answer for this query

By processing queries in sorted order but maintaining their original indices, we can fill in the answer array correctly. This approach transforms multiple BFS runs into a single, incremental exploration of the grid, dramatically improving efficiency from O(k * m * n) to O(m * n * log(m * n) + k * log k) where k is the number of queries.

Learn more about Breadth-First Search, Union Find, Two Pointers, Sorting and Heap (Priority Queue) patterns.

Solution Approach

The implementation follows the offline query processing pattern with BFS using a priority queue (min-heap):

1. Query Preprocessing: First, we sort the queries while preserving their original indices. We create a list of tuples qs = sorted((v, i) for i, v in enumerate(queries)) where each tuple contains (query_value, original_index). This allows us to process queries in ascending order while still being able to place answers in the correct positions.

2. Data Structures Setup:

  • Min-Heap q: Initialized with the starting cell [(grid[0][0], 0, 0)] containing (cell_value, row, col)
  • Visited Matrix vis: A 2D boolean array to track which cells have been visited, initially all False except vis[0][0] = True
  • Counter cnt: Tracks the number of cells visited so far
  • Answer Array ans: Initialized with zeros, same length as queries

3. Processing Queries in Order: For each sorted query (v, k) where v is the query value and k is the original index:

while q and q[0][0] < v:
    _, i, j = heappop(q)
    cnt += 1
    # Explore 4 adjacent cells

This loop continues popping cells from the heap as long as the minimum cell value is less than the current query value v. Each popped cell contributes 1 to our count.

4. Exploring Adjacent Cells: For each popped cell at position (i, j), we check all 4 adjacent cells:

for a, b in pairwise((-1, 0, 1, 0, -1)):
    x, y = i + a, j + b

The pairwise function with (-1, 0, 1, 0, -1) generates the pairs: (-1, 0), (0, 1), (1, 0), (0, -1), representing movements up, right, down, and left respectively.

5. Adding New Cells to Heap: For each valid adjacent cell that hasn't been visited:

  • Check boundaries: 0 <= x < m and 0 <= y < n
  • Check if unvisited: not vis[x][y]
  • If valid, add to heap: heappush(q, (grid[x][y], x, y))
  • Mark as visited: vis[x][y] = True

6. Recording Answers: After processing all cells with values less than the current query v, we record ans[k] = cnt. The count cnt accumulates across queries since larger queries include all cells accessible by smaller queries.

Time Complexity: O(m * n * log(m * n) + k * log k) where:

  • m * n * log(m * n) for heap operations on all grid cells
  • k * log k for sorting queries

Space Complexity: O(m * n) for the visited matrix and heap storage.

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 small example to illustrate the solution approach.

Given:

  • Grid (3x3):
[1, 3, 5]
[2, 6, 4]
[7, 8, 9]
  • Queries: [4, 6, 2]

Step 1: Query Preprocessing Sort queries with original indices:

  • qs = [(2, 2), (4, 0), (6, 1)]
  • This means: process value 2 (originally at index 2), then 4 (index 0), then 6 (index 1)

Step 2: Initial Setup

  • Min-heap: [(1, 0, 0)] (starting cell value=1 at position [0,0])
  • Visited: vis[0][0] = True, all others False
  • Counter: cnt = 0
  • Answer array: ans = [0, 0, 0]

Step 3: Process Query value=2 (original index 2)

  • While heap top (1) < 2:
    • Pop (1, 0, 0), increment cnt = 1
    • Check adjacents:
      • Right [0,1]: value=3, not visited β†’ push (3, 0, 1) to heap, mark visited
      • Down [1,0]: value=2, not visited β†’ push (2, 1, 0) to heap, mark visited
    • Heap now: [(2, 1, 0), (3, 0, 1)]
  • Heap top (2) is NOT < 2, so stop
  • ans[2] = 1 (we visited 1 cell with value < 2)

Step 4: Process Query value=4 (original index 0)

  • While heap top (2) < 4:
    • Pop (2, 1, 0), increment cnt = 2
    • Check adjacents:
      • Right [1,1]: value=6, not visited β†’ push (6, 1, 1) to heap, mark visited
      • Down [2,0]: value=7, not visited β†’ push (7, 2, 0) to heap, mark visited
    • Heap now: [(3, 0, 1), (6, 1, 1), (7, 2, 0)]
  • Continue while heap top (3) < 4:
    • Pop (3, 0, 1), increment cnt = 3
    • Check adjacents:
      • Right [0,2]: value=5, not visited β†’ push (5, 0, 2) to heap, mark visited
      • Down [1,1]: already visited, skip
    • Heap now: [(5, 0, 2), (6, 1, 1), (7, 2, 0)]
  • Heap top (5) is NOT < 4, so stop
  • ans[0] = 3 (we've visited 3 cells total with value < 4)

Step 5: Process Query value=6 (original index 1)

  • While heap top (5) < 6:
    • Pop (5, 0, 2), increment cnt = 4
    • Check adjacents:
      • Down [1,2]: value=4, not visited β†’ push (4, 1, 2) to heap, mark visited
    • Heap now: [(4, 1, 2), (6, 1, 1), (7, 2, 0)]
  • Continue while heap top (4) < 6:
    • Pop (4, 1, 2), increment cnt = 5
    • Check adjacents:
      • Left [1,1]: already visited, skip
      • Down [2,2]: value=9, not visited β†’ push (9, 2, 2) to heap, mark visited
    • Heap now: [(6, 1, 1), (7, 2, 0), (9, 2, 2)]
  • Heap top (6) is NOT < 6, so stop
  • ans[1] = 5 (we've visited 5 cells total with value < 6)

Final Answer: [3, 5, 1]

  • Query 4: can reach 3 cells (values 1, 2, 3)
  • Query 6: can reach 5 cells (values 1, 2, 3, 5, 4)
  • Query 2: can reach 1 cell (value 1)

Solution Implementation

1from typing import List
2from heapq import heappush, heappop
3
4class Solution:
5    def maxPoints(self, grid: List[List[int]], queries: List[int]) -> List[int]:
6        rows, cols = len(grid), len(grid[0])
7      
8        # Sort queries with their original indices to process in ascending order
9        sorted_queries = sorted((value, index) for index, value in enumerate(queries))
10      
11        # Initialize result array to store answers in original query order
12        result = [0] * len(sorted_queries)
13      
14        # Min heap to store cells to explore: (cell_value, row, col)
15        # Start from top-left corner (0, 0)
16        min_heap = [(grid[0][0], 0, 0)]
17      
18        # Counter for reachable cells
19        reachable_count = 0
20      
21        # Track visited cells to avoid revisiting
22        visited = [[False] * cols for _ in range(rows)]
23        visited[0][0] = True
24      
25        # Process each query in ascending order
26        for query_value, original_index in sorted_queries:
27            # Expand reachable area: process all cells with value < query_value
28            while min_heap and min_heap[0][0] < query_value:
29                cell_value, row, col = heappop(min_heap)
30                reachable_count += 1
31              
32                # Check all 4 adjacent cells (up, right, down, left)
33                directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
34                for row_delta, col_delta in directions:
35                    new_row, new_col = row + row_delta, col + col_delta
36                  
37                    # Add valid unvisited neighbors to the heap
38                    if (0 <= new_row < rows and 
39                        0 <= new_col < cols and 
40                        not visited[new_row][new_col]):
41                        heappush(min_heap, (grid[new_row][new_col], new_row, new_col))
42                        visited[new_row][new_col] = True
43          
44            # Store the count for this query at its original position
45            result[original_index] = reachable_count
46      
47        return result
48
1class Solution {
2    public int[] maxPoints(int[][] grid, int[] queries) {
3        int queryCount = queries.length;
4      
5        // Create array to store query value and original index
6        int[][] queryWithIndex = new int[queryCount][2];
7        for (int i = 0; i < queryCount; ++i) {
8            queryWithIndex[i] = new int[] {queries[i], i};
9        }
10      
11        // Sort queries by value in ascending order
12        Arrays.sort(queryWithIndex, (a, b) -> a[0] - b[0]);
13      
14        // Initialize result array
15        int[] result = new int[queryCount];
16      
17        // Grid dimensions
18        int rows = grid.length;
19        int cols = grid[0].length;
20      
21        // Track visited cells
22        boolean[][] visited = new boolean[rows][cols];
23        visited[0][0] = true;
24      
25        // Min heap to process cells by their values
26        // Each element: [cell value, row index, col index]
27        PriorityQueue<int[]> minHeap = new PriorityQueue<>((a, b) -> a[0] - b[0]);
28        minHeap.offer(new int[] {grid[0][0], 0, 0});
29      
30        // Direction vectors for exploring adjacent cells (up, right, down, left)
31        int[] directions = new int[] {-1, 0, 1, 0, -1};
32      
33        // Count of cells that can be reached
34        int reachableCount = 0;
35      
36        // Process each query in sorted order
37        for (int[] queryInfo : queryWithIndex) {
38            int queryValue = queryInfo[0];
39            int originalIndex = queryInfo[1];
40          
41            // Process all cells with value less than current query
42            while (!minHeap.isEmpty() && minHeap.peek()[0] < queryValue) {
43                int[] currentCell = minHeap.poll();
44                reachableCount++;
45              
46                // Explore all 4 adjacent cells
47                for (int dir = 0; dir < 4; ++dir) {
48                    int newRow = currentCell[1] + directions[dir];
49                    int newCol = currentCell[2] + directions[dir + 1];
50                  
51                    // Check if the new position is valid and unvisited
52                    if (newRow >= 0 && newRow < rows && 
53                        newCol >= 0 && newCol < cols && 
54                        !visited[newRow][newCol]) {
55                      
56                        visited[newRow][newCol] = true;
57                        minHeap.offer(new int[] {grid[newRow][newCol], newRow, newCol});
58                    }
59                }
60            }
61          
62            // Store the count at the original query index
63            result[originalIndex] = reachableCount;
64        }
65      
66        return result;
67    }
68}
69
1class Solution {
2public:
3    // Direction vectors for moving up, right, down, left
4    const int directions[5] = {-1, 0, 1, 0, -1};
5
6    vector<int> maxPoints(vector<vector<int>>& grid, vector<int>& queries) {
7        int numQueries = queries.size();
8      
9        // Create pairs of (query value, original index) for sorting
10        vector<pair<int, int>> sortedQueries(numQueries);
11        for (int i = 0; i < numQueries; ++i) {
12            sortedQueries[i] = {queries[i], i};
13        }
14      
15        // Sort queries by value to process them in ascending order
16        sort(sortedQueries.begin(), sortedQueries.end());
17      
18        // Result array to store answers in original query order
19        vector<int> result(numQueries);
20      
21        int rows = grid.size();
22        int cols = grid[0].size();
23      
24        // Visited array to track processed cells
25        bool visited[rows][cols];
26        memset(visited, 0, sizeof visited);
27      
28        // Mark starting position as visited
29        visited[0][0] = true;
30      
31        // Min heap to store cells by their values
32        // Tuple contains: (cell value, row index, column index)
33        priority_queue<tuple<int, int, int>, 
34                      vector<tuple<int, int, int>>, 
35                      greater<tuple<int, int, int>>> minHeap;
36      
37        // Start from top-left corner
38        minHeap.push({grid[0][0], 0, 0});
39      
40        // Counter for cells that can be reached
41        int reachableCount = 0;
42      
43        // Process each query in sorted order
44        for (auto& queryPair : sortedQueries) {
45            int threshold = queryPair.first;
46            int originalIndex = queryPair.second;
47          
48            // Process all cells with values less than current threshold
49            while (!minHeap.empty() && get<0>(minHeap.top()) < threshold) {
50                auto [cellValue, row, col] = minHeap.top();
51                minHeap.pop();
52                ++reachableCount;
53              
54                // Explore all 4 adjacent cells
55                for (int dir = 0; dir < 4; ++dir) {
56                    int newRow = row + directions[dir];
57                    int newCol = col + directions[dir + 1];
58                  
59                    // Check if the new position is valid and unvisited
60                    if (newRow >= 0 && newRow < rows && 
61                        newCol >= 0 && newCol < cols && 
62                        !visited[newRow][newCol]) {
63                      
64                        visited[newRow][newCol] = true;
65                        minHeap.push({grid[newRow][newCol], newRow, newCol});
66                    }
67                }
68            }
69          
70            // Store the count for this query at its original position
71            result[originalIndex] = reachableCount;
72        }
73      
74        return result;
75    }
76};
77
1// Direction vectors for moving up, right, down, left
2const directions: number[] = [-1, 0, 1, 0, -1];
3
4function maxPoints(grid: number[][], queries: number[]): number[] {
5    const numQueries = queries.length;
6  
7    // Create pairs of [query value, original index] for sorting
8    const sortedQueries: [number, number][] = [];
9    for (let i = 0; i < numQueries; i++) {
10        sortedQueries.push([queries[i], i]);
11    }
12  
13    // Sort queries by value to process them in ascending order
14    sortedQueries.sort((a, b) => a[0] - b[0]);
15  
16    // Result array to store answers in original query order
17    const result: number[] = new Array(numQueries).fill(0);
18  
19    const rows = grid.length;
20    const cols = grid[0].length;
21  
22    // Visited array to track processed cells
23    const visited: boolean[][] = Array(rows)
24        .fill(null)
25        .map(() => Array(cols).fill(false));
26  
27    // Mark starting position as visited
28    visited[0][0] = true;
29  
30    // Min heap to store cells by their values
31    // Each element contains: [cell value, row index, column index]
32    const minHeap: [number, number, number][] = [];
33  
34    // Helper function to maintain heap property when inserting
35    const heapPush = (item: [number, number, number]) => {
36        minHeap.push(item);
37        minHeap.sort((a, b) => a[0] - b[0]);
38    };
39  
40    // Helper function to remove minimum element from heap
41    const heapPop = (): [number, number, number] | undefined => {
42        return minHeap.shift();
43    };
44  
45    // Start from top-left corner
46    heapPush([grid[0][0], 0, 0]);
47  
48    // Counter for cells that can be reached
49    let reachableCount = 0;
50  
51    // Process each query in sorted order
52    for (const queryPair of sortedQueries) {
53        const threshold = queryPair[0];
54        const originalIndex = queryPair[1];
55      
56        // Process all cells with values less than current threshold
57        while (minHeap.length > 0 && minHeap[0][0] < threshold) {
58            const [cellValue, row, col] = heapPop()!;
59            reachableCount++;
60          
61            // Explore all 4 adjacent cells
62            for (let dir = 0; dir < 4; dir++) {
63                const newRow = row + directions[dir];
64                const newCol = col + directions[dir + 1];
65              
66                // Check if the new position is valid and unvisited
67                if (newRow >= 0 && newRow < rows && 
68                    newCol >= 0 && newCol < cols && 
69                    !visited[newRow][newCol]) {
70                  
71                    visited[newRow][newCol] = true;
72                    heapPush([grid[newRow][newCol], newRow, newCol]);
73                }
74            }
75        }
76      
77        // Store the count for this query at its original position
78        result[originalIndex] = reachableCount;
79    }
80  
81    return result;
82}
83

Time and Space Complexity

Time Complexity: O(k Γ— log k + m Γ— n Γ— log(m Γ— n))

The time complexity consists of two main parts:

  • Sorting the queries array takes O(k Γ— log k) where k is the number of queries
  • Processing the grid using a min-heap where each cell is visited at most once. Since we have m Γ— n cells total, and each heap operation (push/pop) takes O(log(heap_size)) time, where the heap can contain at most m Γ— n elements, the grid traversal takes O(m Γ— n Γ— log(m Γ— n))

Space Complexity: O(m Γ— n)

The space complexity is dominated by:

  • The vis (visited) array which stores a boolean value for each cell: O(m Γ— n)
  • The heap q which can contain at most m Γ— n elements (one for each cell): O(m Γ— n)
  • The sorted queries list and answer array: O(k)

Since m Γ— n is typically larger than k, the overall space complexity is O(m Γ— n).

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

Common Pitfalls

1. Marking Cells as Visited at the Wrong Time

The Pitfall: A common mistake is marking cells as visited only when they are popped from the heap (when processing them), rather than when they are first added to the heap.

Why It's Wrong:

# INCORRECT approach:
while min_heap and min_heap[0][0] < query_value:
    cell_value, row, col = heappop(min_heap)
    if visited[row][col]:  # Skip if already visited
        continue
    visited[row][col] = True  # Mark as visited when popped
    reachable_count += 1
  
    for row_delta, col_delta in directions:
        new_row, new_col = row + row_delta, col + col_delta
        if (0 <= new_row < rows and 0 <= new_col < cols):
            heappush(min_heap, (grid[new_row][new_col], new_row, new_col))
            # NOT marking as visited here!

This leads to:

  • Duplicate entries in the heap: The same cell can be added multiple times from different neighbors
  • Incorrect counting: When duplicates are popped, they might be counted multiple times
  • Performance degradation: The heap grows unnecessarily large with duplicates

The Solution: Always mark cells as visited immediately when adding them to the heap:

# CORRECT approach:
if (0 <= new_row < rows and 
    0 <= new_col < cols and 
    not visited[new_row][new_col]):
    heappush(min_heap, (grid[new_row][new_col], new_row, new_col))
    visited[new_row][new_col] = True  # Mark immediately when added

2. Resetting State Between Queries

The Pitfall: Another common mistake is resetting the visited matrix or reachable count between queries, thinking each query should be processed independently from scratch.

Why It's Wrong:

# INCORRECT approach:
for query_value, original_index in sorted_queries:
    visited = [[False] * cols for _ in range(rows)]  # Reset visited
    reachable_count = 0  # Reset count
    min_heap = [(grid[0][0], 0, 0)]  # Reset heap
    # ... process query

This breaks the optimization because:

  • Since queries are sorted in ascending order, cells reachable by a smaller query are also reachable by all larger queries
  • Resetting would require re-exploring the same cells multiple times, defeating the purpose of sorting queries

The Solution: Maintain cumulative state across queries. The visited cells and count accumulate as we process larger queries, which is why we sort them first. Each larger query inherits all cells accessible by smaller queries plus any additional cells it can reach.

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

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

Recommended Readings

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

Load More