3242. Design Neighbor Sum Service
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:
-
Constructor
NeighborSum(int[][] grid)
: Initializes the object with the given grid. -
Adjacent Sum
adjacentSum(int value)
: Returns the sum of elements that are directly adjacent to the givenvalue
in the grid. Adjacent means the cells that share an edge with the cell containingvalue
- specifically the cells directly above, below, left, and right. -
Diagonal Sum
diagonalSum(int value)
: Returns the sum of elements that are diagonally adjacent to the givenvalue
in the grid. Diagonally adjacent means the cells at the four corners relative to the cell containingvalue
- 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.
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
- First element
- Iterate through the entire grid using enumerate to get both indices and values
- For each element
x
at position(i, j)
, storeself.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 tableself.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 adjacent (
- 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)
and0 <= y < len(self.grid[0])
- If valid, add the neighbor's value to the sum:
s += self.grid[x][y]
- Calculate neighbor position:
- Return the calculated sum
Public Methods:
adjacentSum(value)
: Simply callsself.cal(value, 0)
to calculate adjacent sumdiagonalSum(value)
: Simply callsself.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 EvaluatorExample 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 neighborsdirs[1] = (-1, 1, 1, -1, -1)
for diagonal neighbors
Step 2: Calculate adjacentSum(4)
- Look up position of value 4:
d[4] = (1, 1)
(center of grid) - 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 ✓
- Sum = 1 + 5 + 7 + 3 = 16
Step 3: Calculate diagonalSum(4)
- Look up position of value 4:
d[4] = (1, 1)
- 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 ✓
- Sum = 2 + 8 + 6 + 0 = 16
Step 4: Calculate adjacentSum(0) (corner element)
- Look up position of value 0:
d[0] = (0, 0)
(top-left corner) - 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 ✗
- 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 thegrid
to build a dictionary mapping each value to its position(i, j)
. With a grid of sizem × n
, this requires visiting all elements once, resulting inO(m × n)
time complexity. -
adjacentSum(value)
anddiagonalSum(value)
: Both methods call thecal
function, which performs a constant number of operations. Thecal
function usespairwise
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 lookupself.d[value]
isO(1)
due to the hash table. Therefore, both methods haveO(1)
time complexity.
Space Complexity:
-
The class stores the entire
grid
and creates a dictionaryd
that contains an entry for each element in the grid. The dictionary storesm × n
key-value pairs where each value is a tuple of coordinates. Therefore, the space complexity isO(m × n)
. -
The
dirs
tuple contains a fixed number of elements (10 total across both tuples), which isO(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.
Which two pointer techniques do you use to check if a string is a palindrome?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!