Facebook Pixel

3242. Design Neighbor Sum Service

EasyDesignArrayHash TableMatrixSimulation
Leetcode Link

Problem Description

You need to design a class called NeighborSum that works with a square grid (n×n 2D array) containing distinct elements ranging from 0 to n²-1.

The class should support three operations:

  1. Constructor NeighborSum(int[][] grid): Initializes the object with the given grid.

  2. Adjacent Sum adjacentSum(int value): Returns the sum of elements that are directly adjacent to the given value in the grid. Adjacent means the cells that share an edge with the cell containing value - specifically the cells directly above, below, left, and right.

  3. Diagonal Sum diagonalSum(int value): Returns the sum of elements that are diagonally adjacent to the given value in the grid. Diagonally adjacent means the cells at the four corners relative to the cell containing value - specifically the cells at top-left, top-right, bottom-left, and bottom-right positions.

The solution uses a hash table to store the position (row, column) of each element in the grid for quick lookup. When calculating sums, it uses direction vectors to iterate through the required neighboring cells. The dirs tuple contains two sets of direction offsets - one for adjacent cells (-1, 0, 1, 0, -1) and another for diagonal cells (-1, 1, 1, -1, -1). The pairwise function is used to extract consecutive pairs of values that represent the row and column offsets for each neighbor. The implementation checks boundaries to ensure only valid grid positions are included in the sum.

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

Intuition

The key insight is that we need to frequently find where a specific value is located in the grid and then calculate sums of its neighbors. Since values are distinct and we might query the same value multiple times, preprocessing the grid to create a value-to-position mapping makes sense.

Instead of searching through the entire grid every time we need to find a value (which would take O(n²) time), we can build a hash table during initialization that maps each value to its coordinates (i, j). This gives us O(1) lookup time for any value.

For calculating neighbor sums, we observe that:

  • Adjacent neighbors are always at a fixed offset: up (-1, 0), right (0, 1), down (1, 0), left (0, -1)
  • Diagonal neighbors are also at fixed offsets: top-left (-1, -1), top-right (-1, 1), bottom-right (1, 1), bottom-left (1, -1)

We can encode these directions cleverly. Notice that adjacent directions can be represented as consecutive pairs from the sequence (-1, 0, 1, 0, -1). Using pairwise, we get (-1, 0), (0, 1), (1, 0), (0, -1). Similarly, diagonal directions come from (-1, 1, 1, -1, -1) giving us (-1, 1), (1, 1), (1, -1), (-1, -1).

This unified approach allows us to write a single helper function cal that handles both types of sums by just switching the direction array. We simply iterate through the appropriate neighbors, check if they're within bounds, and accumulate their values.

Solution Approach

The solution uses a hash table to efficiently map values to their positions in the grid. Here's how the implementation works:

Initialization (__init__ method):

  • Store the grid reference in self.grid
  • Create an empty hash table self.d to store value-to-position mappings
  • Define direction vectors in self.dirs tuple:
    • First element (-1, 0, 1, 0, -1) encodes adjacent directions
    • Second element (-1, 1, 1, -1, -1) encodes diagonal directions
  • Iterate through the entire grid using enumerate to get both indices and values
  • For each element x at position (i, j), store self.d[x] = (i, j)

Helper Function (cal method): This unified function handles both adjacent and diagonal sum calculations:

  • Retrieve the position (i, j) of the given value from the hash table self.d[value]
  • Initialize sum s = 0
  • Select the appropriate direction array using index k (0 for adjacent, 1 for diagonal)
  • Use pairwise(self.dirs[k]) to generate consecutive pairs representing direction offsets:
    • For adjacent (k=0): generates (-1, 0), (0, 1), (1, 0), (0, -1)
    • For diagonal (k=1): generates (-1, 1), (1, 1), (1, -1), (-1, -1)
  • For each direction offset (a, b):
    • Calculate neighbor position: x, y = i + a, j + b
    • Check if the position is within grid boundaries: 0 <= x < len(self.grid) and 0 <= y < len(self.grid[0])
    • If valid, add the neighbor's value to the sum: s += self.grid[x][y]
  • Return the calculated sum

Public Methods:

  • adjacentSum(value): Simply calls self.cal(value, 0) to calculate adjacent sum
  • diagonalSum(value): Simply calls self.cal(value, 1) to calculate diagonal sum

The time complexity for initialization is O(n²) where n is the grid size, while both sum operations run in O(1) time since we're checking at most 4 neighbors and using O(1) hash table lookup.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through a concrete example with a 3×3 grid:

