정점 u에서 시작하여 자기 자신으로 돌아오는 경로가 있는 것을 Cyclic이라 하며, 그 경로를 Cycle이라 한다.
<방향 그래프에서 사이클의 존재 여부 확인하기>
모든 정점에 대해 DFS를 돌려 자기 자신으로 돌아오는 경로가 있다면 사이클이 존재하는 것이다.
두번째 방법은 역방향 간선을 찾으면서 DFS를 하는 것이다.
vector<vector<int>> adj;
vector<bool> vis, finished;
bool hasCycle;
void DFS(int node)
{
vis[node] = true;
for (int i = 0; i < adj[node].size(); ++i) {
int next = adj[node][i];
if (!vis[next])
DFS(next);
else if (finished[next] == false) { // next가 이미 방문했지만, 종료되지 않는 정점이면
hasCycle = true;
}
}
finished[node] = true;
}
<무방향 그래프에서 사이클의 존재 여부 확인하기>
무방향 그래프에서는 간선의 방향이 정해져있지 않다.
부모를 제외한 정점들 중 이미 방문했고, 방문순서가 더 빠른 정점이 존재하게 되면 사이클이 있다고 판단할 수 있다.
void go(int u, int p) {
for (int i = 0; i < adj[u].size(); i++) {
int v = adj[u][i];
if (p != v) {
if (d[v] == 0) {
d[v] = d[u] + 1;
go(v, u);
}
else if (d[u] > d[v]) {
printf("벡에지입니다");
}
}
}
}
<사이클 내의 정점들을 표시>
이를 위해 parents
라는 부모 배열을 만들어 정점의 부모를 저장.
parents[node]
는 node
정점의 부모를 담는다.부모를 찾다가 부모와 사이클의 시작점(역방향 간선이 가리키는 정점)에 도달하게 되면 멈춘다.
사이클에 속한 정점들은 isCycle
배열에 표시해준다.
#include <iostream>
#include <vector>
using namespace std;
vector<vector<int>> adj;
vector<int> parent;
vector<bool> vis, finished, isCycle;
bool isGraphCyclic;
int countCyclicVertices;
void denoteCycle(int node, int next)
{
isCycle[node] = true;
countCyclicVertices++;
if (node == next)
return;
denoteCycle(parent[node], next);
}
void DFS(int node)
{
vis[node] = true;
for (int i = 0; i < adj[node].size(); ++i) {
int next = adj[node][i];
if (!vis[next]) {
parent[next] = node;
DFS(next);
}
else if (finished[next] == false) {
isGraphCyclic = true; // 그래프가 사이클을 갖는지 체크해준다.
denoteCycle(node, next); // 사이클에 포함된 정점들을 표시해준다.
}
}
finished[node] = true;
}
간선은 트리 간선, 역행 간선, 순행 간선, 교차 간선 총 4개로 분류된다.
그래프의 간선은 DFS를 어떤 정점에서 시작하는지, 어떤 순서로 방문할것인지에 따라 다른 트리가 생성되고, 따라서 간선의 구분이 달라질 수 있다.
트리 간선(Tree edge)
정점 v가 간선 (u, v)를 통해 처음 발견되었다면, (u, v)는 트리간선이다.
간단하게는, 스패닝 트리에 포함된 간선을 말한다.
역행 간선(Back edge)
스패닝 트리의 자손에서 선조로 연결되는 간선을 말한다.
방향 그래프에서의 자기 순환 간선들은 역행 간선으로 간주한다.
순행 간선(Forward edge)
교차 간선(Cross edge)
위 세 분류를 제외한 나머지 간선이다.
선조와 자손 관계가 아닌(위계 관계가 없는) 정점들 간에 연결된 간선들을 말한다.
하나의 정점이 다른 정점의 조상이 아니어야 한다.
서로 다른 스패닝트리에 있는 두 정점 사이에 있을수도 있다.
🛑 무방향 그래프의 DFS 스패닝 트리에서 모든 간선은 트리 간선이거나 역행 간선이다.
(u, v)가 순방향 간선이라면 v는 u의 자손이어야 하고, 따라서 v가 u보다 더 늦게 발견되어야 한다.
(u, v)가 역방향 간선이라면, v는 u의 선조이어야 한다. 따라서 v가 u보다 먼저 발견되어야 한다.
(u, v)가 교차 간선이라면, DFS(v)가 종료된 후 DFS(u)가 호출되어야 한다. 따라서 v는 u보다 일찍 발견되어야 한다.
vector<vector<int>> adj;
vector<int> discovered, finished;
int counter;
void DFS(int node)
{
discovered[node] = counter++;
for (int i = 0; i < adj[node].size(); ++i) {
int next = adj[node][i];
cout << "(" << node << "," << next << ") : ";
// 아직 방문하지 않았다면
if (discovered[next] == -1) {
cout << "tree edge" << endl;
DFS(next);
}
// next를 방문했지만 순서가 node보다 늦다면
else if (discovered[node] < discovered[next])
cout << "forward edge" << endl;
// next를 방문했지만 순서가 node보다 빠르다면
// next가 종료하지 않았다면 => next는 node의 선조가 됨.
else if (finished[next] == 0)
cout << "back edge" << endl;
// 이 외에는 모두 교차 간선
else
cout << "cross edge" << endl;
}
finished[node] = 1;
}
discovered
라는 배열에 counter
변수를 이용하여 발견 순서를 저장한다.
이 배열은 정점이 방문되었는지 여부도 한번에 알려준다. 기존 DFS에서의 visited 배열과 유사하다.
이 배열을 통해 선조와 자손, 혹은 아무것도 아닌 관계를 밝혀낼 수 있다.
또한, 정점 u에서의 인접한 모든 정점으로의 DFS가 종료되었는지를 저장하는 배열 finished
를 이용한다.