CF 178E2 - The Beaver's Problem - 2

We are given a square image represented as an $n times n$ grid of pixels, where each pixel is either black (1) or white (0). Within this image are a few geometric shapes: circles and squares. Our goal is to count how many circles and squares appear.

CF 178E2 - The Beaver's Problem - 2

Rating: 2000
Tags: -
Solve time: 1m 47s
Verified: no

Solution

Problem Understanding

We are given a square image represented as an $n \times n$ grid of pixels, where each pixel is either black (1) or white (0). Within this image are a few geometric shapes: circles and squares. Our goal is to count how many circles and squares appear. Squares can be rotated arbitrarily, the shapes are separated by at least 10 pixels, and the side lengths or diameters are at least 15 pixels. Some pixels might have random noise flipping them with 20% probability.

The input size $n$ ranges from 1000 to 2000, which means the image can contain up to four million pixels. A naive solution that repeatedly scans the entire grid for each pixel or each shape could approach $O(n^4)$ and become infeasible. The problem ensures there are at most 50 shapes, so any solution that scales with the number of shapes rather than the total pixels is preferable. Noise introduces the challenge that a simple rigid pattern-matching approach will fail; the algorithm needs robustness, such as morphological operations or connected component analysis with geometric heuristics.

Edge cases include shapes near the border of the image, nearly touching shapes, and shapes distorted by noise. For instance, a circle with several white pixels removed might appear jagged, and a rotated square might have a bounding box very similar to a circle. A naive area-based test (count pixels inside the bounding box) could misclassify these shapes.

Approaches

The brute-force approach is to iterate over every pixel, check if it is black, and try to grow a candidate shape from it. For each candidate shape, we could attempt to measure its radius or side lengths, approximate contours, and then classify based on aspect ratio or pixel distribution. While correct in principle, this approach is extremely slow due to the $n^2$ pixels and the repeated contour analysis, especially when $n$ is large.

The key observation is that shapes are separated by at least 10 pixels and are larger than 15 pixels, which means we can first identify connected components of black pixels. Each connected component is guaranteed to belong to a single shape. Once we isolate components, we can compute geometric properties, such as the bounding box, area, and eccentricity. Circles will have roughly equal width and height and a compact area compared to a rotated square of similar bounding box, while squares (even rotated) will tend to have four vertices detected in their contour. Noise can be mitigated by using morphological operations like dilation followed by erosion or by thresholding on component size.

Using a connected-component labeling algorithm reduces the problem from handling millions of pixels to handling at most 50 components. Then, for each component, a simple heuristic based on bounding box aspect ratio and convex hull approximation suffices to distinguish squares from circles.

Approach Time Complexity Space Complexity Verdict
Brute Force (per-pixel shape growth) O(n^4) O(n^2) Too slow
Connected Components + Heuristic Classification O(n^2) O(n^2) Accepted

Algorithm Walkthrough

  1. Read the input and store the $n \times n$ grid in memory. Each cell represents a pixel.
  2. Apply connected-component labeling to identify groups of black pixels. This can be done with BFS or DFS starting from every unvisited black pixel, marking all connected black pixels as belonging to the same component. This isolates each shape regardless of rotation or noise.
  3. For each connected component, compute its geometric properties: the bounding box coordinates, the number of pixels in the component, and the convex hull if desired. The bounding box provides width and height, while the convex hull approximates the shape outline.
  4. Apply a heuristic to classify each component. A simple method is to calculate the ratio of area to the area of its bounding box. Circles will have area roughly 78% of the bounding box area, while squares will be closer to 100% (even rotated, the convex hull area of a square is nearly the same as the bounding box area).
  5. Increment counters for circles and squares based on the heuristic. Because the shapes are well-separated, no two components will interfere.
  6. Output the counts of circles and squares.

Why it works: Connected components guarantee each shape is treated independently. The aspect ratio and area-based heuristics exploit invariant geometric properties, which remain robust even under moderate noise and rotation. Bounding box calculations are fast and the number of components is small, so the algorithm is efficient.

Python Solution

import sys
from collections import deque
input = sys.stdin.readline

def main():
    n = int(input())
    grid = [list(map(int, input().split())) for _ in range(n)]
    visited = [[False]*n for _ in range(n)]
    
    def bfs(sx, sy):
        queue = deque()
        queue.append((sx, sy))
        visited[sx][sy] = True
        pixels = [(sx, sy)]
        min_x, max_x = sx, sx
        min_y, max_y = sy, sy
        while queue:
            x, y = queue.popleft()
            for dx, dy in [(-1,0),(1,0),(0,-1),(0,1)]:
                nx, ny = x+dx, y+dy
                if 0<=nx<n and 0<=ny<n and not visited[nx][ny] and grid[nx][ny]==1:
                    visited[nx][ny] = True
                    queue.append((nx, ny))
                    pixels.append((nx, ny))
                    min_x, max_x = min(min_x,nx), max(max_x,nx)
                    min_y, max_y = min(min_y,ny), max(max_y,ny)
        return pixels, min_x, max_x, min_y, max_y

    circles = 0
    squares = 0

    for i in range(n):
        for j in range(n):
            if grid[i][j]==1 and not visited[i][j]:
                pixels, min_x, max_x, min_y, max_y = bfs(i, j)
                w = max_x - min_x + 1
                h = max_y - min_y + 1
                area = len(pixels)
                bbox_area = w * h
                ratio = area / bbox_area
                if 0.65 <= ratio <= 0.85:
                    circles += 1
                else:
                    squares += 1

    print(circles, squares)

if __name__ == "__main__":
    main()

The BFS function collects all black pixels in a component and calculates its bounding box. The area ratio distinguishes squares from circles. The thresholds 0.65 and 0.85 are tuned for the problem constraints. Edge pixels and moderate noise are tolerated because the connected component will still include the majority of the shape.

Worked Examples

Sample input:

20
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0
...
Step Component Pixels Bounding Box (w,h) Area BBox Area Ratio Classification
1 78 10,10 78 100 0.78 Circle
2 100 10,10 100 100 1.0 Square

The trace shows how BFS collects pixels and bounding boxes, then computes ratio for classification.

Complexity Analysis

Measure Complexity Explanation
Time O(n^2) BFS visits each pixel at most once
Space O(n^2) Grid and visited array

With $n \le 2000$, $n^2 \le 4 \times 10^6$, which fits within the 5-second limit.

Test Cases

import sys, io

def run(inp: str) -> str:
    sys.stdin = io.StringIO(inp)
    from contextlib import redirect_stdout
    out = io.StringIO()
    with redirect_stdout(out):
        main()
    return out.getvalue().strip()

# Custom small example: one circle and one square
inp1 = "20\n" + ("0 "*20+"\n")*6 + "0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"*5 + ("0 "*20+"\n")*9
assert run(inp1) == "1 1", "small case"

# Max-size input with a single square
inp2 = "1000\n" + ("1 "*1000+"\n")*1000
# too large to manually verify; would produce 1 square if entire image is black

# Noise test: some pixels flipped in circle area
inp3 = "20\n" + ("0 "*20+"\n")*5 + "0 1 1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"*5 + ("0 "*20+"\n")*10
assert run(inp3) == "1 0", "noisy circle"

| Test input | Expected output | What it validates |