본문 바로가기

Algorithm/H. Trees & Graphs

111. Grid & Flood Fill

그리드(격자, Grid)는 특수한 형태의 그래프로, $n$차원 좌표공간에 존재하는 점으로 표현되는 정점들과 정점들 사이를 잇는 간선으로 이루어져 있다. $n$은 대체로 $2$이며 가끔씩 $n$이 다른 값인 문제도 존재한다. 또한 점 대신 칸으로 이루어진 그리드가 등장하는 경우도 많은데, 각 칸을 그 중심점으로 바꾸면 정점과 간선으로 이루어진 그리드로 쉽게 변환이 가능하므로 이것은 별로 문제가 되지 않는다. 그리드의 좌표를 인덱스(Index)라고 하는 경우도 많다.

 

그리드에서 정점 사이를 잇는 간선은 보통 서로 인접한 칸들 사이에는 모두 존재한다. 일반적으로 그리드에서 두 칸이 인접하다는 것은 이렇게 이해할 수 있다: 정점 $\text{A}$의 좌표가 $(a_1, a_2, \ldots, a_n)$이고 정점 $\text{B}$의 좌표가 $(b_1, b_2, \ldots, b_n)$일 때, $\text{A}$와 $\text{B}$가 인접할 경우 다음 식이 성립한다.

$$|a_1 - b_1| + |a_2 - b_2| + \ldots + |a_n - b_n| = 1$$

이 식의 좌변을 보면 각각의 항이 절댓값으로 더해지고 모든 좌표는 정수로 표현되므로 $(n - 1)$개의 항이 $0$이고 나머지 한 항이 $1$일 때만 식이 성립한다는 것을 확인할 수 있다.

 

어떤 문제에서는 대각선 방향에 존재하는 칸들 사이에도 간선이 있는데, 이것 역시 식으로 이해할 수 있다. 인접한 정점과 대각선 방향에 있는 정점들 사이에 간선이 존재하는 그리드의 정점 $\text{A}$와 정점 $\text{B}$ 사이에 간선이 존재할 경우 다음 식이 성립한다.

$$\max(|a_1 - b_1|, |a_2 - b_2|, \ldots, |a_n - b_n|) = 1$$

물론 $n$이 $3$ 이상일 경우 대각선 방향이라는 표현이 모호해지기 때문에 $3$차원 이상의 그리드에서는 이런 표현이 잘 사용되지 않는다.

 

대부분의 문제는 그리드의 좌표 범위에 제한이 있다. 일반적으로 모든 축의 좌표가 $0$ 이상의 정수인 범위로 제한되는 그리드가 주어지는 경우가 많지만 가끔씩 그렇지 않은 경우도 있으며 이때는 음수 좌표의 처리를 편하게 하기 위해서 모든 좌표에 일정한 오프셋(Offset)을 더해서 좌표가 음수가 되지 않게 만든 뒤 문제를 푸는 경우가 많다.


플러드 필(Flood Fill)[각주:1]은 그리드에서 실행되는 탐색 방법으로, 어떤 칸과 연결된 영역을 DFS나 BFS로 찾는다. 원리는 기본 DFS, BFS와 크게 다르지 않다. 일반적으로 영역 자체만을 구할 경우는 DFS를 사용하는 것이 조금 더 편하며, BFS가 강제되는 경우 BFS를 사용하면 된다.

 

플러드 필이 일반 DFS, BFS와 차이를 보이는 부분은 구현이다. 일반 DFS, BFS와 달리 플러드 필에서는 어떤 정점 사이에 간선이 존재하는지가 기본적으로 정의되어 있으므로 인접 행렬이나 인접 리스트 등에 간선 정보를 저장할 필요가 없고, 재귀호출을 하는 부분도 조금 달라진다. DFS를 사용하는 경우의 가장 간단한 구현은 대충 다음과 같다.

