Facebook Pixel

3143. Maximum Points Inside the Square

Problem Description

You have a 2D array points where each points[i] represents the coordinates [x, y] of point i, and a string s where s[i] represents the tag (label) of point i.

The task is to find a square that:

  • Is centered at the origin (0, 0)
  • Has edges parallel to the x and y axes
  • Does not contain two points with the same tag

You need to return the maximum number of points that can be contained in such a valid square.

Key rules:

  • A point is inside the square if it lies on or within the square's boundaries
  • The square's side length can be zero (meaning it could just be a single point at the origin)
  • The square must be "valid" - it cannot contain two points that share the same tag

For example, if you have points at (1, 2) with tag 'a', (2, 1) with tag 'b', and (3, 3) with tag 'a', a square with side length 4 (extending from -2 to 2 on both axes) would only contain the first two points. If we tried to make the square larger to include the third point, it would contain two points with tag 'a', making it invalid.

The goal is to find the size of the square that maximizes the number of points while maintaining validity (no duplicate tags).

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

Intuition

Since the square is centered at the origin and has edges parallel to the axes, we can think of it as extending equally in all four directions. For a square with side length 2d, it extends from -d to d on both the x and y axes.

The key insight is that for any point (x, y) to be inside this square, both |x| and |y| must be at most d. This means the point's distance from the origin (in terms of the square's boundaries) is determined by max(|x|, |y|). This value tells us the minimum half-side length needed for a square to contain that point.

Instead of trying different square sizes, we can group points by their required minimum square size. Points with max(|x|, |y|) = 3 need a square with half-side length at least 3, while points with max(|x|, |y|) = 5 need at least 5.

By sorting these groups in ascending order of their required square size, we can gradually "expand" our square from the origin outward. As we expand to each new size, we include all points at that distance. We keep track of which tags we've already seen - if we encounter a duplicate tag while expanding, we know we can't make the square any larger without violating the "no duplicate tags" constraint.

This greedy approach works because once we encounter a duplicate tag at distance d, any larger square would also contain this duplicate, making it invalid. Therefore, the maximum valid square is the one just before we encounter the first duplicate tag.

Learn more about Binary Search and Sorting patterns.

Solution Approach

The implementation uses a hash table and sorting to efficiently find the maximum valid square:

  1. Group points by distance: Create a hash table g where the key is the distance d = max(|x|, |y|) and the value is a list of indices of points at that distance. This effectively groups points by the minimum square size needed to contain them.

  2. Process points in order: Sort the keys of the hash table (the distances) in ascending order. This allows us to simulate expanding the square from the origin outward.

  3. Track used tags: Use a set vis to keep track of which tags have already been included in our current square.

  4. Expand and validate: For each distance d in sorted order:

    • Get all point indices at this distance from g[d]
    • For each point at this distance:
      • Check if its tag s[i] is already in vis
      • If yes, we've found a duplicate tag - return the current answer immediately
      • If no, add the tag to vis
    • If all points at this distance are valid, add their count to the answer
  5. Return result: If we process all distances without finding duplicates, return the total count.

The algorithm's efficiency comes from:

  • Mapping each point (x, y) to its minimum required square size using max(|x|, |y|) in O(1) time
  • Grouping points by distance to avoid checking unnecessary points
  • Early termination when the first duplicate tag is found
  • Using a set for O(1) tag lookup

This greedy approach guarantees the maximum number of points because we always try to include points closest to the origin first, and stop as soon as including more points would violate the constraint.

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

Input:

  • points = [[1,1], [-2,2], [3,1], [0,1], [2,2]]
  • s = "abcda"

Step 1: Calculate distances and group points

For each point, calculate d = max(|x|, |y|):

  • Point 0: [1,1]d = max(1,1) = 1, tag = 'a'
  • Point 1: [-2,2]d = max(2,2) = 2, tag = 'b'
  • Point 2: [3,1]d = max(3,1) = 3, tag = 'c'
  • Point 3: [0,1]d = max(0,1) = 1, tag = 'd'
  • Point 4: [2,2]d = max(2,2) = 2, tag = 'a'

