You are given a stream of points on the X-Y plane. Design an algorithm that:
An axis-aligned square is a square whose edges are all the same length and are either parallel or perpendicular to the x-axis and y-axis.
Implement the DetectSquares
class:
DetectSquares()
Initializes the object with an empty data structure.void add(int[] point)
Adds a new point point = [x, y]
to the data structure.int count(int[] point)
Counts the number of ways to form axis-aligned squares with point point = [x, y]
as described above.Example:
Input:
["DetectSquares", "add", "add", "add", "count", "count", "add", "count"]
[[], [[3, 10]], [[11, 2]], [[3, 2]], [[11, 10]], [[14, 8]], [[11, 2]], [[11, 10]]]
Output:
[null, null, null, null, 1, 0, null, 2]
Explanation:
DetectSquares detectSquares = new DetectSquares();
detectSquares.add([3, 10]);
detectSquares.add([11, 2]);
detectSquares.add([3, 2]);
detectSquares.count([11, 10]); // return 1. You can choose:
// - The first, second, and third points
detectSquares.count([14, 8]); // return 0. The query point cannot form a square with any points in the data structure.
detectSquares.add([11, 2]); // Adding duplicate points is allowed.
detectSquares.count([11, 10]); // return 2. You can choose:
// - The first, second, and third points
// - The first, third, and fourth points
Constraints:
point.length == 2
0 <= x, y <= 1000
3000
calls in total will be made to add
and count
.When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:
The brute force approach to finding squares from a set of points involves checking every possible combination of points. We try every combination and determine if that combination forms a square. This method guarantees finding all squares, albeit inefficiently.
Here's how the algorithm would work step-by-step:
import math
def detect_squares_brute_force(points):
number_of_points = len(points)
square_count = 0
for first_point_index in range(number_of_points):
for second_point_index in range(first_point_index + 1, number_of_points):
for third_point_index in range(second_point_index + 1, number_of_points):
first_point = points[first_point_index]
second_point = points[second_point_index]
third_point = points[third_point_index]
distance_1_2 = calculate_distance(first_point, second_point)
distance_1_3 = calculate_distance(first_point, third_point)
distance_2_3 = calculate_distance(second_point, third_point)
distances = [distance_1_2, distance_1_3, distance_2_3]
distances.sort()
if abs(distances[0] - distances[1]) > 1e-6:
continue
# Two sides must be equal to possibly form a square
possible_fourth_point = calculate_fourth_point(first_point, second_point, third_point)
if possible_fourth_point in points:
# Fourth point must actually exist
all_points = [first_point, second_point, third_point, possible_fourth_point]
if is_valid_square(all_points):
square_count += 1
return square_count
def calculate_distance(point_1, point_2):
return math.sqrt((point_1[0] - point_2[0])**2 + (point_1[1] - point_2[1])**2)
def calculate_fourth_point(point_1, point_2, point_3):
# Calculates potential 4th point given 3 others
x1, y1 = point_1
x2, y2 = point_2
x3, y3 = point_3
x4a = x2 + x3 - x1
y4a = y2 + y3 - y1
x4b = x1 + x3 - x2
y4b = y1 + y3 - y2
x4c = x1 + x2 - x3
y4c = y1 + y2 - y3
return (x4a, y4a)
def is_valid_square(points):
# Validate that all sides are equal and right angles exist
point_1, point_2, point_3, point_4 = points
distances = [
calculate_distance(point_1, point_2),
calculate_distance(point_1, point_3),
calculate_distance(point_1, point_4),
calculate_distance(point_2, point_3),
calculate_distance(point_2, point_4),
calculate_distance(point_3, point_4)
]
distances.sort()
side_length = distances[0]
diagonal_length = distances[-1]
if abs(side_length * math.sqrt(2) - diagonal_length) > 1e-6:
return False
if abs(distances[0] - distances[3]) > 1e-6:
return False
return True
The key is to efficiently count how many points exist for each possible square we can form. We store each point seen so far, and when a new point comes in, we check if it can be a corner of a square with previously seen points. By focusing on finding potential squares instead of checking every possibility, we save time.
Here's how the algorithm would work step-by-step:
class DetectSquares:
def __init__(self):
self.pointCounts = {}
def add(self, point):
point_string = tuple(point)
self.pointCounts[point_string] = self.pointCounts.get(point_string, 0) + 1
def count(self, point):
count = 0
point_x, point_y = point
# Iterate through all points we've seen so far.
for seen_point, seen_point_count in self.pointCounts.items():
seen_x, seen_y = seen_point
# Ensure it can be a diagonal corner of a square.
if abs(point_x - seen_x) != abs(point_y - seen_y) or (point_x == seen_x or point_y == seen_y):
continue
# Calculate the other two potential corners
other_x1, other_y1 = point_x, seen_y
other_x2, other_y2 = seen_x, point_y
other_point1 = (other_x1, other_y1)
other_point2 = (other_x2, other_y2)
# Verify the other two corners exist
if other_point1 in self.pointCounts and other_point2 in self.pointCounts:
# Multiply counts to get total squares
count += seen_point_count * self.pointCounts[other_point1] * self.pointCounts[other_point2]
return count
Case | How to Handle |
---|---|
Duplicate points added repeatedly | The count map should increment the count for each repeated point to accurately reflect the frequency of that point, which is important for 'count' calculations. |
Points forming very large squares (close to integer overflow during area/distance calculations) | Use long data types to prevent integer overflow during distance or count calculations if the coordinates can result in large intermediate values. |
No points have been added prior to a 'count' call | Return 0 if no points have been added because no square can be formed. |
Points are added that are collinear and would never form a square | The 'count' method automatically filters out collinear points when it looks for the other two points that would complete a square, as they won't exist at the required coordinates. |
Negative coordinates are provided | The solution should handle negative coordinates, as squares can be formed in any quadrant by ensuring correct index access using offsets if necessary and appropriate bounds. |
Large number of add operations followed by few count operations | Ensure the memory used by the data structure holding the points scales linearly with the number of added points; consider using a hash map or similar for efficient storage and lookup. |
The point to be counted is not in the data structure (x,y) | Return 0 if the given point (x, y) does not exist in the data structure, as it cannot be a corner of any square formed by the existing points. |
Points are close to the maximum integer limits which can cause overflow issues. | Use appropriate data types (e.g., long) to handle large coordinates and differences between coordinates to prevent potential overflow during distance or area calculations. |