너비 우선 탐색!
항목 | BFS (너비 우선 탐색) | DFS (깊이 우선 탐색) |
---|---|---|
탐색 방향 | 가까운 노드부터 → 너비(가로)로 확장 | 가능한 한 깊이(세로)로 내려감 |
자료구조 | 큐 (Queue) 사용 | 스택(Stack) 사용 또는 재귀 함수 활용 |
방문 순서 | 레벨 순서 (거리 순) | 경로 따라 깊이 우선 |
주 용도 | 최단 거리 탐색, 레벨 순서 처리 | 경로 탐색, 백트래킹, 조합/순열 생성 |
시간/공간복잡도 | 둘 다 O(V + E) | 둘 다 O(V + E) |
직관적인 예시 | 미로에서 가장 빠른 길 찾기 | 미로에서 한 방향으로 끝까지 가보다가 돌아오는 방식 |
ㅇㅎ BFS는 동심원 DFS는 땅굴
BFS는 그래서 먼저 조건을 만족하는 게 최단 거리다.
DFS는 내가 먼저 조건 만족했다고 최단 거리가 보장 되는 건 아님. 옆으로 굴 파고 내려간 게 더 짧을 수 있으니까.
#include <bits/stdc++.h>
using namespace std;
#define X first
#define Y second
int dx[4] = {1, 0, -1, 0};
int dy[4] = {0, -1, 0, 1};
bool vis[100][100];
char board[100][100];
int N, ans1, ans2;
queue<pair<int, int> > q;
int bfs(int x, int y) {
if (vis[x][y]) return 0;
q.push({x, y});
vis[x][y] = 1;
while (!q.empty()) {
auto cur = q.front();
q.pop();
for (int dir = 0; dir < 4; ++dir) {
int nx = cur.X + dx[dir];
int ny = cur.Y + dy[dir];
if (nx < 0 || nx >= N || ny < 0 || ny >= N) continue;
if (vis[nx][ny]) continue;
if (board[cur.X][cur.Y] != board[nx][ny]) continue;
q.push({nx, ny});
vis[nx][ny] = 1;
}
}
return 1;
}
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> N;
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) cin >> board[i][j];
}
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) ans1 += bfs(i, j);
}
for (int i = 0; i < N; ++i) fill(vis[i], vis[i] + N, 0);
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j)
if (board[i][j] == 'G') board[i][j] = 'R';
}
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) ans2 += bfs(i, j);
}
cout << ans1 << ' ' << ans2;
}
와~~~ 이제 진짜 혼자 풀 수 있다
큐에 넣을 때 방문 체크 하는 것 주의!!
맨 처음에 큐에 넣을 때도 방문 체크를 해줘야 한다. 큐 push + 방문 true
는 세트!!
#include <bits/stdc++.h>
using namespace std;
#define X first
#define Y second
int dx[6] = {1, 0, -1, 0, 0, 0};
int dy[6] = {0, -1, 0, 1, 0, 0};
int dz[6] = {0, 0, 0, 0, 1, -1};
int board[100][100][100];
bool vis[100][100][100];
int N, M, H;
void bfs(int z, int x, int y) {
if (vis[z][x][y] || board[z][x][y] != 1) return;
queue<pair<int, pair<int, int> > > q;
q.push({z, {x, y}});
vis[z][x][y] = 1;
while (!q.empty()) {
auto cur = q.front();
q.pop();
for (int dir = 0; dir < 6; ++dir) {
int nz = cur.X + dz[dir];
int nx = cur.Y.X + dx[dir];
int ny = cur.Y.Y + dy[dir];
if (nz < 0 || nz >= H || nx < 0 || nx >= N || ny < 0 || ny >= M)
continue;
if (vis[nz][nx][ny] || board[nz][nx][ny]) continue;
q.push({nz, {nx, ny}});
board[nz][nx][ny] = board[cur.X][cur.Y.X][cur.Y.Y] + 1;
vis[nz][nx][ny] = 1;
}
}
}
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> M >> N >> H;
for (int i = 0; i < H; ++i) {
for (int j = 0; j < N; ++j) {
for (int k = 0; k < M; ++k) cin >> board[i][j][k];
}
}
for (int i = 0; i < H; ++i) {
for (int j = 0; j < N; ++j) {
for (int k = 0; k < M; ++k) bfs(i, j, k); // 시작점마다 개별 BFS
}
}
int ans = 0;
for (int i = 0; i < H; ++i) {
for (int j = 0; j < N; ++j) {
for (int k = 0; k < M; ++k) {
if (board[i][j][k] == 0) {
cout << -1;
return 0;
}
ans = max(ans, board[i][j][k]);
}
}
}
cout << ans - 1;
}
/*
5 3 1
1 -1 0 0 0
-1 -1 0 1 1
0 0 0 1 1
*/
왜 틀렸냐면! 이 문제는 시작점이 여러 개.
근데 시작점마다 bfs를 해서 문제.
예를 들어
5 3 1
1 -1 0 0 0
-1 -1 0 1(<- 요놈) 1
0 0 0 1 1
인 경우 (0, 1, 3)에서 첫 bfs가 일어날 거고 그 때
5 3 1
1 -1 3 2 3
-1 -1 2 1 1
5 4 3 1 1
이렇게 토마토가 전부 익어버릴 거임.
근데 맨 아랫줄 토마토들은 (0, 2, 3) 토마토에 의해서 익는 게 맞는데 먼저 선수를 쳐버리게 되는 거;; 그리고 오른쪽 윗모서리 애도.
5 3 1
1 -1 3 2 2
-1 -1 2 1 1
4 3 2 1 1
이렇게 되는 게 맞는 건데..
그래서 처음에 큐에 시작점을 모두 넣은 다음에
한 번만 bfs를 해줘야 됨.
그래야 시작점이 먼저 처리 되고 나서, 시작점을 기준으로 주변부 애들이 처리되면서 퍼져나갈 수 있음.
#include <bits/stdc++.h>
using namespace std;
int dx[6] = {1, 0, -1, 0, 0, 0};
int dy[6] = {0, -1, 0, 1, 0, 0};
int dz[6] = {0, 0, 0, 0, 1, -1};
int board[100][100][100];
bool vis[100][100][100];
int N, M, H;
queue<tuple<int, int, int> > q;
void bfs() {
while (!q.empty()) {
auto [z, x, y] = q.front();
q.pop();
for (int dir = 0; dir < 6; ++dir) {
int nz = z + dz[dir];
int nx = x + dx[dir];
int ny = y + dy[dir];
if (nz < 0 || nz >= H || nx < 0 || nx >= N || ny < 0 || ny >= M)
continue;
if (vis[nz][nx][ny] || board[nz][nx][ny]) continue;
q.push({nz, nx, ny});
board[nz][nx][ny] = board[z][x][y] + 1;
vis[nz][nx][ny] = 1;
}
}
}
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> M >> N >> H;
for (int i = 0; i < H; ++i) {
for (int j = 0; j < N; ++j) {
for (int k = 0; k < M; ++k) {
cin >> board[i][j][k];
if (board[i][j][k] == 1) q.push({i, j, k});
}
}
}
bfs();
int ans = 0;
for (int i = 0; i < H; ++i) {
for (int j = 0; j < N; ++j) {
for (int k = 0; k < M; ++k) {
if (board[i][j][k] == 0) {
cout << -1;
return 0;
}
ans = max(ans, board[i][j][k]);
}
}
}
cout << ans - 1;
}
#include <bits/stdc++.h>
using namespace std;
int t, w, h;
char board[1000][1000];
int distF[1000][1000]; // fire
int distP[1000][1000]; // person
int dx[4] = {1, 0, -1, 0};
int dy[4] = {0, -1, 0, 1};
void solve() {
queue<pair<int, int> > fire, person;
cin >> w >> h;
// 입력 & 초기화
for (int i = 0; i < h; ++i) {
for (int j = 0; j < w; ++j) {
cin >> board[i][j];
distF[i][j] = distP[i][j] = -1;
if (board[i][j] == '*') {
fire.push({i, j});
distF[i][j] = 0;
} else if (board[i][j] == '@') {
person.push({i, j});
distP[i][j] = 0;
}
}
}
// 불 bfs
while (!fire.empty()) {
auto [x, y] = fire.front();
fire.pop();
for (int dir = 0; dir < 4; ++dir) {
int nx = x + dx[dir], ny = y + dy[dir];
if (nx < 0 || nx >= h || ny < 0 || ny >= w) continue;
if (distF[nx][ny] != -1 || board[nx][ny] == '#') continue;
fire.push({nx, ny});
distF[nx][ny] = distF[x][y] + 1;
}
}
// 사람 bfs
while (!person.empty()) {
auto [x, y] = person.front();
person.pop();
for (int dir = 0; dir < 4; ++dir) {
int nx = x + dx[dir], ny = y + dy[dir];
if (nx < 0 || nx >= h || ny < 0 || ny >= w) {
cout << distP[x][y] + 1 << '\n';
return;
}
if (distP[nx][ny] != -1 || board[nx][ny] == '#') continue;
if (distF[nx][ny] != -1 && distP[x][y] + 1 >= distF[nx][ny])
continue;
person.push({nx, ny});
distP[nx][ny] = distP[x][y] + 1;
}
}
cout << "IMPOSSIBLE\n";
}
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> t;
while (t--) solve();
}
처음엔 더 깔끔하게 푸는 방법 없을까 고민했는데
불 bfs를 먼저 구하고 사람 bfs를 구하는 방법밖에 없음.
이때! 사람 bfs를 구할 때 가려는 자리에 불이 있다면(-> distF[nx][ny] != -1) 그리고 불이 나보다 먼저 도착했으면(-> distP[x][y] + 1 >= distF[nx][ny]) 그건 q에 넣지 않아야 함.
distP[nx][ny]가 업데이트 된 상태가 아니니까 distP[x][y] + 1 로 조건을 비교해줘야 한다.
그리고 경계조건을 만나면 바로 return 해주면 된다. bfs 특성상 더 진행할 필요가 없음. 처음 경계 조건을 만날 때가 가장 짧은 탈출 거리임.
#include <bits/stdc++.h>
using namespace std;
int dx[4] = {1, 0, -1, 0};
int dy[4] = {0, -1, 0, 1};
char board[1000][1000];
int dist[1000][1000][2];
int n, m;
int bfs() {
queue<tuple<int, int, int> > q;
q.push({0, 0, 0});
dist[0][0][0] = dist[0][0][1] = 1;
while (!q.empty()) {
auto [x, y, broke] = q.front();
q.pop();
if (x == n - 1 && y == m - 1) return dist[x][y][broke];
for (int dir = 0; dir < 4; ++dir) {
int nx = x + dx[dir], ny = y + dy[dir];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
// 벽인데 아직 안 부쉈다면
if (board[nx][ny] && !broke && !dist[nx][ny][1]) {
dist[nx][ny][1] = dist[x][y][0] + 1;
q.push({nx, ny, 1});
}
// 벽이 아니라면
if (!board[nx][ny] && !dist[nx][ny][broke]) {
dist[nx][ny][broke] = dist[x][y][broke] + 1;
q.push({nx, ny, broke});
}
}
}
return -1;
}
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
cin >> board[i][j];
board[i][j] -= '0';
}
}
cout << bfs();
}
어렵다....
첨엔 부술 벽을 정해놓고 각 경우마다 bfs를 구한 다음에 min 값을 구했음. 당연히 시간 초과..
모든 벽에 대해서 BFS를 돌리면 O(NM * K)
풀이 핵심은 벽을 부수는 상태를 BFS에서 같이 들고 가는 것.
이를 위해 q에 {x, y, broke} 값을 넣어준다.
q.push({i, j, 0}) // 벽을 부수지 않았을 때
q.push({i, j, 1}) // 벽을 부수고 나서부터 쭉
// 벽인데 아직 안 부쉈다면
if (board[nx][ny] && !broke && !dist[nx][ny][1]) {
dist[nx][ny][1] = dist[x][y][0] + 1;
q.push({nx, ny, 1});
}
// 벽이 아니라면
if (!board[nx][ny] && !dist[nx][ny][broke]) {
dist[nx][ny][broke] = dist[x][y][broke] + 1;
q.push({nx, ny, broke});
}
그래서 이렇게 해주면 된다.
그리고 dist 배열도 두 버전으로 관리한다.
int dist[1000][1000][2];
dist[x][y][0] → 벽을 부수지 않고 (x, y)에 도달한 최단거리
dist[x][y][1] → 벽을 한 번 부수고 (x, y)에 도달한 최단거리
#include <bits/stdc++.h>
using namespace std;
int n, s[100001], state[100001], cnt;
const int NOT_VISITED = -1;
const int IN_CYCLE = 0;
void mark_cycle(int start) {
int cur = s[start];
state[cur] = IN_CYCLE;
++cnt;
while (cur != start) {
cur = s[cur];
state[cur] = IN_CYCLE;
++cnt;
}
}
void run() {
cin >> n;
cnt = 0;
for (int i = 1; i <= n; ++i) {
cin >> s[i];
state[i] = NOT_VISITED;
}
for (int i = 1; i <= n; ++i) {
if (state[i] != NOT_VISITED) continue;
int cur = i;
while (1) {
state[cur] = i;
cur = s[cur];
if (state[cur] == i) { // 사이클 발견
mark_cycle(cur);
break;
}
if (state[cur] != NOT_VISITED) break; // 이전 회차에서 탐색했었다면
}
}
cout << n - cnt << '\n';
}
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
int T;
cin >> T;
while (T--) run();
}
못 풀어서 바킹독님 풀이 보고 아이디어 얻어서 다시 풀었다.
bfs 문제집에 들어있었는데 bfs는 아니다🤔
방문 상태 배열을 사용하는 건 맞지만, 단순히 0과 1로는 부족하다.
이번 탐색이 어떤 탐색에서 시작된 건지, "탐색의 고유 번호"를 저장해야 한다.
예를 들어 i
번 학생에서 탐색을 시작하면, state[i] = i
로 저장한다.
그다음 지목한 학생을 따라가면서 다음 노드를 확인하는데,
이때 가능한 상황은 두 가지다:
state[cur] = i
를 쓰는 이유는
"같은 탐색 안에서 다시 만난 노드만 사이클"로 간주하기 위해서다.
사이클을 발견하기 위해 같은 곳을 또 방문했을 때 멈추는 조건문을 써야 하는데,
만약 이전 회차에서 방문했을 때와 이번 회차에서 방문했을 때 상태값이 같으면 오류가 생긴다.
왜냐면 이전 회차에서 방문한 노드인데 또 만났다고 사이클로 간주해 버리니까.
그리고 탐색이 끝났는데(방문을 했는데도) IN_CYCLE로 상태가 업데이트 되지 않았다면 걔는 사이클이 안 되는 거다. 다음 회차에서도 볼 필요가 없다.
에혀 방문 상태를 꼭 0,1 로만 생각하지 말기!!
복잡할 때 매직넘버 상수화 하기~~
그리고 팀이 안 된 사람 세는 거 = 전체 - 팀 된 사람
이렇게 생각할 수도 있다는 거 얻어가기~
#include <bits/stdc++.h>
using namespace std;
int n, m, cnt;
int board[301][301], sea[301][301], vis[301][301];
int dx[4] = {1, 0, -1, 0};
int dy[4] = {0, -1, 0, 1};
queue<pair<int, int> > q;
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
// 입력
cin >> n >> m;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
cin >> board[i][j];
// 빙산 count
if (board[i][j]) ++cnt;
}
}
for (int year = 1;; ++year) {
// 초기화
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (board[i][j] == 0)
sea[i][j] = 1;
else
q.push({i, j});
}
}
// 빙산 녹이기
while (!q.empty()) {
auto [x, y] = q.front();
q.pop();
for (int dir = 0; dir < 4; ++dir) {
int nx = x + dx[dir], ny = y + dy[dir];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
if (sea[nx][ny] && board[x][y]) {
--board[x][y];
if (!board[x][y]) --cnt;
if (!cnt) {
cout << 0;
return 0;
}
}
}
}
// 빙산 bfs
int tmp = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
vis[i][j] = 0;
if (board[i][j] && q.empty()) {
q.push({i, j});
vis[i][j] = 1;
++tmp;
}
}
}
while (!q.empty()) {
auto [x, y] = q.front();
q.pop();
for (int dir = 0; dir < 4; ++dir) {
int nx = x + dx[dir], ny = y + dy[dir];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
if (!board[nx][ny] || vis[nx][ny]) continue;
q.push({nx, ny});
vis[nx][ny] = 1;
++tmp;
}
}
// 확인
if (tmp != cnt) {
cout << year;
return 0;
}
}
}
으으 혼자 풀긴 했는데 너무 오래 걸렸다;;
이건 복잡하게 풀 수밖에 없는 것 같다...