grid = [[0, 1, 2],
        [3, 4, 5],
        [6, 7, 8]]

Step 1: Initialization When we create NeighborSum(grid), the constructor builds a hash table mapping each value to its position:

  • d[0] = (0, 0), d[1] = (0, 1), d[2] = (0, 2)
  • d[3] = (1, 0), d[4] = (1, 1), d[5] = (1, 2)
  • d[6] = (2, 0), d[7] = (2, 1), d[8] = (2, 2)

The direction vectors are also stored:

  • dirs[0] = (-1, 0, 1, 0, -1) for adjacent neighbors
  • dirs[1] = (-1, 1, 1, -1, -1) for diagonal neighbors

Step 2: Calculate adjacentSum(4)

  1. Look up position of value 4: d[4] = (1, 1) (center of grid)
  2. Use pairwise(dirs[0]) to get adjacent offsets:
    • (-1, 0) → position (0, 1) → value = 1 ✓
    • (0, 1) → position (1, 2) → value = 5 ✓
    • (1, 0) → position (2, 1) → value = 7 ✓
    • (0, -1) → position (1, 0) → value = 3 ✓
  3. Sum = 1 + 5 + 7 + 3 = 16

Step 3: Calculate diagonalSum(4)

  1. Look up position of value 4: d[4] = (1, 1)
  2. Use pairwise(dirs[1]) to get diagonal offsets:
    • (-1, 1) → position (0, 2) → value = 2 ✓
    • (1, 1) → position (2, 2) → value = 8 ✓
    • (1, -1) → position (2, 0) → value = 6 ✓
    • (-1, -1) → position (0, 0) → value = 0 ✓
  3. Sum = 2 + 8 + 6 + 0 = 16

Step 4: Calculate adjacentSum(0) (corner element)

  1. Look up position of value 0: d[0] = (0, 0) (top-left corner)
  2. Check each adjacent position:
    • (-1, 0) → position (-1, 0) → out of bounds ✗
    • (0, 1) → position (0, 1) → value = 1 ✓
    • (1, 0) → position (1, 0) → value = 3 ✓
    • (0, -1) → position (0, -1) → out of bounds ✗
  3. Sum = 1 + 3 = 4

The boundary checking ensures we only sum valid neighbors, making the solution robust for edge and corner elements.

Solution Implementation

