Dividing the Power Grid into Two

 

BFS

  • Problem I noticed: my BFS fundamentals were weak.
  • → I decided I should go back and re-do some basic BFS problems.
from collections import deque

node = [[0] * 100 for _ in range(100)]
edge = [0 for i in range(100)]
visited = [0 for i in range(100)]

def bfs(graph, queue):
    global n
    global visited
    
    while queue:
        v = queue.popleft()
        # number of edges connected to vertex v
        print(v, end=' ')
        n = edge[v]
        for i in range(n, -1, -1):
            next = node[v][n]
            # if this vertex was already visited, skip
            if visited[next] == 1:
                continue
            # push unvisited vertices into the queue
            queue.append(next)
            visited[next] = 1
    return 

def solution(n, wires):
    global m
    global visited
    
    graph = []
    answer = []
    # build the graph
    for i in range(len(wires)):
        for w in wires:
            if wires[i] != w:
                node[w[0]][edge[w[1]]] = w[1]
                node[w[1]][edge[w[0]]] = w[0]
                edge[w[0]] += 1
                edge[w[1]] += 1

    result = 0
    # use BFS to count the number of connected components
    for i in range(1, n + 1):
        queue = deque()
        queue.append(i)
        if visited[i] == 0:
            visited[i] = 1
            bfs(edge, queue)
            result += 1
    print('result:', result)

n = 9
wires = [[1,3],[2,3],[3,4],[4,5],[4,6],[4,7],[7,8],[7,9]]
solution(n, wires)

# 1 0 2 3 4 5 6 7 8 9 result: 8

Why the heck is it suddenly visiting vertex 0? 😂 That’s so random… What’s going on here?

And where is this result: 8 even coming from?..


from collections import deque

def bfs(graph, queue, node, edge):
    global n
    global visited
    
    while queue:
        v = queue.popleft()
        # number of edges connected to vertex v
        # print(v, end=' ')
        n = edge[v]
        for i in range(n, -1, -1):
            next = node[v][i]
            # if this cell was already visited, skip
            if visited[next] == 1:
                continue
            # push unvisited vertices into the queue
            queue.append(next)
            visited[next] = 1
    return 

def solution(n, wires):
    global m
    global visited
    
    graph = []
    answer = []
    # build the graph
    for i in range(len(wires)):
        node = [[0] * 100 for _ in range(100)]
        edge = [0 for i in range(100)]
        visited = [0 for i in range(100)]
        # pick one wire to cut
        for w in wires:
            # draw the graph with one wire removed
            if wires[i] != w:
                node[w[0] - 1][edge[w[1]]] = w[1] - 1
                node[w[1] - 1][edge[w[0]]] = w[0] - 1
                edge[w[0] - 1] += 1
                edge[w[1] - 1] += 1
                
            result = 0
            # use BFS to count the number of connected components
            for j in range(0, n):
                queue = deque()
                queue.append(j)
                if visited[j] == 0:
                    visited[j] = 1
                    bfs(edge, queue, node, edge)
                    result += 1
            print(result)
           # answer.append(result)
    print(answer)

n = 9
wires = [[1,3],[2,3],[3,4],[4,5],[4,6],[4,7],[7,8],[7,9]]
solution(n, wires)

Now it does traverse, but I still don’t know how many connected chunks I actually have yet…

My “strong suspicion” is that the bug is hidden here:


node[w[0] - 1][edge[w[1]]] = w[1] - 1
node[w[1] - 1][edge[w[0]]] = w[0] - 1
edge[w[0] - 1] += 1
edge[w[1] - 1] += 1


This setup looks more like something you’d do for DFS than for clean adjacency storage in general, so it becomes a problem.

From the figure, vertex 3 is connected to 3 neighbors, but node[v] shows only vertex 4 connected. That’s where the fundamental bug was.

divide-1

divide-2

Root cause found

edge[w[0]] += 1
edge[w[1]] += 1
node[w[0]][edge[w[0]]] = w[1]
node[w[1]][edge[w[1]]] = w[0]


The bug came from how I was storing information into the node array. After fixing that, BFS works cleanly.

Perfect BFS code (without cutting wires)

from collections import deque

def bfs(queue, node, edge, visited, n):
    connected = 0

    while queue:
        v = queue.popleft()
        # count how many vertices are in this connected component
        # print(v, end=' ')
        connected += 1
        k = edge[v]
        for i in range(1, k + 1):
            next = node[v][i]
            # if already visited, skip
            if visited[next] == 1:
                continue
            # enqueue unvisited vertices
            queue.append(next)
            visited[next] = 1
    return (connected, n - connected)

