BFS(너비 우선 탐색)
Queue 큐를 이용하여 가까운 순서대로 방문한다.
출발 정점을 기준으로 가까운 순서대로 차례 차례 방문한다. 예를 들어 순회을 0 정점에서 시작한다면
- 0 정점에서 거리 1 (간선 1 개를 타야 갈 수 있는) 👉 1, 3
- 0 정점에서 거리 2 (간선 2 개를 타야 갈 수 있는) 👉 2, 4
- 0 정점에서 거리 3 (간선 3 개를 타야 갈 수 있는) 👉 5
- 최종적으로 BFS 순회는 0 1 3 2 4 5 순으로 방문하게 된다.
그 때, 그 때 방문하는 정점마다 가장 가까운 거리의 정점들을 큐에 넣는다.(선입선출) 즉, 들어간지 오래된 정점부터 빠져나오게 된다. 따라서 DFS는 다양한 용도에 사용되며, BFS는 거의 최단 거리 칠 갖기 용도로 사용 된다!
[BFS 구현]
큐에 다음에 방문할 정점들을 추가한다. 즉, 다음에 방문할 정점들을 순서대로 예약한다.
using System;
using System.Collections.Generic;
namespace Excercise
{
class Graph
{
int[,] adj = new int[6, 6]
{
{ 0, 1, 0, 1, 0, 0 },
{ 1, 0, 1, 1, 0, 0 },
{ 0, 1, 0, 0, 0, 0 },
{ 1, 1, 0, 0, 1, 0 },
{ 0, 0, 0, 1, 0, 1 },
{ 0, 0, 0, 0, 1, 0 },
};
public void BFS(int start)
{
bool[] found = new bool[6]; // false로 초기화 되어 있음
Queue<int> q = new Queue<int>();
q.Enqueue(start);
found[start] = true;
while (q.Count > 0)
{
// 1. 방문
int now = q.Dequeue();
Console.WriteLine(now);
// 2. 나와 가까운 정점들 중 방문하지 않은 애가 있다면 큐에 추가하여 예약
for (int next = 0; next < 6; next++)
{
if (adj[now, next] == 0) // 연결 되어 있지 않으면 스킵
continue;
if (found[next]) // 이미 방문한 애라면 스킵
continue;
// 예약
q.Enqueue(next);
found[next] = true;
}
}
}
}
class Program
{
static void Main(string[] args)
{
Graph graph = new Graph();
graph.BFS(0); // 0 1 3 2 4 5
}
}
}
아래 과정을 반복한다.
1️⃣ 방문 int now = q.Dequeue();
👉 now 예약 큐에서 들어간지 가장 오래된 정점을 꺼내온다.
2️⃣ 현재 방문하고 있는 정점 now가 앞으로 방문할 수 있는 정점들을 큐에 넣어 예약하기
👉 모든 정점들을 검사한다. (next)
👉 now와 연결 되어 있는 곳이고 and 아직 방문하지 않는 정점이라면 해당 정점 next를 다음에 방문할 수 있도록 큐에 넣어 예약한다.
👉 found 를 이용하여 한번이라도 예약된 적이 있다는 것을 체크한다.
출발지를 기점으로 거리 순서대로 가까운 정점들을 차례대로 방문하게 된다. 예를 들어 graph.BFS(0)로 실행을 한다면 0 1 3 2 4 5 순으로 방문한다.
[BFS 순회한 노드들로부터 부모, 거리 정보 얻기]
using System;
using System.Collections.Generic;
namespace Excercise
{
class Graph
{
int[,] adj = new int[6, 6]
{
{ 0, 1, 0, 1, 0, 0 },
{ 1, 0, 1, 1, 0, 0 },
{ 0, 1, 0, 0, 0, 0 },
{ 1, 1, 0, 0, 1, 0 },
{ 0, 0, 0, 1, 0, 1 },
{ 0, 0, 0, 0, 1, 0 },
};
public void BFS(int start)
{
bool[] found = new bool[6]; // 한번이라도 예약된적이 있는지 여부
int[] parent = new int[6]; // 내 부모가 누구인지
int[] distance = new int[6]; // 내가 오기까지 얼마의 거리가 걸렸는지
Queue<int> q = new Queue<int>();
q.Enqueue(start);
found[start] = true;
parent[start] = start; // 출발지는 자기 자신이 부모라고 하자.
distance[start] = 0;
while (q.Count > 0)
{
// 1. 방문
int now = q.Dequeue();
Console.WriteLine(now);
// 2. 나와 가까운 정점들 중 방문하지 않은 애가 있다면 큐에 추가하여 예약
for (int next = 0; next < 6; next++)
{
if (adj[now, next] == 0) // 연결 되어 있지 않으면 스킵
continue;
if (found[next]) // 이미 한번이라도 예약된적이 있는 애라면 스킵
continue;
// 예약
q.Enqueue(next);
found[next] = true;
parent[next] = now;
distance[next] = distance[now] + 1;
}
}
}
}
class Program
{
static void Main(string[] args)
{
Graph graph = new Graph();
graph.BFS(0); // 0 1 3 2 4 5
}
}
}
found 👉 한번이라도 예약된적이 있는지
parent 👉 해당 인덱스에 대응하는 정점의 부모가 누구인지
distance 👉 출발지로부터 해당 인덱스에 대응하는 정점까지의 거리
// 예약
q.Enqueue(next);
found[next] = true;
parent[next] = now;
distance[next] = distance[now] + 1;
방문할 수 있는 정점 next 을 큐에 예약할 때 parent, distance도 함게 업데이트 한다.
- next의 부모는 now (now로부터 연결 정점을 찾은 것이므로)
- start로부터 next까지의 거리는 now까지의 거리인 distance[now]에서 1 더한다.
parent 👉 0 0 1 0 3 4
- 0 정점 부모 - 0
- 1 정점 부모 - 0
- 2 정점 부모 - 1
- 3 정점 부모 - 0
- 4 정점 부모 - 3
- 5 정점 부모 - 4
distance 👉 0 1 2 1 2 3
- 거리 0 - 출발지인 정점 0
- 거리 1 - 정점 1, 정점 3
- 거리 2 - 정점 2, 정점 4
- 거리 3 - 정점 5
이처럼 BFS를 통해 해당 정점까지의 최단 거리를 정보를 담을 수 있다. 즉 최단 거리 길 찾기에 사용된다.
[BFS를 사용한 미로 길 찾기]
미로 타일 또한 하나의 그래프로 표현할 수 있다. 타일 하나 하나를 정점(Vertex)으로 보고, 현재 타일로부터 해당 타일이 갈 수 있는 타일이라면 연결(Edge) 되는 것으로 표현할 수 있고, 갈 수 없는 벽이라면 연결이 되어 있지 않은 관계라고 생각해볼 수 있다.
[Player.cs - BFS]
public void Initialize(int posY, int posX, Board board)
{
PosX = posX;
PosY = posY;
_board = board;
BFS();
}
void BFS()
{
int[] deltaY = new int[] { -1, 0, 1, 0 };
int[] deltaX = new int[] { 0, -1, 0, 1 };
bool[,] found = new bool[_board.Size, _board.Size];
Pos[,] parent = new Pos[_board.Size, _board.Size];
Queue<Pos> q = new Queue<Pos>();
q.Enqueue(new Pos(PosY, PosX)); // 시작점
found[PosY, PosX] = true;
parent[PosY, PosX] = new Pos(PosY, PosX);
while(q.Count > 0)
{
Pos pos = q.Dequeue();
int nowY = pos.Y;
int nowX = pos.X;
for (int i = 0; i < 4; i++)
{
int nextY = nowY + deltaY[i];
int nextX = nowX + deltaX[i];
if (nextY < 0 || nextY >= _board.Size || nextX < 0 || nextX >= _board.Size)
continue;
if (_board.Tile[nextY, nextX] == Board.TileType.Wall)
continue;
if (found[nextY, nextX])
continue;
q.Enqueue(new Pos(nextY, nextX));
found[nextY, nextX] = true;
parent[nextY, nextX] = new Pos(nowY, nowX);
}
}
int y = _board.DestY;
int x = _board.DestX;
while (parent[y, x].Y != y || parent[y, x].X != x)
{
_points.Add(new Pos(y, x));
Pos pos = parent[y, x];
y = pos.Y;
x = pos.X;
}
_points.Add(new Pos(y, x));
_points.Reverse();
}
오른손 법칙과 마찬가지로 *Update 함수에서 플레이어의 위치를 프레임마다 결정하지 않고 미리 플레이어 위치 초기화 과정에서 미리 도착지까지 향하는 길(플레이어 위치들)을 결정해두고 리스트에 저장한다. Update 함수에서는 리스트에 있는 타일들을 꺼내면 되는 것이다.
[변수 설명]
found 👉 한번이라도 예약된적이 있는 타일 체크
parent 👉 해당 타일의 부모 (연결된 이전 노드)
q 👉 추후 방문할 정점들을 큐에 추가하여 예약. 자신의 가까운 정점들을 차례로 추가한다.
deltaY, deltaX
반시계 방향으로 Up, Left, Down, Right 순으로
- Up 방향으로 가기 위해선 Y 방향으로 deltaY[0](-1) 만큼, X 방향으로 deltaX[0](0) 만큼 이동하면 된다.
- Left 방향으로 가기 위해선 Y 방향으로 deltaY[1](0) 만큼, X 방향으로 deltaX[1](-1) 만큼 이동하면 된다
- Down 방향으로 가기 위해선 Y 방향으로 deltaY[2](1) 만큼, X 방향으로 deltaX[2](0) 만큼 이동하면 된다.
- Right 방향으로 가기 위해선 Y 방향으로 deltaY[3](0) 만큼, X 방향으로 deltaX[3](1) 만큼 이동하면 된다.
[처리 프로세스]
1. 시작하기
- q 큐에 시작 위치(인수로 들어온 PosX, PosY) 넣기
- found 일단 시작 위치는 방문 처리 true
- parent 시작 위치의 부모는 자기 자신
2. 큐가 빌 때까지, 즉 더 이상 예약 된 정점이 아무 것도 없을 때 까지 아래 과정을 반복한다.
1️⃣ 큐에서 꺼내기 (현재 방문 중) 👉 nowY, nowX
2️⃣ 다음에 방문할 수 있도록 큐에 예약하기
- 현재 방문 중인 nowY, nowX을 기준으로 4 개의 방향 Up, Left, Dowwn, Right 검사 (nextY, nextX) 즉 본인 기준 가장 가까운 정점을 검사하는 것과 같다. 본인 주변 4 방향 타일을 검사하는 것이다.
3. 런타임 에러 방지(인덱스 벗어나진 않는지 검사)
- nextY, nextX가 갈 수 있는 곳인지 검사 (그래프로 따지면 연결 되지 않은 곳인지를 검사)
- nextY, nextX가 방문 했던 곳인지 검사
4. 검사 통과 되었으면 방문 가능한 정점이므로 큐에 추가하여 예약하자.
- 큐에 nextY, nextX 추가
- nextY, nextX 미리 방문 체크
- nextY, nextX의 부모(연결된 이전 정점)는 nowY, nowX
여기까지 완료 하면 parent에 벽이 아닌 모든 타일들에 자신의 이전 정점 정보가 들어간다.
- 목적지를 시작으로 목적지부터 거꾸로 부모를 타고 타고 올라가며 시작점에 도달할 때까지 길을 리스트에 저장한다.
- 그 때 그 때 정점을 받문할 때마다 자신과 가장 가까운, 즉 바로 4 방향중에 하나로 연결된 정점들을 우선적으로 순서대로 방문하기 때문에 ⭐어떤 정점이던 간에 부모를 타고 타고 올라가면 출발지까지 최단 경로로 올라갈 수 있다는게 보장된다.⭐
- BFS 언제나 가까운 거리 순으로 방문했으므로 예를 들어 지금 정점5를 방문 중이라면 출발지부터 정점5까지 갈 수 있는 길 중 최단 거리다.
- 시작점은 부모가 자기 자신과 같으므로 부모와 자기 자신이 같을 때까지 반복하면 된다. 그게 바로 시작점에 도착한 것.
마지막으로 리스트를 Reverse 뒤집어 주면 출발지 부터 목적지까지의 최단 경로 타일들이 저장된 리스트가 되는 것이다.
- 마지막으로 _points.Add(new Pos(y, x)); 한번 더 추가해준 이유는 출발지 정점은 while 조건문에 위배되서 while문을 못 돌아서 리스트에 추가가 안되기 때문.
리스트를 순서대로 쭉 돌며 플레이어 위치를 업뎃해나가면 그게 바로 최단 경로.
[Player.cs - 전체 코드]
namespace Algorithm
{
class Pos
{
public Pos(int y, int x) { Y = y; X = x; }
public int Y;
public int X;
}
class Player
{
public int PosY { get; private set; }
public int PosX { get; private set; }
Random _random = new Random();
Board _board;
enum Dir // 반시계방향
{
Up = 0,
Left = 1,
Down = 2,
Right = 3
}
int _dir = (int)Dir.Up;
List<Pos> _points = new List<Pos>();
public void Initialize(int posY, int posX, Board board)
{
PosX = posX;
PosY = posY;
_board = board;
BFS();
}
void BFS()
{
int[] deltaY = new int[] { -1, 0, 1, 0 };
int[] deltaX = new int[] { 0, -1, 0, 1 };
bool[,] found = new bool[_board.Size, _board.Size];
Pos[,] parent = new Pos[_board.Size, _board.Size];
Queue<Pos> q = new Queue<Pos>();
q.Enqueue(new Pos(PosY, PosX)); // 시작점
found[PosY, PosX] = true;
parent[PosY, PosX] = new Pos(PosY, PosX);
while(q.Count > 0)
{
Pos pos = q.Dequeue();
int nowY = pos.Y;
int nowX = pos.X;
for (int i = 0; i < 4; i++)
{
int nextY = nowY + deltaY[i];
int nextX = nowX + deltaX[i];
if (nextY < 0 || nextY >= _board.Size || nextX < 0 || nextX >= _board.Size)
continue;
if (_board.Tile[nextY, nextX] == Board.TileType.Wall)
continue;
if (found[nextY, nextX])
continue;
q.Enqueue(new Pos(nextY, nextX));
found[nextY, nextX] = true;
parent[nextY, nextX] = new Pos(nowY, nowX);
}
}
int y = _board.DestY;
int x = _board.DestX;
while (parent[y, x].Y != y || parent[y, x].X != x)
{
_points.Add(new Pos(y, x));
Pos pos = parent[y, x];
y = pos.Y;
x = pos.X;
}
_points.Add(new Pos(y, x));
_points.Reverse();
}
void RightHand()
{
// 현재 바라보고 있는 방향을 기존으로 좌표 변화를 나타냄
int[] frontY = new int[] { -1, 0, 1, 0 };
int[] frontX = new int[] { 0, -1, 0, 1 };
int[] rightY = new int[] { 0, -1, 0, 1 };
int[] rightX = new int[] { 1, 0, -1, 0 };
_points.Add(new Pos(PosY, PosX));
// 목적지 도착하기 전에는 계속 실행
// 실시간으로 로직 돌리기 전에, 렌더링도 하기 전에, 미리 길을 찾아 보는 것이다.
// 현재 바라보는 방향이 어디냐에 따라 오른손 위치가 다름.(왼쪽을 보고 있는 상태라면 오른손은 절대 기준으로 위쪽일것)
while (PosY != _board.DestY || PosX != _board.DestX)
{
// 1. 현재 바라보는 방향을 기준으로 오른쪽으로 갈 수 있는지 확인
if (_board.Tile[PosY + rightY[_dir], PosX + rightX[_dir]] == Board.TileType.Empty)
{
// 오른쪽으로 가기
// 1. 오른쪽 방향으로 90 도 회전
_dir = (_dir - 1 + 4) % 4;
// 2. 앞으로 한 보 전진
PosY = PosY + frontY[_dir];
PosX = PosX + frontX[_dir];
_points.Add(new Pos(PosY, PosX));
}
// 2. 현재 바라보는 방향을 기준으로 앞 쪽으로 갈 수 있는지
else if (_board.Tile[PosY + frontY[_dir], PosX + frontX[_dir]] == Board.TileType.Empty)
{
// 앞으로 한 보 전진
PosY = PosY + frontY[_dir];
PosX = PosX + frontX[_dir];
_points.Add(new Pos(PosY, PosX));
}
else
{
// 왼쪽 방향으로 90 도 회전 해주고 다음 반복 하러 (반시계방향으로 여러 방향 따져봄)
_dir = (_dir + 1 + 4) % 4;
}
}
}
const int MOVE_TICK = 10; // 10밀리세컨즈 = 0.01 초 마다 움직이게
int _sumTick = 0;
int _lastIndex = 0;
public void Update(int deltaTick)
{
if (_lastIndex >= _points.Count)
return;
_sumTick += deltaTick;
if (_sumTick >= MOVE_TICK) // 이부분은 0.1초마다 실행
{
_sumTick = 0;
PosY = _points[_lastIndex].Y;
PosX = _points[_lastIndex].X;
_lastIndex++;
}
}
}
}
[BFS의 단점]
BFS는 모든 경로를 동일한 조건으로 이동할 수 있을 때만 사용이 가능하다. 즉 가중치 있는 그래프에선사용할 수 없다. 만약 상하좌우 뿐만이 아니라 대각선 이동도 가능하여 총 8 개의 방향으로 움직일 수 있다면 (즉, 연결되어 있는 정점이 8개라면) 상하좌우 이동 비용이 1 이라면 대각선 이동 비용은 Sqrt (2) 이기 때문에 대각선과 상하좌우 정점들이 서로 동등하지 못하다. 왜냐하면 BFS는 자신과 연결되어 있는 정점들을 모두 차례대로 동등하게 탐색하는데, 가중치가 다르므로 거리에 있어 모호해지기 때문이다.
따라서 가중치 있는 그래프는 다익스트아 최단 경로 알고리즘을 사용해 순회한다.
'공부 > 인프런 - Rookiss' 카테고리의 다른 글
Part 2-5-1. 트리 : 트리 이론과 구현 (0) | 2023.08.10 |
---|---|
Part 2-4-5. 그래프 : 다익스트라 최단 경로 알고리즘 (0) | 2023.08.09 |
Part 2-4-3. 그래프 : DFS(깊이 우선 탐색) (0) | 2023.08.09 |
Part 2-4-2. 그래프 : 그래프 이론, 생성 (0) | 2023.08.09 |
Part 2-4-1. 그래프 : 스택과 큐 (0) | 2023.08.09 |