Facebook Pixel

1274. Number of Ships in a Rectangle πŸ”’

Problem Description

This is an interactive problem where you need to count the number of ships in a rectangular area on a 2D plane without knowing their exact positions.

You're given:

  • A rectangular search area defined by two points: topRight (upper-right corner) and bottomLeft (lower-left corner)
  • An API function Sea.hasShips(topRight, bottomLeft) that returns true if there's at least one ship within the specified rectangle (including boundaries), and false otherwise
  • Each ship occupies exactly one integer coordinate point
  • At most one ship can exist at any integer point
  • There are at most 10 ships total in the search area

Your task is to determine the exact count of ships within the given rectangle using only the hasShips API function. You cannot directly access the ship positions - you must work "blindfolded" by making strategic calls to hasShips.

Key Constraints:

  • You can make at most 400 calls to hasShips
  • Coordinates range from 0 to 1000
  • The solution uses a divide-and-conquer approach: recursively split the search area into four quadrants, check each quadrant for ships using hasShips, and only continue searching quadrants that contain ships
  • When a region is reduced to a single point (x1 == x2 and y1 == y2) and contains a ship, count it as 1
  • The algorithm efficiently prunes empty regions to minimize API calls while ensuring all ships are counted

The challenge is to design an efficient search strategy that finds all ships within the API call limit, leveraging the fact that there are at most 10 ships to optimize the search pattern.

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 efficiently search a potentially large area (up to 1000 x 1000 grid) for at most 10 ships, using a limited number of API calls (400). If we checked each point individually, we'd need up to 1 million calls, which is far too many.

The breakthrough comes from realizing we can use a divide-and-conquer strategy similar to binary search, but in 2D space. The hasShips API tells us if any ships exist in a rectangle, which we can leverage to intelligently prune our search space.

Here's the thought process:

  1. Start with the whole rectangle - if hasShips returns false, we immediately know there are 0 ships and we're done with just 1 API call.

  2. If ships exist, divide the rectangle into smaller parts - but how should we divide? The natural choice is to split into 4 quadrants by dividing both horizontally and vertically at the midpoints. This gives us four sub-rectangles of roughly equal size.

  3. Recursively search each quadrant - for each of the 4 quadrants, we call hasShips. If a quadrant has no ships, we skip it entirely (pruning that branch). If it has ships, we recursively divide it further.

  4. Base case: single point - when we've narrowed down to a single coordinate (x1 == x2 and y1 == y2), if hasShips returns true, we've found exactly 1 ship.

The efficiency comes from the fact that with at most 10 ships, most quadrants will be empty and get pruned early. Each level of recursion divides the area by 4, so we quickly zoom in on the actual ship locations. The worst case would be if ships are spread out maximally, but even then, with only 10 ships, we won't need many recursive calls to isolate each one.

This approach naturally balances between:

  • Broad coverage - we start by checking large areas
  • Focused search - we only drill down into areas that actually contain ships
  • Guaranteed completeness - we won't miss any ships since we systematically cover the entire area

Learn more about Divide and Conquer patterns.

Solution Approach

The solution implements a recursive divide-and-conquer algorithm that systematically narrows down the search space to locate all ships.