def solution(n, wires):
    
    node = [[0] * 100 for _ in range(100)]
    edge = [0 for i in range(100)]
    visited = [0 for i in range(100)]

    # build the graph
    for w in wires:
        edge[w[0]] += 1
        edge[w[1]] += 1
        node[w[0]][edge[w[0]]] = w[1]
        node[w[1]][edge[w[1]]] = w[0]

    result = 0
    answer = []
    # use BFS to find how many connected components there are
    for j in range(1, n):
        queue = deque()
        queue.append(j)
        if visited[j] == 0:
            visited[j] = 1
            c, u = bfs(queue, node, edge, visited, n)
            result += 1
            answer.append([c, u])
    print(answer)

n = 9
wires = [[1,3],[2,3],[3,4],[4,5],[4,6],[4,7],[7,8],[7,9]]
solution(n, wires)

Final version with wires cut (working solution)

from collections import deque

def bfs(queue, node, edge, visited, n):
    connected = 0

    while queue:
        v = queue.popleft()
        # number of edges connected to v
        # print(v, end=' ')
        connected += 1
        k = edge[v]
        for i in range(1, k + 1):
            next = node[v][i]
            # if already visited, skip
            if visited[next] == 1:
                continue
            # enqueue unvisited vertices
            queue.append(next)
            visited[next] = 1
    return (connected, n - connected)

def solution(n, wires):
    answer = []
    for i in range(len(wires)):
        node = [[0] * 100 for _ in range(100)]
        edge = [0 for i in range(100)]
        visited = [0 for i in range(100)]

        # build the graph
        for w in wires:
            # skip exactly one wire (the one we are “cutting”)
            if wires[i] != w:
                edge[w[0]] += 1
                edge[w[1]] += 1
                node[w[0]][edge[w[0]]] = w[1]
                node[w[1]][edge[w[1]]] = w[0]

        result = 0
        # use BFS to find the two connected components after cutting one wire
        for j in range(1, n):
            queue = deque()
            queue.append(j)
            if visited[j] == 0:
                visited[j] = 1
                c, u = bfs(queue, node, edge, visited, n)
                result += 1
                val = abs(c - u)
                print(f'val = {val}')
                answer.append(val)
    
    return min(answer)


But: with this approach, I got 84.6% accuracy, and there were runtime errors on 2 test cases. Probably because I’m doing BFS way more often than necessary.

How to make it better

Instead of building edge and node arrays manually, just use an adjacency list and append neighbors directly.

Here all edge weights are 1, so BFS is naturally suitable.


import sys
from collections import defaultdict, deque

# Breadth-first search
def bfs(a, b, wire_node, n):
    que = deque()
    que.append(a)
    visited = [0] * (n + 1)
    visited[a] = 1
    
    cnt = 0
    while que:
        tmp = que.popleft()
        for next_wire in wire_node[tmp]:
            # skip the one specific edge we are “cutting”
            if tmp == a and next_wire == b:
                continue
            if visited[next_wire] == 0:
                visited[next_wire] = 1
                que.append(next_wire)
                cnt += 1
                
    return cnt
                     
def solution(n, wires):
    # build graph (power grid nodes)
    wire_node = defaultdict(list)
    for a, b in wires:
        wire_node[a].append(b)
        wire_node[b].append(a)
        
    # try cutting each wire
    min_val = sys.maxsize  # initialize with a huge value
    for wire in wires:
        cut1, cut2 = wire
        val = abs(bfs(cut1, cut2, wire_node, n) - bfs(cut2, cut1, wire_node, n))
        if val < min_val:
            min_val = val
    
    return min_val


Basic Problems (BFS/DFS warm-up) Ice Cream (counting components in a grid)

from collections import deque

def bfs(i, j, table, visited):
    queue = deque()
    if table[i][j - 1] == 1:
        queue.push([i, j])

def solution():
    n, m = map(int, input().split())
    # The two lines below actually aren’t necessary.
    table = [[0] * (m + 2) for _ in range(n + 2)]
    visited = [[0] * m for _ in range(n)]
    # Table setting – and honestly, this is way more complicated than needed.
    for i in range(0,  n + 2):
        for j in range(0, n + 2):
            if i == 0 or i == n + 1 or j == 0 or j == n + 1:
                table[i][j] = 2
            else:    
                num = int(input())
                table[i][j] = num
    # The simpler way:
    for i in range(n):
        graph.append(list(map(int, input())))
    # The full table is already given as input, so there’s no need
    # to construct it via (i, j) indices manually.
    # Just iterate over it:
    for i in range(n):
        for j in range(m):
            if visited[i][j] == 0:
                bfs(i, j, table, visited)