1from typing import List
2from itertools import pairwise
3
4
5class NeighborSum:
6    def __init__(self, grid: List[List[int]]):
7        """
8        Initialize with a 2D grid of unique integers.
9        Creates a mapping from each value to its (row, col) position.
10        """
11        self.grid = grid
12        # Dictionary to store value -> (row, col) mapping
13        self.value_to_position = {}
14      
15        # Direction vectors for adjacent and diagonal neighbors
16        # Adjacent: (-1,0), (0,1), (1,0), (0,-1) representing up, right, down, left
17        # Diagonal: (-1,-1), (1,1), (1,-1), (-1,1) representing four diagonal directions
18        self.directions = ((-1, 0, 1, 0, -1), (-1, 1, 1, -1, -1))
19      
20        # Build the value to position mapping
21        for row_idx, row in enumerate(grid):
22            for col_idx, value in enumerate(row):
23                self.value_to_position[value] = (row_idx, col_idx)
24
25    def adjacentSum(self, value: int) -> int:
26        """
27        Calculate the sum of all orthogonally adjacent cells (up, down, left, right).
28      
29        Args:
30            value: The value whose adjacent neighbors' sum to calculate
31          
32        Returns:
33            Sum of all valid adjacent neighbors
34        """
35        return self._calculate_neighbor_sum(value, direction_type=0)
36
37    def _calculate_neighbor_sum(self, value: int, direction_type: int) -> int:
38        """
39        Helper method to calculate sum of neighbors based on direction type.
40      
41        Args:
42            value: The value whose neighbors' sum to calculate
43            direction_type: 0 for adjacent (orthogonal), 1 for diagonal
44          
45        Returns:
46            Sum of all valid neighbors in the specified direction
47        """
48        # Get position of the value in the grid
49        row, col = self.value_to_position[value]
50        total_sum = 0
51      
52        # Use pairwise to get consecutive pairs from direction array
53        # This creates the (dx, dy) offsets for each neighbor
54        for dx, dy in pairwise(self.directions[direction_type]):
55            neighbor_row = row + dx
56            neighbor_col = col + dy
57          
58            # Check if the neighbor position is within grid bounds
59            if (0 <= neighbor_row < len(self.grid) and 
60                0 <= neighbor_col < len(self.grid[0])):
61                total_sum += self.grid[neighbor_row][neighbor_col]
62      
63        return total_sum
64
65    def diagonalSum(self, value: int) -> int:
66        """
67        Calculate the sum of all diagonally adjacent cells.
68      
69        Args:
70            value: The value whose diagonal neighbors' sum to calculate
71          
72        Returns:
73            Sum of all valid diagonal neighbors
74        """
75        return self._calculate_neighbor_sum(value, direction_type=1)
76
77
78# Your NeighborSum object will be instantiated and called as such:
79# obj = NeighborSum(grid)
80# param_1 = obj.adjacentSum(value)
81# param_2 = obj.diagonalSum(value)
82
1class NeighborSum {
2    // Store the grid
3    private int[][] grid;
4    // Map to store value -> position (row, col) mapping for O(1) lookup
5    private final Map<Integer, int[]> valueToPosition = new HashMap<>();
6    // Direction arrays: first row for adjacent (up, right, down, left)
7    //                   second row for diagonal (up-left, up-right, down-right, down-left)
8    private final int[][] directions = {{-1, 0, 1, 0, -1}, {-1, 1, 1, -1, -1}};
9
10    /**
11     * Constructor to initialize the grid and build position mapping
12     * @param grid 2D array containing unique values
13     */
14    public NeighborSum(int[][] grid) {
15        this.grid = grid;
16        int rows = grid.length;
17        int cols = grid[0].length;
18      
19        // Build a mapping from each value to its position in the grid
20        for (int row = 0; row < rows; row++) {
21            for (int col = 0; col < cols; col++) {
22                valueToPosition.put(grid[row][col], new int[] {row, col});
23            }
24        }
25    }
26
27    /**
28     * Calculate sum of adjacent neighbors (up, down, left, right)
29     * @param value The value whose adjacent neighbors need to be summed
30     * @return Sum of adjacent neighbors
31     */
32    public int adjacentSum(int value) {
33        return calculateSum(value, 0);
34    }
35
36    /**
37     * Calculate sum of diagonal neighbors (four diagonal positions)
38     * @param value The value whose diagonal neighbors need to be summed
39     * @return Sum of diagonal neighbors
40     */
41    public int diagonalSum(int value) {
42        return calculateSum(value, 1);
43    }
44
45    /**
46     * Helper method to calculate sum based on direction type
47     * @param value The target value in the grid
48     * @param directionType 0 for adjacent, 1 for diagonal
49     * @return Sum of neighbors based on direction type
50     */
51    private int calculateSum(int value, int directionType) {
52        // Get the position of the value
53        int[] position = valueToPosition.get(value);
54        int sum = 0;
55      
56        // Iterate through 4 directions (adjacent or diagonal based on directionType)
57        for (int i = 0; i < 4; i++) {
58            // Calculate new position using direction arrays
59            int newRow = position[0] + directions[directionType][i];
60            int newCol = position[1] + directions[directionType][i + 1];
61          
62            // Check if the new position is within grid bounds
63            if (newRow >= 0 && newRow < grid.length && 
64                newCol >= 0 && newCol < grid[0].length) {
65                sum += grid[newRow][newCol];
66            }
67        }
68      
69        return sum;
70    }
71}
72
73/**
74 * Your NeighborSum object will be instantiated and called as such:
75 * NeighborSum obj = new NeighborSum(grid);
76 * int param_1 = obj.adjacentSum(value);
77 * int param_2 = obj.diagonalSum(value);
78 */
79
1class NeighborSum {
2public:
3    /**
4     * Constructor: Initialize the grid and build a value-to-position mapping
5     * @param grid: 2D grid containing unique values
6     */
7    NeighborSum(vector<vector<int>>& grid) {
8        this->grid = grid;
9        int rows = grid.size();
10        int cols = grid[0].size();
11      
12        // Build hash map: value -> (row, col) position
13        for (int i = 0; i < rows; ++i) {
14            for (int j = 0; j < cols; ++j) {
15                valueToPosition[grid[i][j]] = {i, j};
16            }
17        }
18    }
19  
20    /**
21     * Calculate sum of adjacent neighbors (up, down, left, right)
22     * @param value: The value whose adjacent neighbors we want to sum
23     * @return: Sum of all adjacent neighbor values
24     */
25    int adjacentSum(int value) {
26        return calculateSum(value, 0);
27    }
28  
29    /**
30     * Calculate sum of diagonal neighbors (top-left, top-right, bottom-left, bottom-right)
31     * @param value: The value whose diagonal neighbors we want to sum
32     * @return: Sum of all diagonal neighbor values
33     */
34    int diagonalSum(int value) {
35        return calculateSum(value, 1);
36    }
37
38private:
39    vector<vector<int>> grid;
40    unordered_map<int, pair<int, int>> valueToPosition;
41  
42    // Direction arrays for adjacent and diagonal neighbors
43    // directions[0]: adjacent directions (up, right, down, left)
44    // directions[1]: diagonal directions (top-left, top-right, bottom-right, bottom-left)
45    int directions[2][5] = {
46        {-1, 0, 1, 0, -1},    // Adjacent: row differences followed by column differences
47        {-1, 1, 1, -1, -1}    // Diagonal: alternating row and column differences
48    };
49  
50    /**
51     * Helper function to calculate sum based on direction type
52     * @param value: The target value in the grid
53     * @param directionType: 0 for adjacent, 1 for diagonal
54     * @return: Sum of neighbors based on direction type
55     */
56    int calculateSum(int value, int directionType) {
57        // Get position of the value
58        auto [row, col] = valueToPosition[value];
59        int sum = 0;
60      
61        // Iterate through 4 directions (adjacent or diagonal)
62        for (int k = 0; k < 4; ++k) {
63            int newRow = row + directions[directionType][k];
64            int newCol = col + directions[directionType][k + 1];
65          
66            // Check if the new position is within grid boundaries
67            if (newRow >= 0 && newRow < grid.size() && 
68                newCol >= 0 && newCol < grid[0].size()) {
69                sum += grid[newRow][newCol];
70            }
71        }
72      
73        return sum;
74    }
75};
76
77/**
78 * Your NeighborSum object will be instantiated and called as such:
79 * NeighborSum* obj = new NeighborSum(grid);
80 * int param_1 = obj->adjacentSum(value);
81 * int param_2 = obj->diagonalSum(value);
82 */
83
1// Store the grid globally
2let grid: number[][] = [];
3
4// Map to store value to position mapping
5let valueToPosition: Map<number, [number, number]> = new Map();
6
7// Direction vectors for adjacent and diagonal neighbors
8// First row: adjacent (up, right, down, left)
9// Second row: diagonal (top-left, top-right, bottom-right, bottom-left)
10const directionVectors: number[][] = [
11    [-1, 0, 1, 0, -1],  // Adjacent: combines row and column offsets
12    [-1, 1, 1, -1, -1], // Diagonal: combines row and column offsets
13];
14
15/**
16 * Initialize the grid and build value to position mapping
17 * @param inputGrid - 2D array representing the grid
18 */
19function initializeGrid(inputGrid: number[][]): void {
20    grid = inputGrid;
21    valueToPosition.clear();
22  
23    // Build mapping of each value to its position in the grid
24    for (let row = 0; row < grid.length; row++) {
25        for (let col = 0; col < grid[0].length; col++) {
26            valueToPosition.set(grid[row][col], [row, col]);
27        }
28    }
29}
30
31/**
32 * Calculate sum of adjacent neighbors (up, down, left, right)
33 * @param value - The target value in the grid
34 * @returns Sum of all adjacent neighbor values
35 */
36function adjacentSum(value: number): number {
37    return calculateNeighborSum(value, 0);
38}
39
40/**
41 * Calculate sum of diagonal neighbors
42 * @param value - The target value in the grid
43 * @returns Sum of all diagonal neighbor values
44 */
45function diagonalSum(value: number): number {
46    return calculateNeighborSum(value, 1);
47}
48
49/**
50 * Helper function to calculate neighbor sum based on direction type
51 * @param value - The target value in the grid
52 * @param directionType - 0 for adjacent, 1 for diagonal
53 * @returns Sum of neighbors based on direction type
54 */
55function calculateNeighborSum(value: number, directionType: number): number {
56    // Get position of the value from the map
57    const position = valueToPosition.get(value);
58    if (!position) return 0;
59  
60    const [currentRow, currentCol] = position;
61    let sum = 0;
62  
63    // Iterate through 4 directions (adjacent or diagonal based on directionType)
64    for (let direction = 0; direction < 4; direction++) {
65        // Calculate neighbor position using direction vectors
66        const neighborRow = currentRow + directionVectors[directionType][direction];
67        const neighborCol = currentCol + directionVectors[directionType][direction + 1];
68      
69        // Check if neighbor position is within grid bounds
70        if (neighborRow >= 0 && neighborRow < grid.length && 
71            neighborCol >= 0 && neighborCol < grid[0].length) {
72            sum += grid[neighborRow][neighborCol];
73        }
74    }
75  
76    return sum;
77}
78