Implementation Details:

  1. Main Function Structure:

    • The countShips method initiates the search by calling a helper function dfs with the initial rectangle boundaries
    • The dfs function handles the recursive logic and returns the count of ships in its assigned rectangle
  2. Recursive Function dfs(topRight, bottomLeft):

    First, extract the coordinates for cleaner code:

    x1, y1 = bottomLeft.x, bottomLeft.y
    x2, y2 = topRight.x, topRight.y
  3. Boundary Validation:

    if x1 > x2 or y1 > y2:
        return 0

    This handles invalid rectangles where the bottom-left is actually to the right or above the top-right.

  4. Early Termination (Pruning):

    if not sea.hasShips(topRight, bottomLeft):
        return 0

    If the current rectangle contains no ships, immediately return 0. This is crucial for efficiency - it prevents unnecessary subdivision of empty areas.

  5. Base Case - Single Point:

    if x1 == x2 and y1 == y2:
        return 1

    When the rectangle is reduced to a single point and we know it contains a ship (from the previous hasShips check), we've found exactly one ship.

  6. Divide into Four Quadrants:

    Calculate the midpoints:

    midx = (x1 + x2) >> 1  # Equivalent to (x1 + x2) // 2
    midy = (y1 + y2) >> 1

    The >> 1 is a bitwise right shift, efficiently dividing by 2.

  7. Recursively Search Each Quadrant:

    The rectangle is divided into four parts:

    a = dfs(topRight, Point(midx + 1, midy + 1))          # Top-right quadrant
    b = dfs(Point(midx, y2), Point(x1, midy + 1))         # Top-left quadrant
    c = dfs(Point(midx, midy), bottomLeft)                # Bottom-left quadrant
    d = dfs(Point(x2, midy), Point(midx + 1, y1))         # Bottom-right quadrant

    The quadrants are defined as:

    • Top-right: from (midx + 1, midy + 1) to (x2, y2)
    • Top-left: from (x1, midy + 1) to (midx, y2)
    • Bottom-left: from (x1, y1) to (midx, midy)
    • Bottom-right: from (midx + 1, y1) to (x2, midy)
  8. Aggregate Results:

    return a + b + c + d

    The total count is the sum of ships found in all four quadrants.

Why This Works:

  • Each recursive call either finds ships in a smaller area or terminates early if no ships exist
  • The search space reduces exponentially with each level (by a factor of 4)
  • With at most 10 ships, most branches get pruned early, keeping the number of API calls well below 400
  • The algorithm guarantees finding all ships since it systematically covers every point in the original rectangle

Time Complexity Analysis:

  • In the worst case with ships spread out, we might need O(ships * log(area)) API calls
  • With 10 ships and a 1000 x 1000 grid, this stays well within the 400-call limit

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 small example to illustrate the solution approach. Imagine we have a 4Γ—4 grid (coordinates from 0 to 3) with 3 ships located at positions (1,1), (3,0), and (3,3).

Initial Call:

  • dfs(topRight=(3,3), bottomLeft=(0,0))
  • hasShips((3,3), (0,0)) returns true (there are ships in the area)
  • Not a single point, so we divide into quadrants
  • Calculate midpoints: midx = 1, midy = 1

Level 1 - Four Quadrants:

  1. Top-right quadrant: dfs((3,3), (2,2))

    • hasShips((3,3), (2,2)) returns true (ship at (3,3) is here)
    • Not a single point, divide further with midx=2, midy=2
    • Sub-quadrants:
      • dfs((3,3), (3,3)): Single point, hasShips returns true β†’ Count = 1
      • dfs((2,3), (2,3)): Single point, hasShips returns false β†’ Count = 0
      • dfs((2,2), (2,2)): Single point, hasShips returns false β†’ Count = 0
      • dfs((3,2), (3,2)): Single point, hasShips returns false β†’ Count = 0
    • Total for this quadrant: 1
  2. Top-left quadrant: dfs((1,3), (0,2))

    • hasShips((1,3), (0,2)) returns false (no ships here)
    • Early termination β†’ Count = 0
  3. Bottom-left quadrant: dfs((1,1), (0,0))

    • hasShips((1,1), (0,0)) returns true (ship at (1,1) is here)
    • Not a single point, divide with midx=0, midy=0
    • Sub-quadrants:
      • dfs((1,1), (1,1)): Single point, hasShips returns true β†’ Count = 1
      • dfs((0,1), (0,1)): Single point, hasShips returns false β†’ Count = 0
      • dfs((0,0), (0,0)): Single point, hasShips returns false β†’ Count = 0
      • dfs((1,0), (1,0)): Single point, hasShips returns false β†’ Count = 0
    • Total for this quadrant: 1
  4. Bottom-right quadrant: dfs((3,1), (2,0))

    • hasShips((3,1), (2,0)) returns true (ship at (3,0) is here)
    • Not a single point, divide with midx=2, midy=0
    • Sub-quadrants:
      • dfs((3,1), (3,1)): Single point, hasShips returns false β†’ Count = 0
      • dfs((2,1), (2,1)): Single point, hasShips returns false β†’ Count = 0
      • dfs((2,0), (2,0)): Single point, hasShips returns false β†’ Count = 0
      • dfs((3,0), (3,0)): Single point, hasShips returns true β†’ Count = 1
    • Total for this quadrant: 1

