📍 <프로그래밍 대회에서 배우는 알고리즘 문제 해결 전략(a.k.a. 종만북)>으로 공부한 내용을 정리한 게시글입니다.
그래프 G(V, E)
V(Vertex, 정점)
E(Edge, 간선)
vector<list<int>> adjacent;
vector<list<pair<int, int>>> adjacent; // 가중치 그래프
adjacent[i]
: 정점 i와 간선을 통해 연결된 정점들의 번호를 저장하는 연결 리스트
vector<vector<bool>> adjacent;
vector<vector<int>> adjacent; // 가중치 그래프
adjacent[i, j]
: 정점 i에서 정점 j로 가는 간선이 있는지를 나타내는 불린 값 변수
그래프의 모든 정점을 발견하는 방법
현재 정점과 인접한 간선들을 하나씩 검사하다가, 아직 방문하지 않은 정점으로 향하는 간선이 있다면 그 간선을 무조건 따라감. 이 과정에서 더이상 갈 곳이 없는 막힌 정점에 도달하면 포기하고, 마지막에 따라왔던 간선을 따라 뒤로 돌아감 -> 재귀 호출
vector<vector<int>> adj; // 그래프의 인접 리스트
vector<bool> visited; // 각 정점을 방문했는지 - visited[0] == true이면 0번 정점은 방문했음
void dfs(int here) {
cout << "DFS visits " << here << "\n";
visited[here] = true;
for(int i = 0; i < adj[here].size(); ++i){
int there = adj[here][i];
if(!visited[there) // 아직 방문하지 않은 경우 방문
dfs(there);
}
}
void dfsAll() {
visited = vector<bool> (adj.size(), false);
for(int i = 0; i < adj.size(); ++i)
if(!visited[i]) dfs(i);
}
dfs()
: 현재 정점과 인접한 정점을 순차적으로 방문
dfsAll()
: 그래프가 하나의 컴포넌트로 이루어지지 않은 경우에도 모든 정점 방문
dfs(u)
를 수행하면 u에서부터 간선들을 통해 갈 수 있는 모든 정점을 방문
-> visited[]
를 참조하면 u에서 갈 수 있는지 확인 가능(true인 경우 연결되어 있음)
dfs()
함수는 시작한 정점에서 갈 수 있는 모든 정점을 방문 -> 같은 컴포넌트에 속한 점은 모조리 방문
-> dfsAll()에서 dfs()의 호출 횟수를 세면 그래프가 몇 개의 컴포넌트로 나뉘어 있는지 확인 가능
의존성이 있는 작업들이 주어질 때, 이들을 어떤 순서로 수행해야 하는지 계산
의존성 그래프
- 각 작업을 정점으로 표현, 작업 간의 의존 관계를 간선으로 표현한 방향 그래프
- 사이클이 없는 방향 그래프(DAG)
모든 의존성이 만족되려면, 모든 간선이 왼쪽에서 오른쪽으로 가야함
이러한 DAG의 정점을 배열하는 문제를 위상 정렬이라고 함
dfsAll()
을 수행하며 dfs()
가 종료할 때마다 현재 정점의 번호를 기록dfsAll()
이 종료한 뒤 기록된 순서를 뒤집음그래프의 모든 간선을 정확히 한 번씩 시자나서 시작점으로 돌아오는 경로 찾기
무방향 그래프에서 오일러 서킷이 존재하기 위한 조건
- 모든 정점이 짝수점(정점의 차수가 짝수)
❔ 방향 그래프에서는 나가는 각 정점에서 들어오는 간선의 수와 나가는 간선의 수가 같아야 함- 간선들이 하나의 컴포넌트에 포함
// adj[i][j] : i와 j 사이의 간선의 수
vector<vector<int>> adj;
void getEulerCircuit(int here, vector<int>& circuit) {
for(int there = 0; there < adj.size() ; ++there) {
while(adj[here][there] > 0) {
adj[here][there]--; // 양쪽 간선을 모두 지움
adj[there][here]--;
getEulerCircuit(there, circuit);
}
}
curcuit.push_back(here);
}
📍 오일러 트레일(시작점과 끝점이 다른 경우) 구하기
점 a에서 시작해서 b에서 끝나는 오일러 트레일을 찾는 경우, a와 b 사이에 간선 (b, a)를 추가한 뒤 오일러 서킷을 찾고 (b, a) 간선을 지워 서킷을 끊으면 됨
트리 간선 : 스패닝 트리에 포함된 간선
순방향 간선 : 스패닝 트리의 선조에서 자손으로 연결되지만 트리 간선이 아닌 간선
역방향 간선 : 스패닝 트리의 자손에서 선조로 연견되는 간선
교차 간선 : 그 외 나머지 간선
✔️ 간선 구분을 위해서는 발견 순서 정보를 저장해야함
vector<vector<int>> adj;
// discovered[i] : i번 정점의 발견 순서
// finished[i] : dfs(i)가 종료했으면 1, 아니면 0
vector<int> discovered, finished;
int counter; // 지금까지 발견한 정점의 수
void dfs(int here) {
discovered[here] = counter++;
for(int i = 0; i < adj[here].size(); ++i) {
int there = adj[here][i];
// 아직 방문한 적 없다면 방문
if(discovered[there] == -1) {
cout << "tree edge"<< "\n";
dfs(there);
}
// there이 here보다 늦게 발견됐다면 there은 here의 후손
else if(discovered[here] < discovered[there])
cout << "forward edge" << "\n";
// dfs[there]이 아직 끝나지 않았다면 there은 here의 선조
else if(finished[there] == 0)
cout << "back edge" << "\n";
// 나머지 경우는 모두 교차간선
else cout << "cross edge" << "\n";
}
finished[here] = 1;
}