In this problem, the inputs are not “edge lists” but the grid itself, so it’s better to change the mindset and treat it as a grid traversal problem.


from collections import deque

def dfs(i, j, n, m, graph):
    # NOTE: This is actually a DFS using recursion, but I originally named it dfs.
    if i <= -1 or i >= n or j <= -1 or j >= m:
        return False

    if graph[i][j] == 0:
        graph[i][j] = 1
        dfs(i + 1, j, n, m, graph)
        dfs(i - 1, j, n, m, graph)
        dfs(i, j + 1, n, m, graph)
        dfs(i, j - 1, n, m, graph)
        return True

def solution():
    n, m = map(int, input().split())
    graph = []
    for i in range(n):
        graph.append(list(map(int, input())))
    # Since we already have the full grid, there’s no need to reconstruct it by (i, j).
    result = 0
    for i in range(n):
        for j in range(m):
            if dfs(i, j, n, m, graph) == True:
                result += 1
    print(result)
    return result

solution()


Difference between the two codes:

In the first version, I added “padding” around the grid to generalize the logic, but that’s not necessarily a good idea.

The second version uses boundary checks directly (i <= -1 or i >= n etc.), which is simpler and more standard.

I actually wanted to solve this with BFS, but I ended up using DFS first. So I tried BFS too:


from collections import deque
from re import L

visited = [[0] * 100 for _ in range(100)]

def bfs(graph, x, y, queue):
    global n
    global m
    global visited
    dx = [-1, 1, 0, 0]
    dy = [0, 0, -1, 1]
    depth = 0

    while queue:
        v = queue.popleft()
        print(v, end=' ')
        for i in range(4):
            nx = v[0] + dx[i]
            ny = v[1] + dy[i]
            # If out of bounds, ignore
            if nx < 0 or nx >= n or ny < 0 or ny >= m:
                continue    
            # Skip already visited cells
            if visited[nx][ny] == 1:
                continue
            if graph[nx][ny] == 0:
                queue.append([nx, ny])
                visited[nx][ny] = 1
                depth = depth + 1
    return 

def solution():
    global n
    global m
    global visited
    n, m = map(int, input().split())

    graph = []
    for i in range(n):
        graph.append(list(map(int, input())))
    # Again, the grid is already given.
    result = 0 
    # Iterate through all cells
    for i in range(n):
        for j in range(m):
            queue = deque()
            queue.append([i, j])
            # This condition was crucial for solving it
            if visited[i][j] == 0 and graph[i][j] == 0:
                visited[i][j] = 1
                bfs(graph, i, j, queue)
                result += 1
    print(result)
    return

solution()

This time I solved it with BFS. In practice, the difference between DFS and BFS isn’t huge here; it depends more on how the problem is framed.

Maze (Shortest Path)

For shortest path problems with uniform edge weights, BFS is usually the best choice.

If all edges have the same cost, BFS increments the distance level by level.

DFS instead goes all the way down one path before checking others, which is inefficient for shortest path.


from collections import deque

def bfs(graph, x, y, queue):
    global n
    global m

    dx = [-1, 1, 0, 0]
    dy = [0, 0, -1, 1]
    depth = 0

    while queue:
        v = queue.popleft()
        print(v, end=' ')
        for i in range(4):
            nx = v[0] + dx[i]
            ny = v[1] + dy[i]
            if nx == n - 1 and ny == m - 1: 
                print(depth)
                return depth
            # out of bounds
            if nx < 0 or nx >= n or ny < 0 or ny >= m:
                continue    
            # already visited
            if visited[nx][ny] == 1:
                continue
            if graph[nx][ny] == 1:
                queue.append([nx, ny])
                visited[nx][ny] = 1
                depth += 1

    pass

def solution():
    global n
    global m
    global visited
    n, m = map(int, input().split())

    graph = []
    for i in range(n):
        graph.append(list(map(int, input())))
    # The grid is given directly as input.
    visited = [[0] * m for _ in range(n)]
    queue = deque()
    queue.append([1, 1])
    visited[1][1] = 1
    bfs(graph, 1, 1, queue)
    return

solution()

Again, BFS is naturally suited for shortest paths in grids with uniform move cost.