#import<bits/stdc++.h>
using namespace std;
int m, n, a[1005][1005];
void flood_fill(int x, int y)
{
    if(x < 1 || x > n || y < 1 || y > m || a[x][y])return;
    a[x][y] = 1;
    flood_fill(x - 1, y);
    flood_fill(x, y - 1);
    flood_fill(x, y + 1);
    flood_fill(x + 1, y);
}
int main()
{
    int x, y;
    cin >> n >> m >> x >> y;
    flood_fill(x, y); // Flood Fill 시작 정점
}

위 코드를 실행하면 칸 $(x, y)$에서 플러드 필을 시작해서 $n \times m$ 크기의 그리드의 각 칸을 $1$로 채운다. flood_fill 함수 안에는 가장 기본적인 세 가지 요소가 포함되어 있다.

  • 더 탐색하지 않는 경우에 대한 조건: 인덱스가 그리드의 범위를 벗어났거나, 이미 방문한 칸을 다시 방문했을 경우 이 칸에서 더 탐색하지 않고 되돌아간다. 위 코드에는 존재하지 않지만, 어떤 조건에 의해 이동할 수 없는 칸을 방문했을 경우도 탐색하지 않고 되돌아가야 한다.
  • 방문 체크: 더 탐색을 하는 경우, 이 칸을 방문했다는 것을 체크하고 넘어간다.
  • 인접한 칸으로의 재귀호출: 인접한 칸들을 재귀적으로 탐색한다.

