Skip to content

Flip Tool

  • #Project
  • #Algorithm
Read time: 3 minutes
Zane LI
Zane LI

Flip Tool Problem πŸ”—

ID: flip πŸ”—

Time limit: 10 seconds πŸ”—

Orso is using some simple software to create 2D images. Unfortunately, the software has stopped working correctly. Only the flip tool is working. Orso has already drawn some pixels black and some pixels white (every pixel is either black or white). He wants to get back to a state where every pixel is the same colour.

The way the flip tool works is by first having the user select a pixel. After this pixel is selected, the flip tool finds the largest 8-connected region of pixels all of the same colour containing the selected pixel, then flips the colour of each pixel in that region (from white to black or from black to white). In other words, the flip tool starts at the selected pixel, then selects each adjacent pixel that shares an edge or corner with it, and which has the same colour as it, then it selects each of their neighbours of the same colour, and so on. All the pixels selected this way have their colour flipped.

What is the fewest number of flip tool applications required to get to a state where every pixel is the same colour?

Input πŸ”—

The first line of input contains two integers R (1 ≀ R ≀ 1 000) and C (1 ≀ C ≀ 1 000), which are the number of rows and columns of pixels in the image respectively. The next R lines describe the image. Each of these lines contains a string of length C containing only the characters β€˜.’ and β€˜x’. A β€˜.’ represents a white pixel and a β€˜x’ represents a black pixel.

πŸ”—

OutputπŸ”—

Display the minimum number of flip tool operations required.

Solution Steps:πŸ”—

  1. Find 8-connected components:
    • Use Breadth-First Search (BFS) to traverse the image and find all 8-connected components.
    • Each connected component consists of pixels with the same color that are 8-connected.
    • The bfs function starts from a pixel and finds all other pixels that are 8-connected to it and share the same color, forming a connected component.
    • The visited set records the visited pixels to avoid redundant processing.
  2. Build a supergraph:
    • Treat each connected component as a supernode.
    • Traverse the image, checking each pixel's 8 neighbors. If a neighbor belongs to a different connected component, add an edge between the two corresponding supernodes in the supergraph.
    • The constructed supergraph is essentially a tree. This is because connected supernodes are of different colors, and the supernodes are 8-connected components in the grid graph. Thus, the supergraph cannot contain cycles. (Skipping formal proof for simplicity.)
  3. Calculate the tree diameter:
    • Use two BFS traversals to calculate the diameter of the supergraph. The diameter of a tree is the length of the longest path in the tree.
    • The diameter can be calculated by picking an arbitrary node, finding the farthest node from it, and then finding the farthest node from this new node. The path length is the diameter.
  4. Calculate the minimum number of flips:
    • For a tree, the optimal strategy to flip colors is to start flipping from the middle of the tree's diameter.
    • Therefore, the minimum number of flips is the ceiling of half the diameter, i.e., (diameter + 1) // 2.
πŸ“ FlipTool.py
from collections import deque

# Calculate the diameter of a tree
def find_diameter(graph):
    # BFS to find the farthest node and its distance
    def bfs(start):
        dist = [-1] * len(graph)  # Initialize the distance array, all nodes set to -1
        dist[start] = 0  # Set the starting node's distance to 0
        queue = deque([start])  # Initialize the queue
        farthest = start  # Initialize the farthest node as the starting node
        while queue:
            node = queue.popleft()  # Dequeue the front element
            for neighbor in graph[node]:  # Traverse adjacent nodes
                if dist[neighbor] == -1:  # If the neighbor has not been visited
                    dist[neighbor] = dist[node] + 1  # Update the distance
                    queue.append(neighbor)  # Enqueue the neighbor
                    if dist[neighbor] > dist[farthest]:  # Update the farthest node
                        farthest = neighbor
        return farthest, dist[farthest]  # Return the farthest node and its distance

    # Choose an arbitrary node as the starting point
    start, _ = bfs(0)
    # Find the farthest node from the starting point
    end, diameter = bfs(start)
    return diameter

# Main algorithm
def solve(R, C, image):
    # BFS to find 8-connected components, merging all connected pixels into one set as a supernode
    def bfs(start_i, start_j, color):
        component = []  # Store the current connected component
        queue = deque([(start_i, start_j)])  # Initialize the queue
        visited.add((start_i, start_j))  # Mark the pixel as visited

        while queue:
            i, j = queue.popleft()  # Dequeue the front element
            component.append((i, j))  # Add the pixel to the current connected component

            # Traverse in 8 directions
            for di, dj in [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]:
                ni, nj = i + di, j + dj
                # Check boundary conditions and color match
                if 0 <= ni < R and 0 <= nj < C and image[ni][nj] == color and (ni, nj) not in visited:
                    queue.append((ni, nj))  # Enqueue the pixel
                    visited.add((ni, nj))  # Mark the pixel as visited

        return component

    visited = set()  # Set of visited pixels
    components = []  # List to store all connected components
    component_map = [[-1] * C for _ in range(R)]  # Map pixels to their connected component IDs
    # Traverse each pixel to find all 8-connected components
    for i in range(R):
        for j in range(C):
            if (i, j) not in visited:
                component = bfs(i, j, image[i][j])  # Find a connected component
                component_index = len(components)  # Index of the current connected component
                components.append(component)  # Add the component to the list
                for x, y in component:
                    component_map[x][y] = component_index  # Update the mapping

    # Build the connectivity between connected components
    graph = [set() for _ in range(len(components))]  # Initialize the graph
    for i in range(R):
        for j in range(C):
            current_component = component_map[i][j]  # Current pixel's connected component
            # Traverse in 8 directions
            for di, dj in [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]:
                ni, nj = i + di, j + dj
                if 0 <= ni < R and 0 <= nj < C:
                    neighbor_component = component_map[ni][nj]  # Neighbor pixel's connected component
                    if neighbor_component != current_component:
                        graph[current_component].add(neighbor_component)  # Add an edge
                        graph[neighbor_component].add(current_component)  # Add an edge

    # Convert sets to lists
    graph = [list(neighbors) for neighbors in graph]
    # Find the diameter of the supergraph (which is guaranteed to be a tree)
    diameter = find_diameter(graph)
    return (diameter + 1) // 2  # Return the minimum number of flips

# Read input
R, C = map(int, input().split())
image = [input().strip() for _ in range(R)]

# Generate random data for debugging
import random
def generate_random_input(max_R=20, max_C=20):
    R = random.randint(1, max_R)  # Randomly generate the number of rows
    C = random.randint(1, max_C)  # Randomly generate the number of columns
    image = []
    for _ in range(R):
        row = ''.join(random.choice('.x') for _ in range(C))  # Randomly generate the image
        image.append(row)

    print(f"{R} {C}")
    for row in image:
        print(row)

    return R, C, image

# Use randomly generated input
# R, C, image = generate_random_input()

# Output the result
print(solve(R, C, image))