Final Result: 1 + 0 + 1 + 1 = 3 ships found

Key Observations:

  • The top-left quadrant was pruned immediately (1 API call saved multiple recursive calls)
  • Each ship was eventually isolated to a single point through recursive subdivision
  • The algorithm made approximately 20 API calls for this example, well below the 400 limit
  • Empty regions were efficiently skipped, focusing the search on areas with ships

Solution Implementation

1# """
2# This is Sea's API interface.
3# You should not implement it, or speculate about its implementation
4# """
5# class Sea:
6#    def hasShips(self, topRight: 'Point', bottomLeft: 'Point') -> bool:
7#
8# class Point:
9#     def __init__(self, x: int, y: int):
10#         self.x = x
11#         self.y = y
12
13
14class Solution:
15    def countShips(self, sea: "Sea", topRight: "Point", bottomLeft: "Point") -> int:
16        """
17        Count the number of ships in the given rectangle area using divide and conquer.
18      
19        Args:
20            sea: Sea object with hasShips API to check if ships exist in a rectangle
21            topRight: Top-right corner of the search area
22            bottomLeft: Bottom-left corner of the search area
23          
24        Returns:
25            Total number of ships in the given area
26        """
27      
28        def search_area(top_right: "Point", bottom_left: "Point") -> int:
29            """
30            Recursively search for ships in the given rectangular area.
31            Uses divide and conquer by splitting the area into 4 quadrants.
32          
33            Args:
34                top_right: Top-right corner of current search area
35                bottom_left: Bottom-left corner of current search area
36              
37            Returns:
38                Number of ships found in the current area
39            """
40            # Extract coordinates for clarity
41            left_x, bottom_y = bottom_left.x, bottom_left.y
42            right_x, top_y = top_right.x, top_right.y
43          
44            # Check if the rectangle is valid (not inverted)
45            if left_x > right_x or bottom_y > top_y:
46                return 0
47          
48            # Early termination: no ships in this area
49            if not sea.hasShips(top_right, bottom_left):
50                return 0
51          
52            # Base case: single point, contains exactly one ship
53            if left_x == right_x and bottom_y == top_y:
54                return 1
55          
56            # Calculate midpoints to divide the area into quadrants
57            mid_x = (left_x + right_x) >> 1  # Bitwise right shift for integer division by 2
58            mid_y = (bottom_y + top_y) >> 1
59          
60            # Recursively search all four quadrants
61            # Top-right quadrant
62            top_right_quadrant = search_area(
63                top_right, 
64                Point(mid_x + 1, mid_y + 1)
65            )
66          
67            # Top-left quadrant
68            top_left_quadrant = search_area(
69                Point(mid_x, top_y), 
70                Point(left_x, mid_y + 1)
71            )
72          
73            # Bottom-left quadrant
74            bottom_left_quadrant = search_area(
75                Point(mid_x, mid_y), 
76                bottom_left
77            )
78          
79            # Bottom-right quadrant
80            bottom_right_quadrant = search_area(
81                Point(right_x, mid_y), 
82                Point(mid_x + 1, bottom_y)
83            )
84          
85            # Return total count from all quadrants
86            return (top_right_quadrant + top_left_quadrant + 
87                   bottom_left_quadrant + bottom_right_quadrant)
88      
89        # Start the recursive search from the entire area
90        return search_area(topRight, bottomLeft)
91
1/**
2 * // This is Sea's API interface.
3 * // You should not implement it, or speculate about its implementation
4 * class Sea {
5 *     public boolean hasShips(int[] topRight, int[] bottomLeft);
6 * }
7 */
8
9class Solution {
10    /**
11     * Counts the number of ships in a rectangular sea area using divide and conquer.
12     * The algorithm recursively divides the search area into 4 quadrants and counts ships in each.
13     * 
14     * @param sea The Sea object with hasShips API to check if ships exist in a given rectangle
15     * @param topRight The top-right corner coordinates [x, y] of the search rectangle
16     * @param bottomLeft The bottom-left corner coordinates [x, y] of the search rectangle
17     * @return The total number of ships in the specified rectangular area
18     */
19    public int countShips(Sea sea, int[] topRight, int[] bottomLeft) {
20        // Extract coordinates for better readability
21        int leftX = bottomLeft[0];
22        int bottomY = bottomLeft[1];
23        int rightX = topRight[0];
24        int topY = topRight[1];
25      
26        // Base case: Invalid rectangle (coordinates are inverted)
27        if (leftX > rightX || bottomY > topY) {
28            return 0;
29        }
30      
31        // Base case: No ships in this rectangle, skip further division
32        if (!sea.hasShips(topRight, bottomLeft)) {
33            return 0;
34        }
35      
36        // Base case: Single point rectangle contains a ship
37        if (leftX == rightX && bottomY == topY) {
38            return 1;
39        }
40      
41        // Calculate midpoints to divide the rectangle into 4 quadrants
42        int midX = (leftX + rightX) >> 1;  // Equivalent to (leftX + rightX) / 2
43        int midY = (bottomY + topY) >> 1;  // Equivalent to (bottomY + topY) / 2
44      
45        // Recursively count ships in each quadrant:
46        // Top-right quadrant
47        int topRightQuadrant = countShips(sea, topRight, new int[] {midX + 1, midY + 1});
48      
49        // Top-left quadrant
50        int topLeftQuadrant = countShips(sea, new int[] {midX, topY}, new int[] {leftX, midY + 1});
51      
52        // Bottom-left quadrant
53        int bottomLeftQuadrant = countShips(sea, new int[] {midX, midY}, bottomLeft);
54      
55        // Bottom-right quadrant
56        int bottomRightQuadrant = countShips(sea, new int[] {rightX, midY}, new int[] {midX + 1, bottomY});
57      
58        // Return the total count from all quadrants
59        return topRightQuadrant + topLeftQuadrant + bottomLeftQuadrant + bottomRightQuadrant;
60    }
61}
62
1/**
2 * // This is Sea's API interface.
3 * // You should not implement it, or speculate about its implementation
4 * class Sea {
5 *   public:
6 *     bool hasShips(vector<int> topRight, vector<int> bottomLeft);
7 * };
8 */
9
10class Solution {
11public:
12    /**
13     * Counts the number of ships in a given rectangular area using divide and conquer
14     * @param sea - The Sea object with hasShips API to check if ships exist in a rectangle
15     * @param topRight - Top-right corner coordinates [x, y] of the search rectangle
16     * @param bottomLeft - Bottom-left corner coordinates [x, y] of the search rectangle
17     * @return The total number of ships in the specified rectangle
18     */
19    int countShips(Sea sea, vector<int> topRight, vector<int> bottomLeft) {
20        // Extract coordinates for clarity
21        int leftX = bottomLeft[0];
22        int bottomY = bottomLeft[1];
23        int rightX = topRight[0];
24        int topY = topRight[1];
25      
26        // Base case: Invalid rectangle (coordinates are flipped)
27        if (leftX > rightX || bottomY > topY) {
28            return 0;
29        }
30      
31        // Base case: No ships exist in this rectangle
32        if (!sea.hasShips(topRight, bottomLeft)) {
33            return 0;
34        }
35      
36        // Base case: Single point rectangle contains exactly one ship
37        if (leftX == rightX && bottomY == topY) {
38            return 1;
39        }
40      
41        // Calculate midpoints to divide the rectangle into 4 quadrants
42        int midX = (leftX + rightX) >> 1;  // Equivalent to (leftX + rightX) / 2
43        int midY = (bottomY + topY) >> 1;  // Equivalent to (bottomY + topY) / 2
44      
45        // Recursively count ships in each quadrant:
46        // Top-right quadrant
47        int topRightQuadrant = countShips(sea, topRight, {midX + 1, midY + 1});
48      
49        // Top-left quadrant
50        int topLeftQuadrant = countShips(sea, {midX, topY}, {leftX, midY + 1});
51      
52        // Bottom-left quadrant
53        int bottomLeftQuadrant = countShips(sea, {midX, midY}, bottomLeft);
54      
55        // Bottom-right quadrant
56        int bottomRightQuadrant = countShips(sea, {rightX, midY}, {midX + 1, bottomY});
57      
58        // Return the sum of ships in all four quadrants
59        return topRightQuadrant + topLeftQuadrant + bottomLeftQuadrant + bottomRightQuadrant;
60    }
61};
62
1/**
2 * // This is the Sea's API interface.
3 * // You should not implement it, or speculate about its implementation
4 * class Sea {
5 *      hasShips(topRight: number[], bottomLeft: number[]): boolean {}
6 * }
7 */
8
9/**
10 * Counts the number of ships in a rectangular sea area using divide and conquer approach.
11 * The algorithm recursively divides the search area into 4 quadrants and counts ships in each.
12 * 
13 * @param sea - The Sea object that provides the hasShips API
14 * @param topRight - Coordinates [x, y] of the top-right corner of the search area
15 * @param bottomLeft - Coordinates [x, y] of the bottom-left corner of the search area
16 * @returns The total number of ships in the specified rectangular area
17 */
18function countShips(sea: Sea, topRight: number[], bottomLeft: number[]): number {
19    // Extract coordinates for better readability
20    const [bottomLeftX, bottomLeftY] = bottomLeft;
21    const [topRightX, topRightY] = topRight;
22  
23    // Base case: Invalid rectangle or no ships in this area
24    if (bottomLeftX > topRightX || bottomLeftY > topRightY || !sea.hasShips(topRight, bottomLeft)) {
25        return 0;
26    }
27  
28    // Base case: Single point contains exactly one ship
29    if (bottomLeftX === topRightX && bottomLeftY === topRightY) {
30        return 1;
31    }
32  
33    // Calculate midpoints to divide the rectangle into 4 quadrants
34    const midX = (bottomLeftX + topRightX) >> 1;  // Bitwise right shift for integer division
35    const midY = (bottomLeftY + topRightY) >> 1;
36  
37    // Recursively count ships in each quadrant:
38    // Top-right quadrant
39    const topRightQuadrant = countShips(sea, topRight, [midX + 1, midY + 1]);
40  
41    // Top-left quadrant
42    const topLeftQuadrant = countShips(sea, [midX, topRightY], [bottomLeftX, midY + 1]);
43  
44    // Bottom-left quadrant
45    const bottomLeftQuadrant = countShips(sea, [midX, midY], bottomLeft);
46  
47    // Bottom-right quadrant
48    const bottomRightQuadrant = countShips(sea, [topRightX, midY], [midX + 1, bottomLeftY]);
49  
50    // Return the sum of ships in all quadrants
51    return topRightQuadrant + topLeftQuadrant + bottomLeftQuadrant + bottomRightQuadrant;
52}
53