Time and Space Complexity

Time Complexity:

  • Initialization (__init__): The constructor iterates through every element in the grid to build a dictionary mapping each value to its position (i, j). With a grid of size m × n, this requires visiting all elements once, resulting in O(m × n) time complexity.

  • adjacentSum(value) and diagonalSum(value): Both methods call the cal function, which performs a constant number of operations. The cal function uses pairwise to iterate through exactly 4 directions (either adjacent or diagonal), and for each direction, it performs constant-time operations (boundary checking and grid access). The position lookup self.d[value] is O(1) due to the hash table. Therefore, both methods have O(1) time complexity.

Space Complexity:

  • The class stores the entire grid and creates a dictionary d that contains an entry for each element in the grid. The dictionary stores m × n key-value pairs where each value is a tuple of coordinates. Therefore, the space complexity is O(m × n).

  • The dirs tuple contains a fixed number of elements (10 total across both tuples), which is O(1) additional space.

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

Common Pitfalls

1. Incorrect Direction Vector Encoding

One of the most common mistakes is misunderstanding how the direction vectors are encoded using the compact form (-1, 0, 1, 0, -1) and how pairwise extracts the direction offsets.

The Pitfall: Developers might incorrectly assume the direction array directly represents individual offsets or try to iterate through it without using pairwise, leading to incorrect neighbor calculations.

