하나의 정점으로부터 시작하여 차례대로 모든 정점들을 한 번씩 방문하는 것
그래프 완전 탐색 기법 중 하나
그래프 시작 노드에서 출발하여 탐색할 한 쪽 분기를 정하여 최대 깊이까지 탐색을 마친 후 다른 쪽 분기로 이동해 다시 탐색을 수행하는 알고리즘
1. a 노드(시작 노드)를 방문한다.
-> 방문한 노드는 방문했다고 표시한다.
2. a와 인접한 노드들을 차례로 순회한다.
-> a와 인접한 노드가 없다면 종료한다.
3. a와 이웃한 노드 b를 방문했다면, a와 인접한 또 다른 노드를 방문하기 전에 b의 이웃 노드들을 전부 방문해야 한다.
-> b를 시작 정점으로 DFS를 다시 시작하여 b의 이웃 노드들을 방문한다.
4. b의 분기를 전부 완벽하게 탐색했다면 다시 a에 인접한 정점들 중에서 아직 방문이 안 된 정점을 찾는다.
-> 즉, b의 분기를 전부 완벽하게 탐색한 뒤에야 a의 다른 이웃 노드를 방문할 수 있다는 뜻이다.
-> 아직 방문이 안 된 정점이 없으면 종료한다.
-> 있으면 다시 그 정점을 시작 정점으로 DFS를 시작한다.
DFS는 그래프(정점의 수: N, 간선의 수: E)의 모든 간선을 조회한다.
희소 그래프
노드개수보다 간선개수가 적은 그래프
한 번 방문한 노드를 다시 방문하면 안된다!
노드 방문 여부를 체크할 배열이 필요하며, 그래프는 인접리스트로 표현한다.
탐색방식은 후입선출의 특성을 가지므로, 이에 대한 설명은 스택을 사용해 설명한다. (구현은 재귀함수 이용)
방향이 없는 그래프는 양쪽 방향으로 에지를 모두 저장해야한다.
모든 노드를 탐색하는 데 실행한 DFS의 실행 횟수 = 연결 요소 개수
1) 그래프의 모든 정점을 방문하는 것이 주요한 문제
DFS, BFS 둘 다 상관없다.
2) 경로의 특징을 저장해둬야 하는 문제
예를 들면 각 정점에 숫자가 적혀있고 a부터 b까지 가는 경로를 구하는데 경로에 같은 숫자가 있으면 안 된다는 문제 등, 각각의 경로마다 특징을 저장해둬야 할 때는 DFS를 사용합니다. (BFS는 경로의 특징을 가지지 못합니다)
이밖에도
public class Dfs_cd {
static List<Integer>[] nodeList;
static int[] visited;
public static void main(String[] args) {
// 입력 데이터
int N = 5;
int[][] nodes = {{0,1},{0,2},{1,3},{1,4}};
// 리스트, 배열 초기화
nodeList = new ArrayList[N];
for(int i=0; i<N; i++) nodeList[i] = new ArrayList<>();
visited = new int[N];
// 입력받은 간선을 간선 리스트에 저장
// ex) 1-2 : nodeList[1].add(2); nodeList[2].add(1);
// 무방향성이므로 양쪽을 저장
for(int[] e : nodes){
nodeList[e[0]].add(e[1]);
nodeList[e[1]].add(e[0]);
}
visited[0] = 1; // 0부터 출발, 0은 방문 처리
dfs(0);
}
static void dfs(int cur) {
for(int next : nodeList[cur]) {
if(visited[next] == 0) { // 이어진 점이 방문한 곳이 아니면
visited[next] = 1; // 방문 처리하고
dfs(next); // 해당 점부터 재귀
}
}
}
}
그래프에서 사이클이 존재하는지 여부는 dfs 를 사용해서 직관적으로 찾을 수 있다.
일반적인 그래프의 코드를 살펴보도록 하자
static void dfs(int v) {
visit[v] = true;
for(int u : adj[v]) {
if(visit[u]) continue;
dfs(u);
}
}
v 라는 정점을 기준으로 dfs 를 수행한다고 했을 때 dfs(v) 가 끝나기 전에 v 를 재방문하는 일이 생기면 사이클이 존재함을 알 수 있다.
아래의 코드는 dfs 를 이용한 사이클 존재 여부를 확인하는 코드이다.
static boolean cycle = false;
static boolean[] visit = new boolean[n+1];
static boolean[] finished = new boolean[n+1];
static void dfs(int v) {
visit[v] = true;
for(int u : adj[v]) {
if(!visit[u]) dfs(u);
else if(!finished[u]) cycle = true;
}
finished[v] = true;
}
finished 라는 배열을 통해서 해당 정점에서의 dfs 탐색이 끝났는지 여부를 저장한다. 이 때 이미 방문한 정점의 dfs 탐색이 끝나지 않았는데 해당 정점을 재방문하는 경우 사이클이 존재함을 확인할 수 있다.
무방향 그래프에 사이클이 존재하는지의 여부를 확인
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Stack;
import java.util.StringTokenizer;
// 무방향 그래프 -> cycle찾기
public class isCycle {
static int N, M, T, cnt, flag;
static ArrayList<Integer> adj[];
static boolean visited[];
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st;
T = Integer.parseInt(br.readLine());
for(int test_case = 1; test_case <=T; test_case++){
st = new StringTokenizer(br.readLine());
N = Integer.parseInt(st.nextToken());
M = Integer.parseInt(st.nextToken());
adj = new ArrayList[N+1];
for(int i=1; i<=N; i++){
adj[i] = new ArrayList<>();
}
for(int i=1; i<=M; i++){
st = new StringTokenizer(br.readLine());
int u = Integer.parseInt(st.nextToken());
int v = Integer.parseInt(st.nextToken());
adj[u].add(v);
adj[v].add(u);
}
visited = new boolean[N+1];
if(isDfsCycle(1, -1)){
System.out.println("사이클이다");
}else{
System.out.println("사이클 아니다");
}
}
}
private static boolean isDfsCycle(int curr, int parent) {
visited[curr] = true;
for(int next : adj[curr]){
if(!visited[next]){
if(isDfsCycle(next, curr)){
return true;
}
}else if(next != parent){ //방문 했었는데, 부모노드가아니라 다른 노드일 경우니까 이건 사이클이다.
return true;
}
}
return false;
}
}
출처
https://dev-typo.tistory.com/22
https://beenlife.tistory.com/54
한 정점에서 DFS를 수행하게 되면 말단 노드까지 내려갈 것이다. 말단 노드는 진출 차수가 0이기 때문에 가장 마지막에 수행되어야 하는 작업을 뜻하게 된다.
그렇다면 LIFO 형태의 스택을 이용한다면 가장 먼저 해야 할 작업부터 가장 늦게 해야 할 작업의 순서로 출력을 쉽게 할 수 있다.
임의의 방문하지 않은 정점을 선택해도 상관이 없는 이유는 DFS는 선택한 정점보다 늦게 수행되어야 할 정점(더 깊은 정점)들만 방문하는 원리이기 때문이다.
import java.util.ArrayList;
import java.util.Stack;
class MyGraph {
int vertexCnt;
ArrayList<Integer>[] edge_List;
boolean[] visit;
boolean[] finish;
Stack<Integer> answer;
boolean cycle;
public MyGraph(int N) {
this.vertexCnt = N;
edge_List = new ArrayList[N+1];
for (int i = 0; i <= N; ++i) {
edge_List[i] = new ArrayList<>();
}
visit = new boolean[vertexCnt+1]; //방문 표시
finish = new boolean[vertexCnt+1]; //사이클 판단
answer = new Stack<>(); //결과를 담을 스택
}
public void insert_Edge(int from, int to) {
edge_List[from].add(to);
}
public void topological_Sort() {
//방문하지 않은 정점을 DFS 수행
for (int i = 1; i <= vertexCnt; ++i) {
if (cycle) {
System.out.println("그래프에 사이클 존재");
return;
}
if (!visit[i]) {
dfs(i);
}
}
//스택에 담긴 정점들을 출력
while (!answer.isEmpty()) {
System.out.print(answer.pop() + " ");
}
}
public void dfs(int v) {
visit[v] = true;
for (int i = 0; i < edge_List[v].size(); ++i) {
int nv = edge_List[v].get(i);
//방문하지 않았으면 dfs 수행
if (!visit[nv]) {
dfs(nv);
}
//방문한 정점인데 finish 체크가 되지 않았으면 사이클이 존재한다는 의미
else if (!finish[nv]) {
cycle = true;
return;
}
}
//더 이상 갈 곳이 없는 정점을 finish 체크 & 스택에 넣어줌 (말단부터 상위로)
finish[v] = true;
answer.push(v);
}
}
public class Blog {
public static void main(String[] args) {
MyGraph mg = new MyGraph(7);
mg.insert_Edge(1, 2);
mg.insert_Edge(1, 3);
mg.insert_Edge(1, 4);
mg.insert_Edge(2, 5);
mg.insert_Edge(2, 6);
mg.insert_Edge(3, 7);
mg.insert_Edge(3, 6);
mg.insert_Edge(4, 3);
mg.insert_Edge(6, 5);
mg.topological_Sort();
}
}