Time and Space Complexity

Time Complexity: O(C Γ— log(max(m, n)))

The algorithm uses a divide-and-conquer approach by recursively dividing the search area into 4 quadrants. The key insights for time complexity analysis are:

  1. When there are no ships in a region (hasShips returns false), that branch terminates immediately with O(1) work.

  2. When a region contains ships, it's divided into 4 smaller regions. The division continues until we reach individual cells (where x1 == x2 and y1 == y2).

  3. The depth of recursion is O(log(max(m, n))) because we're halving the larger dimension at each level.

  4. For each ship, the algorithm explores a path from the root to a leaf node containing that ship. While multiple ships might share parts of their paths, each ship contributes at most O(log(max(m, n))) nodes to the recursion tree.

  5. Since we have C ships total, and each contributes O(log(max(m, n))) work, the total time complexity is O(C Γ— log(max(m, n))).

Space Complexity: O(log(max(m, n)))

The space complexity is determined by the maximum depth of the recursion stack. Since we divide the search space in half at each recursive level (using midx and midy), the maximum recursion depth is O(log(max(m, n))), where m and n represent the dimensions of the initial rectangle. This occurs when we need to drill down from the largest dimension to a single cell.

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

Common Pitfalls

1. Incorrect Quadrant Boundaries

One of the most common mistakes is incorrectly defining the boundaries when dividing the rectangle into quadrants. This can lead to:

  • Overlapping regions: Counting the same ship multiple times
  • Gaps between quadrants: Missing ships that fall in the gaps