# WRONG: Trying to use directions directly
directions = (-1, 0, 1, 0, -1)
for offset in directions:  # This doesn't give you (dx, dy) pairs!
    neighbor_row = row + offset
    # Missing the column offset entirely

The Solution: Understanding that pairwise creates consecutive overlapping pairs from the sequence. For (-1, 0, 1, 0, -1), it generates: (-1, 0), (0, 1), (1, 0), (0, -1) which represent (row_offset, col_offset) for up, right, down, and left respectively.

# CORRECT: Using pairwise to extract direction pairs
for dx, dy in pairwise(directions[0]):
    neighbor_row = row + dx
    neighbor_col = col + dy

2. Boundary Checking Logic Errors

Another frequent mistake involves incorrect boundary validation, especially forgetting that the grid might not be perfectly square or using wrong comparison operators.

The Pitfall:

# WRONG: Assuming square grid and using wrong bounds
if 0 <= neighbor_row <= len(self.grid) and 0 <= neighbor_col <= len(self.grid):
    # This allows neighbor_row == len(self.grid), which is out of bounds!

The Solution: Always use strict less-than for upper bounds and check both dimensions separately:

# CORRECT: Proper boundary checking
if (0 <= neighbor_row < len(self.grid) and 
    0 <= neighbor_col < len(self.grid[0])):
    total_sum += self.grid[neighbor_row][neighbor_col]

3. Missing Import or Python Version Incompatibility

The pairwise function is only available in Python 3.10+. Using it in earlier versions will cause an ImportError.

The Pitfall:

from itertools import pairwise  # ImportError in Python < 3.10

The Solution: For Python versions before 3.10, implement your own pairwise function or use an alternative approach:

# Alternative 1: Manual implementation of pairwise
def pairwise(iterable):
    a = iter(iterable)
    b = iter(iterable)
    next(b, None)
    return zip(a, b)

# Alternative 2: Use explicit direction arrays
class NeighborSum:
    def __init__(self, grid):
        self.grid = grid
        self.value_to_position = {}
        # More explicit but clearer
        self.adjacent_dirs = [(-1, 0), (0, 1), (1, 0), (0, -1)]
        self.diagonal_dirs = [(-1, -1), (-1, 1), (1, 1), (1, -1)]
      
        for i, row in enumerate(grid):
            for j, value in enumerate(row):
                self.value_to_position[value] = (i, j)
  
    def _calculate_neighbor_sum(self, value, use_diagonal):
        row, col = self.value_to_position[value]
        directions = self.diagonal_dirs if use_diagonal else self.adjacent_dirs
        total = 0
      
        for dx, dy in directions:
            nr, nc = row + dx, col + dy
            if 0 <= nr < len(self.grid) and 0 <= nc < len(self.grid[0]):
                total += self.grid[nr][nc]
      
        return total

This alternative approach is more readable and works across all Python versions, making it a safer choice for production code.

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

Which two pointer techniques do you use to check if a string is a palindrome?


Recommended Readings

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

Load More