Group by distance:

  • Distance 1: points [0, 3] with tags ['a', 'd']
  • Distance 2: points [1, 4] with tags ['b', 'a']
  • Distance 3: points [2] with tag ['c']

Step 2: Process distances in ascending order

Initialize: vis = {} (empty set), answer = 0

Distance 1:

  • Process point 0 (tag 'a'): Not in vis, add 'a' to vis = {'a'}
  • Process point 3 (tag 'd'): Not in vis, add 'd' to vis = {'a', 'd'}
  • Both points valid, answer = 2

Distance 2:

  • Process point 1 (tag 'b'): Not in vis, add 'b' to vis = {'a', 'd', 'b'}
  • Process point 4 (tag 'a'): 'a' is already in vis!
  • Found duplicate tag, stop immediately

Result: Return answer = 3

The maximum valid square has half-side length between 1 and 2 (specifically, it needs to be at least 1 but less than 2 to exclude point 4). This square contains 3 points: [1,1], [0,1], and [-2,2] with tags 'a', 'd', and 'b' respectively, with no duplicate tags.

Solution Implementation

1from collections import defaultdict
2from typing import List
3
4class Solution:
5    def maxPointsInsideSquare(self, points: List[List[int]], s: str) -> int:
6        # Group points by their distance from origin (using Chebyshev/infinity norm)
7        # The distance is the maximum of |x| and |y|, which represents the smallest
8        # square centered at origin that contains the point
9        distance_to_indices = defaultdict(list)
10      
11        for index, (x, y) in enumerate(points):
12            # Calculate the Chebyshev distance (L-infinity norm)
13            distance = max(abs(x), abs(y))
14            distance_to_indices[distance].append(index)
15      
16        # Track which tags have been used
17        used_tags = set()
18        total_points = 0
19      
20        # Process points in increasing order of distance
21        for distance in sorted(distance_to_indices.keys()):
22            indices_at_distance = distance_to_indices[distance]
23          
24            # Check if any point at this distance has a duplicate tag
25            for index in indices_at_distance:
26                tag = s[index]
27                if tag in used_tags:
28                    # Found a duplicate tag, return count before this distance
29                    return total_points
30                used_tags.add(tag)
31          
32            # All points at this distance have unique tags, include them
33            total_points += len(indices_at_distance)
34      
35        # All points can be included without tag conflicts
36        return total_points
37
1class Solution {
2    public int maxPointsInsideSquare(int[][] points, String s) {
3        // TreeMap to group points by their distance from origin (sorted by key)
4        // Key: maximum of absolute x and y coordinates (defines square boundary)
5        // Value: list of indices of points at this distance
6        TreeMap<Integer, List<Integer>> distanceToPointIndices = new TreeMap<>();
7      
8        // Group all points by their distance from origin
9        for (int i = 0; i < points.length; i++) {
10            int x = points[i][0];
11            int y = points[i][1];
12            // Distance from origin for a square is the max of absolute coordinates
13            int distance = Math.max(Math.abs(x), Math.abs(y));
14            // Add point index to the list for this distance
15            distanceToPointIndices.computeIfAbsent(distance, k -> new ArrayList<>()).add(i);
16        }
17      
18        // Track which characters (tags) have been used
19        boolean[] usedCharacters = new boolean[26];
20        int totalPoints = 0;
21      
22        // Process points in order of increasing distance from origin
23        for (List<Integer> pointIndices : distanceToPointIndices.values()) {
24            // Check if any character appears twice at current distance
25            for (int pointIndex : pointIndices) {
26                int characterIndex = s.charAt(pointIndex) - 'a';
27                // If character already used, we cannot include this distance level
28                if (usedCharacters[characterIndex]) {
29                    return totalPoints;
30                }
31                // Mark character as used
32                usedCharacters[characterIndex] = true;
33            }
34            // All points at this distance can be included
35            totalPoints += pointIndices.size();
36        }
37      
38        return totalPoints;
39    }
40}
41
1class Solution {
2public:
3    int maxPointsInsideSquare(vector<vector<int>>& points, string s) {
4        // Group points by their distance from origin (using Chebyshev distance)
5        // The distance is the maximum of absolute x and y coordinates
6        map<int, vector<int>> distanceToIndices;
7      
8        for (int i = 0; i < points.size(); ++i) {
9            auto& point = points[i];
10            // Calculate Chebyshev distance (L∞ norm) - determines the minimum square size
11            int distance = max(abs(point[0]), abs(point[1]));
12            distanceToIndices[distance].push_back(i);
13        }
14      
15        // Track which tags (characters) have been used
16        bool usedTags[26]{};
17        int validPointCount = 0;
18      
19        // Process points in order of increasing distance
20        for (auto& [distance, indices] : distanceToIndices) {
21            // Check if all points at this distance have unique tags
22            for (int index : indices) {
23                int tagIndex = s[index] - 'a';
24              
25                // If tag is already used, we cannot include this distance level
26                if (usedTags[tagIndex]) {
27                    return validPointCount;
28                }
29                usedTags[tagIndex] = true;
30            }
31          
32            // All points at this distance have unique tags, include them
33            validPointCount += indices.size();
34        }
35      
36        return validPointCount;
37    }
38};
39
1/**
2 * Finds the maximum number of points that can be placed inside a square
3 * without having duplicate characters at the same distance from origin
4 * @param points - Array of 2D coordinates representing point positions
5 * @param s - String where s[i] represents the character/tag for points[i]
6 * @returns Maximum number of valid points inside the square
7 */
8function maxPointsInsideSquare(points: number[][], s: string): number {
9    const pointCount: number = points.length;
10  
11    // Group points by their distance from origin (using Chebyshev distance)
12    // Key: distance, Value: array of point indices
13    const distanceToIndicesMap: Map<number, number[]> = new Map();
14  
15    for (let i = 0; i < pointCount; i++) {
16        const [x, y] = points[i];
17        // Calculate Chebyshev distance (max of absolute x and y)
18        const distance: number = Math.max(Math.abs(x), Math.abs(y));
19      
20        if (!distanceToIndicesMap.has(distance)) {
21            distanceToIndicesMap.set(distance, []);
22        }
23        distanceToIndicesMap.get(distance)!.push(i);
24    }
25  
26    // Sort distances in ascending order
27    const sortedDistances: number[] = Array.from(distanceToIndicesMap.keys()).sort((a, b) => a - b);
28  
29    // Track which characters have been used (26 lowercase letters)
30    const usedCharacters: boolean[] = Array(26).fill(false);
31    let validPointCount: number = 0;
32  
33    // Process points by increasing distance from origin
34    for (const distance of sortedDistances) {
35        const indicesAtDistance: number[] = distanceToIndicesMap.get(distance)!;
36      
37        // Check if any character at this distance is already used
38        for (const pointIndex of indicesAtDistance) {
39            // Convert character to index (0-25 for 'a'-'z')
40            const characterIndex: number = s.charCodeAt(pointIndex) - 'a'.charCodeAt(0);
41          
42            // If character already used, we cannot include this distance level
43            if (usedCharacters[characterIndex]) {
44                return validPointCount;
45            }
46            usedCharacters[characterIndex] = true;
47        }
48      
49        // All points at this distance are valid, add them to count
50        validPointCount += indicesAtDistance.length;
51    }
52  
53    return validPointCount;
54}
55