Example of incorrect division:

# WRONG - Creates overlapping quadrants at the midpoint
top_right_quadrant = search_area(top_right, Point(mid_x, mid_y))
top_left_quadrant = search_area(Point(mid_x, top_y), Point(left_x, mid_y))
# Ships at (mid_x, mid_y) would be counted multiple times!

Correct approach:

# Ensure no overlap by using mid_x + 1 and mid_y + 1 for appropriate quadrants
top_right_quadrant = search_area(top_right, Point(mid_x + 1, mid_y + 1))
top_left_quadrant = search_area(Point(mid_x, top_y), Point(left_x, mid_y + 1))
bottom_left_quadrant = search_area(Point(mid_x, mid_y), bottom_left)
bottom_right_quadrant = search_area(Point(right_x, mid_y), Point(mid_x + 1, bottom_y))

2. Missing the Early Termination Check

Forgetting to check hasShips() before recursing deeper wastes API calls and can exceed the 400-call limit.

Incorrect implementation:

def search_area(top_right, bottom_left):
    # WRONG - No early termination check
    if left_x == right_x and bottom_y == top_y:
        # This might return 1 even for empty points!
        return 1 if sea.hasShips(top_right, bottom_left) else 0
  
    # Continues to divide even for empty areas
    mid_x = (left_x + right_x) >> 1
    mid_y = (bottom_y + top_y) >> 1
    # ... recursive calls

