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).
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:
-
Group points by distance: Create a hash table
g
where the key is the distanced = 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. -
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.
-
Track used tags: Use a set
vis
to keep track of which tags have already been included in our current square. -
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 invis
- If yes, we've found a duplicate tag - return the current answer immediately
- If no, add the tag to
vis
- Check if its tag
- If all points at this distance are valid, add their count to the answer
- Get all point indices at this distance from
-
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 usingmax(|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 EvaluatorExample 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' tovis = {'a'}
- Process point 3 (tag 'd'): Not in
vis
, add 'd' tovis = {'a', 'd'}
- Both points valid,
answer = 2
Distance 2:
- Process point 1 (tag 'b'): Not in
vis
, add 'b' tovis = {'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 calculationmax(abs(x), abs(y))
takesO(1)
time - Sorting the keys of dictionary
g
:O(k × log k)
, wherek
is the number of unique distances. In the worst case,k = n
(all points have different distances), so this becomesO(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 storesn
indices across all distance groups - Set
vis
storing unique characters:O(min(n, 26))
=O(1)
for lowercase English letters, orO(n)
in the general case - The sorted list of keys:
O(k)
wherek ≤ n
, soO(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
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
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
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
Want a Structured Path to Master System Design Too? Don’t Miss This!