Time and Space Complexity

Time Complexity: O(n × log n)

The time complexity is determined by the following operations:

  • Creating the dictionary g by iterating through all points: O(n), where each point calculation max(abs(x), abs(y)) takes O(1) time
  • Sorting the keys of dictionary g: O(k × log k), where k is the number of unique distances. In the worst case, k = n (all points have different distances), so this becomes O(n × log n)
  • Iterating through the sorted distances and checking each point: O(n) total, as each point is visited exactly once

The dominant operation is the sorting step, giving us an overall time complexity of O(n × log n).

Space Complexity: O(n)

The space complexity comes from:

  • Dictionary g storing all point indices grouped by distance: O(n) in total, as it stores n indices across all distance groups
  • Set vis storing unique characters: O(min(n, 26)) = O(1) for lowercase English letters, or O(n) in the general case
  • The sorted list of keys: O(k) where k ≤ n, so O(n) in the worst case

The overall space complexity is O(n).

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

Common Pitfalls

Pitfall 1: Misunderstanding Distance Calculation

Issue: Using Euclidean distance sqrt(x² + y²) instead of Chebyshev distance max(|x|, |y|).

Since the square has edges parallel to the axes and is centered at the origin, a point (x, y) requires a square with side length 2 * max(|x|, |y|) to be contained. Using Euclidean distance would incorrectly determine which points can fit in the same square.