Correct approach:

def search_area(top_right, bottom_left):
    # Check for ships FIRST
    if not sea.hasShips(top_right, bottom_left):
        return 0  # Don't recurse into empty areas
  
    # Only then check if it's a single point
    if left_x == right_x and bottom_y == top_y:
        return 1  # We know there's a ship here from the check above

3. Incorrect Base Case Logic

The base case must verify that we've reached a single point AND that point contains a ship (already verified by the early termination check).

Common mistake:

# WRONG - Doesn't ensure we're at a single point
if sea.hasShips(top_right, bottom_left):
    return 1  # This could be a large area with multiple ships!

Correct approach:

# First check if area has ships (done earlier)
# Then check if it's a single point
if left_x == right_x and bottom_y == top_y:
    return 1  # Single point with a ship

4. Integer Division Pitfall

When calculating midpoints, ensure proper integer division to avoid floating-point coordinates.

Potential issue:

# In Python 3, this returns a float if not careful
mid_x = (left_x + right_x) / 2  # WRONG - returns float

Correct approaches:

mid_x = (left_x + right_x) // 2  # Integer division
# OR
mid_x = (left_x + right_x) >> 1  # Bitwise shift (faster)

5. Not Handling Edge Cases

Failing to handle invalid rectangles where bottom_left is actually above or to the right of top_right.

Missing validation:

def search_area(top_right, bottom_left):
    # WRONG - No validation
    if not sea.hasShips(top_right, bottom_left):
        return 0
    # This might crash or behave unexpectedly for invalid rectangles

Correct approach:

def search_area(top_right, bottom_left):
    # Validate rectangle first
    if left_x > right_x or bottom_y > top_y:
        return 0  # Invalid rectangle
  
    if not sea.hasShips(top_right, bottom_left):
        return 0

6. Order of Operations in Base Case

The order of checks matters for efficiency and correctness.

Inefficient ordering:

# WRONG ORDER - Checks single point before checking if ships exist
if left_x == right_x and bottom_y == top_y:
    return 1 if sea.hasShips(top_right, bottom_left) else 0
    # Wastes an API call for the base case

Optimal ordering:

# Check for ships first (prunes empty branches)
if not sea.hasShips(top_right, bottom_left):
    return 0

# Then check if single point (no additional API call needed)
if left_x == right_x and bottom_y == top_y:
    return 1

By avoiding these pitfalls, the solution efficiently finds all ships while minimizing API calls and ensuring correctness.

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

What data structure does Breadth-first search typically uses to store intermediate states?


Recommended Readings

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

Load More