$2$차원 그리드에서 위 코드와 같이 상하좌우로 인접한 $4$칸이 서로 연결되는 방식을 $4$방향 연결(${\color{#FF0000}4}$-Connectivity)이라고 하고 이 방식으로 탐색을 진행하는 것을 $4$방향 탐색(${\color{#FF0000}4}$-Way Search)이라고 한다. 어떤 $2$차원 그리드는 대각선으로도 서로 연결이 되는데, 이 방식을 $8$방향 연결(${\color{#FF0000}8}$-Connectivity)이라고 하고 이 방식으로 탐색을 진행하는 것을 $8$방향 탐색(${\color{#FF0000}8}$-Way Search)이라고 한다. $8$방향 탐색을 하는 경우 아래와 같이 재귀호출을 $8$번 할 수도 있고,

#import<bits/stdc++.h>
using namespace std;
int m, n, a[1005][1005];
void flood_fill(int x, int y)
{
    if(x < 1 || x > n || y < 1 || y > m || a[x][y])return;
    a[x][y] = 1;
    flood_fill(x - 1, y - 1);
    flood_fill(x - 1, y);
    flood_fill(x - 1, y + 1);
    flood_fill(x, y - 1);
    flood_fill(x, y + 1);
    flood_fill(x + 1, y - 1);
    flood_fill(x + 1, y);
    flood_fill(x + 1, y + 1);
}
int main()
{
    int x, y;
    cin >> n >> m >> x >> y;
    flood_fill(x, y); // Flood Fill 시작 정점
}

코드가 길어지는 것을 방지하기 위해서 다음과 같이 작성하기도 한다.

#import<bits/stdc++.h>
using namespace std;
int m, n, dx[8] = {-1, -1, -1, 0, 0, 1, 1, 1}, dy[8] = {-1, 0, 1, -1, 1, -1, 0, 1}, a[1005][1005];
void flood_fill(int x, int y)
{
    if(x < 1 || x > n || y < 1 || y > m || a[x][y])return;
    a[x][y] = 1;
    for(int i = 0; i < 8; i++)flood_fill(x + dx[i], y + dy[i]);
}
int main()
{
    int x, y;
    cin >> n >> m >> x >> y;
    flood_fill(x, y); // Flood Fill 시작 정점
}

========================================================================================================================

#import<bits/stdc++.h>
using namespace std;
int m, n, dx[3] = {-1, 0, 1}, dy[3] = {-1, 0, 1}, a[1005][1005];
void flood_fill(int x, int y)
{
    if(x < 1 || x > n || y < 1 || y > m || a[x][y])return;
    a[x][y] = 1;
    for(int i = 0; i < 3; i++)
    {
        for(int j = 0; j < 3; j++)
        {
            if(i != 1 || j != 1)flood_fill(x + dx[i], y + dy[j]); // if문은 생략할 수 있음
        }
    }
}
int main()
{
    int x, y;
    cin >> n >> m >> x >> y;
    flood_fill(x, y); // Flood Fill 시작 정점
}

위의 마지막 코드를 BFS를 사용하는 방식으로 바꾸면 대략 다음과 같이 된다.

#import<bits/stdc++.h>
using namespace std;
int dx[3] = {-1, 0, 1}, dy[3] = {-1, 0, 1}, a[1005][1005];
queue<pair<int, int>>Q;
int main()
{
    int m, n, tx, ty, x, y;
    cin >> n >> m >> x >> y;
    a[x][y] = 1;
    for(Q.push({x, y}; Q.size(); Q.pop())
    {
        x = Q.front().first;
        y = Q.front().second;
        for(int i = 0; i < 3; i++)
        {
            for(int j = 0; j < 3; j++)
            {
                tx = x + dx[i];
                ty = y + dy[j];
                if(0 < tx && tx <= n && 0 < ty && ty <= m && !a[tx][ty])
                {
                    a[tx][ty] = 1;
                    Q.push({tx, ty});
                }
            }
        }
    }
}

BFS를 사용해서 구현할 때는 같은 칸이 큐에 여러 번 들어가지 않게 하는 것이 중요하다. 일반적인 그래프에서 BFS를 구현할 때에 비해 플러드 필을 BFS로 구현할 때 이 실수가 발생하기 쉽다. 이런 이유로 칸이 큐에 들어가는 시점에 그 칸의 방문 체크를 한다.

 

[연습문제: DFS, BFS 모두 사용이 가능한 경우]

 

BOJ 1012. 유기농 배추 (Silver II)

더보기

$1$로 이루어진 컴포넌트의 개수를 세는 문제이다. 4963번도 같은 방식으로 풀 수 있다.

 

BOJ 2468. 안전 영역 (Silver I)

더보기

비의 높이가 $1$인 경우부터 $100$인 경우까지를 하나씩 확인하면서 컴포넌트의 개수의 최대값을 찾는다.

 

BOJ 2583. 영역 구하기 (Silver I)

더보기

직사각형을 그리드에 직접 표시하고 각 컴포넌트의 크기를 구한다.

 

BOJ 2667. 단지번호붙이기 (Silver I)

더보기

컴포넌트들의 크기를 구하고 정렬해서 출력한다.

 

BOJ 2636. 치즈 (Gold V)

더보기

플러드 필로 매 시간마다 어디에 있는 치즈들이 녹는지 체크하고, 치즈가 녹은 후 안쪽의 구멍이 새로 외부와 연결되는 경우가 있는지를 체크하면서 답을 구한다.

 

BOJ 10026. 적록색약 (Gold V)

더보기

이번에는 그리드 전체를 컴포넌트로 분리해서 컴포넌트의 개수를 세야 한다. 빨강과 초록을 같은 색으로 간주하는 경우는 미리 빨강과 초록을 한 색으로 통일하는 등의 전처리 이후 구하면 된다.

 

BOJ 2573. 빙산 (Gold IV)

더보기

빙산의 각 칸의 높이를 줄이고 컴포넌트가 분리되었는지 확인하는 과정을 반복한다.

 

BOJ 2638. 치즈 (Gold IV)

더보기

2636번보다 약간 더 어려운 버전이다. 외부와 한 변만 접촉한 치즈는 녹지 않는다는 조건이 추가되었다. 이것만 고려해서 시뮬레이션을 해 주면 된다.

 

BOJ 16946. 벽 부수고 이동하기 4 (Gold II)

더보기

이동할 수 있는 곳들로 연결된 컴포넌트들의 크기를 구한 뒤 벽이 있는 각각의 칸에 대해 인접한 컴포넌트들의 크기를 더해서 답을 구한다. 이때 같은 컴포넌트를 여러 번 세지 않도록 주의해야 한다.

 

CF 1344B. Monopole Magnets (2000)

더보기

어떤 컴포넌트가 어떤 줄에서 불연속적으로 나타나는 경우가 존재한다면 $-1$, 존재하지 않는다면 컴포넌트의 개수를 출력한다.

 

[연습문제: BFS를 사용해야 하는 경우]

 

BOJ 2178. 미로 탐색 (Silver I)

더보기

BFS로 출발지에서 목적지까지의 최단 경로의 길이를 구한다.

 

BOJ 7576. 토마토 (Silver I)

더보기

이미 익은 토마토의 인덱스를 모두 큐에 넣고 나서 BFS를 실행한다.

 

BOJ 2206. 벽 부수고 이동하기 (Gold IV)

더보기

출발지와 도착지에서 각각 BFS를 실행한 결과를 저장한다. 이렇게 하면 벽을 한 개 부술 때의 최단 경로의 길이를 비교적 간단하게 구할 수 있다.

 

BOJ 3055. 탈출 (Gold IV)

더보기

물이 차 있는 칸의 인덱스를 모두 큐에 넣은 다음 고슴도치가 있는 칸의 인덱스를 큐에 넣는다. 이후 BFS를 실행하는데 물이 차 있는 칸과 인접한 칸은 물의 영역, 고슴도치가 갈 수 있는 칸과 인접한 칸은 고슴도치의 영역으로 구분한다. BFS의 실행이 끝났을 때 비버의 굴이 고슴도치의 영역에 포함되었으면 이동이 가능하고 그렇지 않으면 이동이 불가능하다.

 

CF 1105D. Kilani and the Game (1900)

더보기

플레이어의 초기 위치를 번호 순서대로 큐에 넣고 BFS를 돌리는데 플레이어마다 이동 속도가 존재하므로 이것을 잘 처리해야 한다.

 

CF 1349C. Orac and Game of Life (2000)

더보기

각각의 칸이 처음 몇 턴 동안 색이 유지되는지를 구하면 되는데, 만약 어떤 칸이 어떤 인접한 칸과 색이 같다면 그 칸은 첫 턴부터 색이 변한다. 이런 칸을 모두 큐에 넣은 뒤 BFS를 돌리면 나머지 칸이 처음 몇 던 동안 색이 유지되는지도 구할 수 있다. 이 값은 처음에 큐에 넣은 칸들로부터의 최단 거리와 같다.

 

CF 1316D. Nash Matrix (2000)

더보기

먼저 무한루프에 걸리지 않는 칸들을 처리한다. 종료되는 칸이 자기 자신인 칸들을 모두 찾아서 Blocked zone으로 설정하고 이 칸들에서 BFS를 시작해서 그 칸에서 종료되는 인접한 칸들을 차례로 처리한다. 이후 무한루프들을 처리하는데 이쪽은 인접한 두 칸을 아무거나 골라서 서로 상대 칸으로 가도록 하고 나머지 칸들을 잘 처리하면 된다. 만약 처리되지 않는 칸이 있으면 답은 INVALID이다.

 

[연습문제: $3$차원 이상의 그리드]

 

BOJ 7569. 토마토 (Silver I)

더보기

7576번의 $3$차원 버전이다.

 

→ solved.ac tag: flood_fill


  1. 시드 필(Seed Fill)이라는 표현도 존재하지만 거의 사용되지 않는다. [본문으로]

'Algorithm > H. Trees & Graphs' 카테고리의 다른 글

113. Diameter of Tree  (0) 2021.12.29
112. Bipartite Graph  (0) 2021.12.27
110. Breadth First Search  (0) 2021.12.22
109. Depth First Search  (0) 2021.11.19
108. Postorder Traversal  (0) 2021.11.17