Example: Point (3, 1) requires a square extending from -3 to 3 (side length 6), not based on sqrt(10).

Solution:

# Correct: Chebyshev distance
distance = max(abs(x), abs(y))

# Incorrect: Euclidean distance
# distance = math.sqrt(x**2 + y**2)

Pitfall 2: Including Points at Invalid Distance Level

Issue: Adding all points at the current distance to the answer before checking if they create tag conflicts.

The algorithm must check ALL points at the same distance for tag conflicts BEFORE adding any of them to the result. If any point at distance d has a duplicate tag, NO points at distance d should be included.

Incorrect approach:

for distance in sorted(distance_to_indices.keys()):
    for index in distance_to_indices[distance]:
        tag = s[index]
        if tag in used_tags:
            return total_points
        used_tags.add(tag)
        total_points += 1  # Wrong: incrementing immediately

Correct approach:

for distance in sorted(distance_to_indices.keys()):
    # First check ALL points at this distance
    for index in distance_to_indices[distance]:
        tag = s[index]
        if tag in used_tags:
            return total_points  # Don't include ANY points at this distance
        used_tags.add(tag)
  
    # Only increment after confirming all points are valid
    total_points += len(distance_to_indices[distance])

Pitfall 3: Not Handling Edge Cases

Issue: Forgetting to handle special cases like empty input or single point at origin.

Solution: The current implementation handles these naturally:

  • Empty points array returns 0
  • Single point works correctly
  • Point at origin (0, 0) has distance 0 and is handled properly

Pitfall 4: Using Wrong Data Structure for Tag Tracking

Issue: Using a list instead of a set for used_tags, leading to O(n) lookup time instead of O(1).

Solution: Always use a set for membership checking:

used_tags = set()  # O(1) lookup
# Not: used_tags = []  # O(n) lookup
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What does the following code do?

1def f(arr1, arr2):
2  i, j = 0, 0
3  new_arr = []
4  while i < len(arr1) and j < len(arr2):
5      if arr1[i] < arr2[j]:
6          new_arr.append(arr1[i])
7          i += 1
8      else:
9          new_arr.append(arr2[j])
10          j += 1
11  new_arr.extend(arr1[i:])
12  new_arr.extend(arr2[j:])
13  return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2  int i = 0, j = 0;
3  List<Integer> newArr = new ArrayList<>();
4
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.add(arr1[i]);
8          i++;
9      } else {
10          newArr.add(arr2[j]);
11          j++;
12      }
13  }
14
15  while (i < arr1.length) {
16      newArr.add(arr1[i]);
17      i++;
18  }
19
20  while (j < arr2.length) {
21      newArr.add(arr2[j]);
22      j++;
23  }
24
25  return newArr;
26}
27
1function f(arr1, arr2) {
2  let i = 0, j = 0;
3  let newArr = [];
4  
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.push(arr1[i]);
8          i++;
9      } else {
10          newArr.push(arr2[j]);
11          j++;
12      }
13  }
14  
15  while (i < arr1.length) {
16      newArr.push(arr1[i]);
17      i++;
18  }
19  
20  while (j < arr2.length) {
21      newArr.push(arr2[j]);
22      j++;
23  }
24  
25  return newArr;
26}
27

Recommended Readings

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

Load More