좋아요! 백준 1671번, **상어의 저녁식사 문제**를 자세히 다뤄보겠습니다.
이 문제를 정리해 드리고, 예제 입출력과 정답 코드(Java), 그리고 한 줄 한 줄 디버깅 수준의 자세한 해설을 준비할게요.
여러 상어가 있는데, 상어는 다른 상어를 잡아먹을 수 있습니다. 상어의 정보 (크기, 속도, 지능)가 주어질 때:
상어 A가 상어 B를 잡아먹을 수 있는 조건:
- 상어 A가 상어 B보다 크기, 속도, 지능이 모두 크거나 같으면 잡아먹을 수 있다.
- 만약 상어의 정보가 완전히 같으면, 번호가 낮은 상어가 높은 상어를 잡아먹을 수 있다.
목표: 잡아먹히지 않고 살아남을 수 있는 상어의 최소 수를 출력.
5
1 2 3
2 2 2
2 2 2
3 3 3
9 8 7
2
이분 매칭(Bipartite Matching) 문제로 바꿀 수 있다.
최대 유량 또는 이분 매칭을 이용해 최대 잡아먹을 수 있는 쌍을 찾는다.
살아남을 수 있는 상어 수 = N - 최대 매칭 수.
이 문제를 **자바(Java)**로 해결해 보겠습니다.
그리고 한 줄 한 줄 디버깅 수준의 아주 자세한 설명을 곁들일게요!
import java.io.*;
import java.util.*;
class Shark {
int size, speed, intel;
Shark(int s, int sp, int i) {
this.size = s;
this.speed = sp;
this.intel = i;
}
}
public class Main {
static int N;
static Shark[] sharks;
static ArrayList<Integer>[] graph;
static int[] match;
static boolean[] visited;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
N = Integer.parseInt(br.readLine().trim()); // 상어 수 N을 읽음
sharks = new Shark[N]; // 상어 정보를 담을 배열
for (int i = 0; i < N; i++) {
String[] input = br.readLine().split(" ");
int s = Integer.parseInt(input[0]);
int sp = Integer.parseInt(input[1]);
int i2 = Integer.parseInt(input[2]);
sharks[i] = new Shark(s, sp, i2); // 상어 정보 초기화
}
graph = new ArrayList[N]; // 그래프 초기화
for (int i = 0; i < N; i++) graph[i] = new ArrayList<>();
// 잡아먹을 수 있는 경우의 수를 그래프로 연결
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (i == j) continue;
if (canEat(sharks[i], sharks[j], i, j)) { // i가 j를 잡아먹을 수 있는가?
graph[i].add(j); // 간선 연결
}
}
}
match = new int[N]; // 잡아먹히는 상대를 표시
Arrays.fill(match, -1); // -1로 초기화
int result = 0; // 최대 잡아먹을 수 있는 쌍 수
for (int i = 0; i < N; i++) {
for (int k = 0; k < 2; k++) { // 같은 상어가 2번 잡을 수 있게 함
visited = new boolean[N]; // 방문 초기화
if (dfs(i)) result++;
}
}
System.out.println(N - result); // 살아남은 상어 수 출력
}
static boolean canEat(Shark a, Shark b, int i, int j) {
// a가 b를 잡아먹을 수 있는지 검사
if (a.size >= b.size && a.speed >= b.speed && a.intel >= b.intel) {
// 완전히 동일한 경우에는 index가 낮은 쪽이 잡아먹음
if (a.size == b.size && a.speed == b.speed && a.intel == b.intel && i > j) {
return false;
}
return true;
}
return false;
}
static boolean dfs(int node) {
if (visited[node]) return false; // 이미 방문한 경우 false 반환
visited[node] = true; // 현재 정점을 방문 표시
for (int next : graph[node]) {
if (match[next] == -1 || dfs(match[next])) {
// next를 아직 안 잡았거나 이미 잡힌 상어를 다른 상어가 잡을 수 있으면
match[next] = node;
return true;
}
}
return false;
}
}
이 부분은 초보자 눈높이에 맞춰 한 줄 한 줄 디버깅 느낌으로 자세히 씁니다.
💡 우리가 위의 예제 입력(
5\n1 2 3\n2 2 2\n2 2 2\n3 3 3\n9 8 7\n)을 넣고 디버깅해봅시다.
N = Integer.parseInt(br.readLine().trim());
N=5. 상어 수를 읽습니다.sharks = new Shark[N];
for (int i = 0; i < N; i++) {
...
sharks[i] = new Shark(s, sp, i2);
}
i=0일 때 sharks[0] = (1,2,3),i=1일 때 sharks[1] = (2,2,2) ... 이렇게 차례대로 5마리 채웁니다.for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (i==j) continue;
if (canEat(sharks[i], sharks[j], i, j)) {
graph[i].add(j);
}
}
}
graph[i].add(j).for (int i = 0; i < N; i++) {
for (int k = 0; k < 2; k++) {
visited = new boolean[N];
if (dfs(i)) result++;
}
}
dfs(i)로 먹을 수 있는 상대 찾기.static boolean dfs(int node) {
if (visited[node]) return false;
visited[node] = true;
for (int next : graph[node]) {
if (match[next] == -1 || dfs(match[next])) {
match[next] = node;
return true;
}
}
return false;
}
System.out.println(N - result);
✅ 위의 코드와 같이 동작하면, 남은 상어 수(2)가 출력되며 문제의 정답과 일치합니다.
좋아요! 🎯
지금 보고 계신 부분은, 입력값을 한 줄씩 읽고 각 상어의 크기(s), 속도(sp), 지능(i2)을 Shark 객체에 담아 sharks` 배열에 채우는 부분이에요.
이 부분을 실제 예제 입력을 가지고 디버깅해 볼게요.
5
1 2 3
2 2 2
2 2 2
3 3 3
9 8 7
상어 수 N = 5
우리는 i = 0부터 i = 4까지 루프를 돌며 br.readLine() → split() → parseInt() → sharks[i] = new Shark(...)를 하게 됩니다.
br.readLine() → "1 2 3"input = ["1","2","3"]s = 1, sp = 2, i2 = 3sharks[0] = new Shark(1,2,3);🐟 sharks[0]: { size=1, speed=2, intel=3 }
br.readLine() → "2 2 2"input = ["2","2","2"]s = 2, sp = 2, i2 = 2sharks[1] = new Shark(2,2,2);🐟 sharks[1]: { size=2, speed=2, intel=2 }
br.readLine() → "2 2 2"input = ["2","2","2"]s = 2, sp = 2, i2 = 2sharks[2] = new Shark(2,2,2);🐟 sharks[2]: { size=2, speed=2, intel=2 }
br.readLine() → "3 3 3"input = ["3","3","3"]s = 3, sp = 3, i2 = 3sharks[3] = new Shark(3,3,3);🐟 sharks[3]: { size=3, speed=3, intel=3 }
br.readLine() → "9 8 7"input = ["9","8","7"]s = 9, sp = 8, i2 = 7sharks[4] = new Shark(9,8,7);🐟 sharks[4]: { size=9, speed=8, intel=7 }
| Index | size | speed | intel |
|---|---|---|---|
| 0 | 1 | 2 | 3 |
| 1 | 2 | 2 | 2 |
| 2 | 2 | 2 | 2 |
| 3 | 3 | 3 | 3 |
| 4 | 9 | 8 | 7 |
이렇게 sharks 배열이 채워지며, 나중에 canEat 검사나 매칭할 때 이용하게 됩니다.
좋아요! 🎯 이번엔 아래 부분을 예제 데이터를 바탕으로 한 줄 한 줄 디버깅해보겠습니다.
앞서 정리한 대로:
| Index | size | speed | intel |
|---|---|---|---|
| 0 | 1 | 2 | 3 |
| 1 | 2 | 2 | 2 |
| 2 | 2 | 2 | 2 |
| 3 | 3 | 3 | 3 |
| 4 | 9 | 8 | 7 |
이 함수의 핵심:
if (a.size >= b.size && a.speed >= b.speed && a.intel >= b.intel) {
// 모두 크거나 같으면 잡아먹을 수 있음
// 단, (a,b) 완전히 동일하다면 index가 낮은 쪽만 잡아먹을 수 있음
}
graph[0]에는 아무것도 안 들어감.graph[1].add(2);graph[1] = [2]graph[2].add(1);graph[2] = [1]graph[3].add(0);graph[3].add(1);graph[3].add(2);graph[3] = [0,1,2]graph[4].add(0);graph[4].add(1);graph[4].add(2);graph[4].add(3);graph[4] = [0,1,2,3]| i | graph[i] (먹을 수 있는 상어 목록) |
|---|---|
| 0 | [] |
| 1 | [2] |
| 2 | [1] |
| 3 | [0,1,2] |
| 4 | [0,1,2,3] |
이중 for 루프:
i==j 건너뛴다.
canEat() 조건 검사:
graph[i].add(j)로 관계 저장.
이 정보는 나중에 이분 매칭(DFS) 단계에서 누가 누구를 잡을 수 있는지를 탐색할 수 있게 합니다.
좋아요! 🎯 이제 dfs(int node) 메서드를 예제 상황을 기준으로 한 줄 한 줄 디버깅해 드릴게요.
이 부분은 **이분 매칭 알고리즘(Bipartite Matching)**의 핵심 부분이라, “잡아먹기 쌍”을 찾아가는 과정을 디버깅 느낌으로 아주 자세히 보여드릴 거예요.
dfs(node) 함수의 역할이 함수의 목적:
node가 잡을 수 있는 상어들(graph[node] 안의 원소들) 중,match[next] == -1),match[next] != -1)를 다른 상어에게 넘길 수 있으면,node가 next를 잡도록 연결(match[next] = node)하고 true 반환!graph[0] = []graph[1] = [2]graph[2] = [1]graph[3] = [0,1,2]graph[4] = [0,1,2,3]그리고 초기에:
match = [-1, -1, -1, -1, -1]; // 아무도 안 잡힌 상태
visited = new boolean[N]; // dfs 호출마다 false로 초기화
dfs(1) 호출visited[1] = false에서 시작
visited[1] = true; 표시
graph[1] = [2], 그러므로 next = 2 검사
match[2] == -1 → 아직 안 잡힘!match[2] = 1로 잡기 등록dfs(1) 반환값 = true
👉 상어 1이 상어 2를 잡음!
dfs(2) 호출visited[2] = false에서 시작
visited[2] = true;
graph[2] = [1], next = 1 검사
match[1] == -1 → 아직 안 잡힘match[1] = 2; 등록반환 true
👉 상어 2가 상어 1 잡음!
dfs(3) 호출visited[3] = false에서 시작
visited[3] = true;
graph[3] = [0,1,2] 검사 시작
next = 0
match[0] == -1 → 안 잡혔으니 잡기match[0] = 3; 등록dfs(4) 호출visited[4] = false에서 시작
visited[4] = true;
graph[4] = [0,1,2,3] 검사 시작
next = 0
match[0] == 3 (이미 상어 3이 잡고 있음)
dfs(match[0]) = dfs(3) 호출
하지만 visited[3]을 dfs 호출 직전에 false로 초기화하기 때문에 dfs(3)에서 검사
visited[3] = true; 설정
graph[3] 검사 → (0,1,2)
next=0 → 이미 잡음, dfs(3) 안에서 dfs(3) 호출되므로 visited[3]=true라 false
next=1 → match[1]=2 → dfs(2) 호출
next=2 → match[2]=1 → dfs(1) 호출
dfs(3)에서 잡을 수 있는 다른 경로 없음 → false 반환
dfs(3) false 반환 받았으므로 next=0에서 실패
next = 1
match[1] == 2 잡힌 상태
dfs(2) 호출 → visited[2]=false 초기화된 상태
visited[2]=true; graph[2]=[1]
dfs(2) 반환 false
잡기 실패
next = 2
match[2] == 1 잡힌 상태
dfs(1) 호출
visited[1]=false 초기화된 상태
visited[1]=true; graph[1]=[2]
dfs(1) 반환 false
next = 3
match[3] == -1 아직 안 잡힘👉 상어 4가 상어 3 잡음!
이 dfs() 함수는:
이 과정을 통해 최대 잡아먹을 수 있는 쌍을 확장하게 되고,
최종적으로 match 배열의 잡힌 수만큼 살아남지 않는 상어 수가 계산됩니다.
🎯 이렇게 디버깅을 해보면 dfs의 역할을 이해하기 좋아요:
좋은 질문이에요! 🎯
이건 이분 매칭의 특성 때문인데, 하나씩 잘 짚어드릴게요.
dfs(node)에서 graph[node] 안의 next를 하나씩 검사하다가, next가 이미 다른 상어에게 잡혔으면, 그 상어(match[next])가 다른 곳을 잡을 수 있는지를 재귀(dfs(match[next]))로 탐색해봄.dfs(1), dfs(2), dfs(3))에서는 대부분 "빈 자리(match[next] == -1)"를 바로 잡아서 재귀 호출이 적었고,dfs(4) 때는 이미 많은 상어가 잡힌 상태여서 잡히지 않은 자리(-1)가 거의 없음 → 기존 잡힌 상어를 다른 자리로 옮겨주기 위해 dfs()를 여러 번 호출하게 된 거예요.예를 들어:
match[next])가 다른 걸 잡도록 자리 비우기를 부탁해야 함.dfs(match[next])를 호출하여 "혹시 다른 걸 잡을 수 있나?"를 물어보는 거죠.이렇게 초반에는 match[next] == -1 상황이 많았어요.
dfs(4) 호출할 때쯤이면 match[] 대부분 채워져 있음.match[next] != -1이므로,dfs(match[next]) 호출을 여러 번 하게 됨.이번 호출이 유난히 많은 재귀를 하는 건, 잡을 자리가 거의 남지 않아 이미 잡힌 상어의 자리를 다른 상어로 밀어내야만 하기 때문이에요.
그래서 dfs가 연속 호출되며 깊게 탐색하게 되는 거죠.