[Algorithm] Algorithm 그래프

선형 : 1:1 , 한줄로 줄 세울 수 있다 비 선형: 1:1이 아닌 관계(1:다, 다대다) -> 트리, 그래프

그래프는 아이템(사물 또는 추상적 개념)들과 이들의 연결 관계를 표현

  • 정점(Vertex): 그래프의 구성요소로 하나의 연결점.
  • 간선(Edge): 두 정점을 연결하는 선
  • 차수(degree): 정점에 연결된 간선의 수
  • 그래프는 정점(Vertex)들의 집합과 이들을 연결하는 간선(Edge)들의 집합으로 구성된 자료구조

V: 정점의 개수, E: 그래프에 포함된 간선의 개수 V개의 정점을 가지는 그래프는 최대 V(V-1)/2간선이 가능 예) 5개 정점이 있는 그래프의 최대 간선수는 10(=>54/2)개이다

  • 선형 자료구조나 트리 자료구조로 표현하기 어려운 N:N관계를 가지는 원소들 표현하기 용이.

#### 그래프 유형

  • 무향 그래프(방향성 無) => 양방향 관계
  • 유향 그래프
  • 가중치 그래프
  • 사이클 없는 방향 그래프( DAG, Directed Acyclic Graph)

20210316_092412


  • 완전 그래프 : 정점들에 대해 가능한 모든 간선을 가진 그래프
  • 부분 그래프 : 원래 그래프에서 일부 정점이나 간선을 제외한 그그래프
  • 트리는 싸이클이 없는 무향 연결 그래프이다.
    1. 두 노드 사이에는 유일한 경로가 존재한다.
    2. 각 노드는 최대 하나의 부모 노드가 존재할 수 있다.
    3. 각 노드는 자식 노드가 없거나 하나 이상이 존재할 수 있다.

인접 정점

인접(Adjacency)
  • 두 개의 정점이 간선에 존재(연결됨)하면 서로 인접해 있다고 한다.
  • 완전 그래프에 속한 임의의 두 정점은 모두 인접해 있다.

20210316_093713

그래프 경로

경로란(path) 어떤 정점에서 시작해 다른 정점(B)로 끝나는 순회로 두 정점 사이를 잇는 간선들을 순서대로 나열한 것.

완전탐색하면 루트부터 하는데 그래프는 그런게 없으니까 임의의 정점에서 구해도 다 탐색이 가능하다.(임의의 정점이 다른 하나의 정점에 연결되어 있으면 다 탐색 가능)

  • 0~6의 경로
  • 정점들: 0-2-4-6
  • 간선들: (0,2),(2,4),(4,6)

  • 경로 중 한 정점을 최대 한 번만 지나는 경로를 단순경로라 한다.
  • 0-2-4-6, 0-1-6
순환 경로(Cyclic Path)
  • 경로의 시작점과 끝 점이 같은
  • 경로에서 어떤 정점을 2번 이상 거치는 경우
  • 1-3-5-1

그래프 표현

  • 간선의 정보를 저장하는 방식, 메모리나 성능을 고려해서 결정

  • 인접행렬(Adjacent matrix) 1.VxV 크기의 2차원 배열을 이용해 간선 정보를 저장 2.배열의 배열(Reference Array)

  • 인접 리스트(Adjacent List) 각 정점마다 다른 정점으로 나가는 간선의 정보를 저장

  • 간선 리스트(Edge List) 간선(시작 정점, 끝 정점)의 정보를 객체로 표현하여 리스트에 저장

20210316_095223


두 정점을 연결하는 간선의 유무를 행렬로 표현

  • VxV 정방 행렬
  • 행 번호와 열 번호는 정점에 대응
  • 두 정점이 인접되어 있으면 1 , 아니면 0으로 표현

  • 무향 그래프 i번쨰 항 = i번쨰 열의 합 = Vi의 차수

  • 유향 그래프 행 i의 합 = Vi의 진출 차수 열 i의 합 = Vi의 진입 차수.

20210316_103546


인접 행렬의 단점은?

희소 그래프(sparse Graph) vs 밀집 그래프 (Dense Graph)

20210316_103759

희소 그래프일때는 인접 리스트를 쓰다.


인접 리스트를
  • 각 정점에 대한 인접 정점들을 순차적으로 표현

  • 하나의 정점에 대한 인접 정점들을 각각 노드로 하는 연결리스트로 저장.

20210316_104236

20210316_104105

맨 앞 배열들처럼 있는게 from 거기서 오른쪽으로 나온 애들을 to로 보면 된다.

0번 정점 기준으로 인접한 애들 1번 정점 기준으로 인접한 애들.. … 그렇게 옆으로 정점이 이어져있다.

20210316_104343

연결리스트의 head를 가지고 있으면(첫번째) 다 가지고 있는 거. 연결리스트 만들 때 정점 개수만큼 만들고 자신의 연결리스트의 head만 유지한다.

근데 연결리스트 못 만들겠다고 어레이리스트로 만들면 어레이리스트로 저장하면 된다.

구현 방식에 따라 틀리다.

  1. 연결리스트로 만들고 하나하나 노드로 만들면 Node타입의 배열로 선언하면 됨.

  2. 만약 연결리스트로 안 만들고 ArrayList로 만들면 그 어레이리스트로 담는 배열. 그럼 얘는 ArrayList의 배열이 된다. (2번이 더 쉽다고 생각할 수 있는데 어레이 리스트는 자바의 컬렉션이고 컬렉션은 성능상 불리함이 좀 있다.) 직접 연결 리스트로 구현하는게 더 좋다.(30프로 이상 속도 개선)

그리고 연결 리스트 안의 순서는 중요한가? 전혀 중요하지 않다. 앞으로 삽입하는 알고리즘으로 추가하면 됨.(head가 가리키는 거 계속 바꿔주니까 복잡할 게 전혀 없다.)

(2차원 배열로 만드는 거보다 손이 더 가서 잘 안쓰려고 하는데 알아두는게 좋다.)


20210316_105058

위는 무향인데 0-6, 6-0 추가 간선 8개인데 의미로는 16개. 즉 4,1,1,2,3,3,2개로 16개.(대칭되게 인접행렬 만든거 처럼 인접행렬 만들 때도 뒤집어서 관계성 표현 똑같이 해줘야 한다.)

근데 밑은 유향이니까 그럴 필요가 없다. 유향그래프가 0-6의 그래프는 0번에서만 하면 되지 6번 인접헤드에서 할 필요도 없다.

따라가면서 자신하고 연결된 애의 수가 된다.


간선 리스트

  • 두 정점에 대한 간선 그 자체를 객체로 표현하여 리스트로 저장
  • 간선을 표현하는 두 정점의 정보를 나타냄(시작 정점, 끝 정점)

어느 정점에서 어느정점으로 가는 간선정보고 그때 가중치가 얼마인지.

무향이면 간선정보 하나 더 만들어서넣어주면 됨.

20210316_105540


그래프 탐색(순회)

  • 그래프 순회는 비 선형 구조인 그래프로 표현된 모든 자료(정점)를 빠짐 없이 탐색하는 것을 의미한다

  • 두가지 방법

    1. 너비 우선 탐색(Breadth frist search, BFS)
    2. 깊이 우선 탐색(descriptionth frist search, DFS)

  • 너비 우선 탐색은 탐색 시작점의 인접한 정점들을 먼저 모두 차례로 방문한 후에, 방문했던 정점을 시작점으로 해서 다시 이넞ㅂ한 정점들을 차례로 방문하는 방식

  • 인접한 정점들에 대해 탐색 한 후, 차례로 다시 너비우선 탐색을 진행해야 하므로, 선입 선출 형태의 자료구조인 큐를 활용함.

  • 최단 거리 찾을 때 BFS 많이 사용.

20210316_110049

20210316_110242


package com.ssafy.graph;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.Queue;
import java.util.StringTokenizer;

/*
7
8
0 1
0 2
1 3
1 4
2 4
3 5
4 5
5 6
*/

public class G1_AdjMatrixTest {
	static int N;
	static boolean[][] adjMatrix;

	public static void main(String[] args) throws NumberFormatException, IOException {
		BufferedReader br  = new BufferedReader(new InputStreamReader(System.in));
		N = Integer.parseInt(br.readLine());
		int C = Integer.parseInt(br.readLine());
		adjMatrix = new boolean[N][N];

		StringTokenizer st = null;
		for(int i =0; i<C;i++) {
			st = new StringTokenizer(br.readLine()," ");
			int from = Integer.parseInt(st.nextToken());
			int to = Integer.parseInt(st.nextToken());
			adjMatrix[to][from] = adjMatrix[from][to] =true;
		}
		bfs();
	}
	private static void bfs() {
		Queue<Integer> queue = new LinkedList<Integer>();
		boolean [] visited = new boolean[N];

		//탐색 시작 정점 : 0으로 출발
		int start =0;
		queue.offer(start);
		visited[start] = true;

		while(!queue.isEmpty()) {
			int current = queue.poll();
			//현재 정점에 관련된 처리
			System.out.println((char)(current+65));

			//인접 정점 탐색
			for(int i = 0;i<N;i++) {
				if(adjMatrix[current][i] //인접정렬
						&& !visited[i]) {//미방문 정점
					queue.offer(i);
					visited[i] = true;

			}
		}
	}
}
}

20210316_150139

20210316_152757

0번 노드에서 마지막 노드 따라간 다음 그 마지막 노드 뒤에 붙여야(0-2면) 0-3이면 똑같이 2 마지막 따라가서 붙인다. 그리고 이 부분들이 상관이 없다.

그리고 연결리스트는 방향이 상관 없다.(순서 아무 상관 없음)

연결리스트 맨뒤 넣으려면 맨뒤 찾는 작업해야됨 근데 맨 앞에 집어넣는 알고리즘도 배웠다.

20210316_153240 즉 연결리스트는 지금 구현됨.(순서 아무 상관 없음) 즉 뒤로 넣을 필요 굳이 없고 앞(첫번째)으로 집어 넣어도 상관없음.

0번 정점에서 아무것도 들어있지 않은 null이 고스란이 넘어감. 그렇게 만들어진 객체의 레퍼런스를 from 의 인접리스트에 집어넣음.

		adjList[from] = new Node(to,adjList[from]);

20210316_153446

from 의 정점의 헤드를 지금 새로 만드는 애로 바꿔라 대신 새로 만드는 애가 첫째가 되기 위해선 기존 헤드 정보를 넥스트에 두어서 자신의 뒤로 오게 하면 계속 첫번쨰 노드로 집어넣게 되는 것

이렇게 만드는 법을 꼭 알아둬야한다(쉬운거만 할 수 없다.)

temp가 각 이동하게(tmep가 바라보는 next)

20210316_154316

 for(Node temp = adjList[current]; temp!=null; temp = temp.next)

이 부분은 인접 정점만 반복처리 탐색하는 법이 바뀌는 거 bfs 논리는 그대로임. 이 아래는 인접 체크 조건이 바뀐거.

정점이 1000개인데 한 정점마다 간선정보가 2,3개 미만이면 원래 인접행렬은 인접행렬 계속 찾았다면 인접리스트는 인접한 애들만 처리해서 처리도 줄어들고 공간적으로 관계 없는 애들까지 자리 차지하는게 아니라 있는 애들만 쓰기 때문에 공간 효율도 좋다.

-> 인접리스트가 훨씬 좋다.

근데 출력이 약간 다른데 위의 리스트는 앞에 노드를 계속 넣어서 그럼(뒤가 아니라)

즉 바뀐 부분은 너비가 같은 부분들임.

인접행렬에서 무조건 오른쪽으로 탐색한다고 맞나? 왼쪽방향으로 탐색해도 동일함.

20210316_160455

너비가 같은 거 안에서는 순서는 상관이 없다.

이렇게 안하려면 마지막 노드로 매번 집어넣어야됨. 근데 순서가 막 바뀌어 들어올수도. 결과가 이렇게 들어오는 이유 아려면 인접리스트가 어떻게 들어오는지 꼭 이해해야된다.

먼저 처리한 인접리스트 정점이 맨 뒤, 마지막 처리한 인접리스트 정점이 맨 앞이라 순서가 바뀐거 처럼 느끼졈.


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.Queue;
import java.util.StringTokenizer;

/*
7
8
0 1
0 2
1 3
1 4
2 4
3 5
4 5
5 6
*/

public class G2_AdjListTest {

	static class Node{

		int vertex;
		Node next;
		//연결리스트의 요소 하나하나 나타내려 만듬.
		//정점 번호랑 연결리스트 역할위한 링크의노드 포인터 가짐.
		public Node(int vertex, Node next) {	//생성자 생성.(노드생성)
			super();
			this.vertex = vertex;
			this.next = next;

		}
		public Node(int vertex) {		//정점정보만 있는 노드 생성.
			super();
			this.vertex = vertex;
		}
		@Override
		public String toString() {	//오버라이딩
			return "Node [vertex=" + vertex + ", next=" + next + "]";
		}

	}

	static int N;
	static Node[] adjList;	//노드 배열
	//0번 정점에 서 인접해있는	인접 정점들의 연결리스트의 몫(head)
	//기차의 처음만 가져오면 다 딸려오듯. 리스트 처음만 알면 링크 따라 다 끌고 옴.

	public static void main(String[] args) throws NumberFormatException, IOException {
		BufferedReader br  = new BufferedReader(new InputStreamReader(System.in));
		N = Integer.parseInt(br.readLine());
		int C = Integer.parseInt(br.readLine());
		adjList = new Node[N];	//정점의 개수만큼 배열 만듬

		StringTokenizer st = null;
		for(int i =0; i<C;i++) {
			st = new StringTokenizer(br.readLine()," ");
			int from = Integer.parseInt(st.nextToken());
			int to = Integer.parseInt(st.nextToken());
			adjList[from] = new Node(to,adjList[from]);
			//0번 노드에서 마지막 노드 따라간 다음 그 마지막 노드 뒤에 붙여야(0-2면)
			//0-3이면 똑같이 2 마지막 따라가서 붙인다.
			//그리고 이 부분들이 상관이 없다.
			//from 의 정점의 헤드를 지금 새로 만드는 애로 바꿔라 대신 새로 만드는 애가 첫째가 되기 위해선 기존 헤드 정보를 넥스트에 두어서 자신의 뒤로 오게 하면 계속 첫번쨰 노드로 집어넣게 되는 것

			adjList[to] = new Node(from,adjList[to]);	//뒤집어서 한번 더해야
			//이렇게 하면 인접리스트 끝. 간선개수만큼 하면 지금 한 작업들이 계속 반복되면 각 노드마다 인접한 정점으로 연결리스트 만들고 각 헤드를 다 물고 있게 된다.

		}
		bfs();
	}
	private static void bfs() {
		Queue<Integer> queue = new LinkedList<Integer>();
		boolean [] visited = new boolean[N];

		//탐색 시작 정점 : 0으로 출발
		int start =0;
		queue.offer(start);
		visited[start] = true;

		while(!queue.isEmpty()) {	//큐가 비어있지 않을때까지 돌면서
			int current = queue.poll();	//뺀다(가장 앞의 정점)
			//현재 정점에 관련된 처리
			System.out.println((char)(current+65));

			//인접 정점 탐색(여기서 부터 달라짐)
			//아까는 인접행렬이라 정점개수만큼 들여다보며 하나씩 체크했는데 이번은 인접리스트라 어떤정점의 인접리스트엔 인접한 애들만 들어있ㅅ다.
//			그래서 인접여부 체크 할 필요가 없다.
			for(Node temp = adjList[current]; temp!=null; temp = temp.next) {
				//Node temp = adjList[current]; 이게 인접리스트의 첫번쨰 해드

//for(Node temp = adjList[current]; temp!=null; temp = temp.next) { 이 부분은 인접 정점만 반복처리
//탐색하는 법이 바뀌는 거 bfs 논리는 그대로임. 이 아래는 인접 체크 조건이 바뀐거. 불필요한 조건이 없어짐.

					if(!visited[temp.vertex]) {	//연결은 되어있으니 인접여부는 판단할 필요가 없고  노드가 저장된 정점을 보면 그게 어떤 정점 정보인지 알 수 있다. 그게 방문되었는지 아닌지만 체크
						queue.offer(temp.vertex);
						visited[temp.vertex] = true;
					}

				}
			}
		}
}

꼭 기억해야 상황따라 인접행렬 못 만드는 경우가 있다! 그럼 인접리스트 만들어야 된다.

굳이 못 만들겠다면 어레이리스를 차곡차곡 해서 여러개 만들수 있긴 한데 아무튼 인접리스트 부분은 꼭 알아두자.


DFS 알고리즘

  • 시작정점의 한 방향으로 갈 수 있는 경로 있는 곳 까 깊이탐색해가다가 더 이상 갈 곳이 없게 되면 가장 마지막에 만났던 갈림길 간선이 있는 정점으로 되돌아와서 다른 방향의 정점으로 계속 탐색을 반복하여 결국 모든 정점을 방문하는 순회방법.

  • 가장 마지막에 만났던 갈림길의 정점으로 되돌아가서 다시 깊이우선탐색을 반복해야 하므로 재귀적으로 구현하거나 후입선출 구조의 스택사용.

20210316_162321


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.Queue;
import java.util.StringTokenizer;

/*
7
8
0 1
0 2
1 3
1 4
2 4
3 5
4 5
5 6
*/

public class G2_AdjListTest {

	static class Node{

		int vertex;
		Node next;
		//연결리스트의 요소 하나하나 나타내려 만듬.
		//정점 번호랑 연결리스트 역할위한 링크의노드 포인터 가짐.
		public Node(int vertex, Node next) {	//생성자 생성.(노드생성)
			super();
			this.vertex = vertex;
			this.next = next;

		}
		public Node(int vertex) {		//정점정보만 있는 노드 생성.
			super();
			this.vertex = vertex;
		}
		@Override
		public String toString() {	//오버라이딩
			return "Node [vertex=" + vertex + ", next=" + next + "]";
		}

	}

	static int N;
	static Node[] adjList;	//노드 배열
	static boolean[] visited;
	//0번 정점에 서 인접해있는	인접 정점들의 연결리스트의 몫(head)
	//기차의 처음만 가져오면 다 딸려오듯. 리스트 처음만 알면 링크 따라 다 끌고 옴.

	public static void main(String[] args) throws NumberFormatException, IOException {
		BufferedReader br  = new BufferedReader(new InputStreamReader(System.in));
		N = Integer.parseInt(br.readLine());
		int C = Integer.parseInt(br.readLine());
		adjList = new Node[N];	//정점의 개수만큼 배열 만듬

		StringTokenizer st = null;
		for(int i =0; i<C;i++) {
			st = new StringTokenizer(br.readLine()," ");
			int from = Integer.parseInt(st.nextToken());
			int to = Integer.parseInt(st.nextToken());
			adjList[from] = new Node(to,adjList[from]);
			//0번 노드에서 마지막 노드 따라간 다음 그 마지막 노드 뒤에 붙여야(0-2면)
			//0-3이면 똑같이 2 마지막 따라가서 붙인다.
			//그리고 이 부분들이 상관이 없다.
			//from 의 정점의 헤드를 지금 새로 만드는 애로 바꿔라 대신 새로 만드는 애가 첫째가 되기 위해선 기존 헤드 정보를 넥스트에 두어서 자신의 뒤로 오게 하면 계속 첫번쨰 노드로 집어넣게 되는 것

			adjList[to] = new Node(from,adjList[to]);	//뒤집어서 한번 더해야
			//이렇게 하면 인접리스트 끝. 간선개수만큼 하면 지금 한 작업들이 계속 반복되면 각 노드마다 인접한 정점으로 연결리스트 만들고 각 헤드를 다 물고 있게 된다.

		}
		bfs();

		visited = new boolean[N];
		visited[0] = true;
		dfs(0);
	}
	private static void bfs() {
		Queue<Integer> queue = new LinkedList<Integer>();
		boolean [] visited = new boolean[N];

		//탐색 시작 정점 : 0으로 출발
		int start =0;
		queue.offer(start);
		visited[start] = true;

		while(!queue.isEmpty()) {	//큐가 비어있지 않을때까지 돌면서
			int current = queue.poll();	//뺀다(가장 앞의 정점)
			//현재 정점에 관련된 처리
			System.out.println((char)(current+65));

			//인접 정점 탐색(여기서 부터 달라짐)
			//아까는 인접행렬이라 정점개수만큼 들여다보며 하나씩 체크했는데 이번은 인접리스트라 어떤정점의 인접리스트엔 인접한 애들만 들어있ㅅ다.
//			그래서 인접여부 체크 할 필요가 없다.
			for(Node temp = adjList[current]; temp!=null; temp = temp.next) {
				//Node temp = adjList[current]; 이게 인접리스트의 첫번쨰 해드

//for(Node temp = adjList[current]; temp!=null; temp = temp.next) { 이 부분은 인접 정점만 반복처리
//탐색하는 법이 바뀌는 거 bfs 논리는 그대로임. 이 아래는 인접 체크 조건이 바뀐거. 불필요한 조건이 없어짐.

					if(!visited[temp.vertex]) {	//연결은 되어있으니 인접여부는 판단할 필요가 없고  노드가 저장된 정점을 보면 그게 어떤 정점 정보인지 알 수 있다. 그게 방문되었는지 아닌지만 체크
						queue.offer(temp.vertex);
						visited[temp.vertex] = true;
					}

				}
			}
		}

	private static void dfs(int current) {	//bfs랑 비슷 큐에서 뽑아내는 정점이 dfs는 재귀는 현재 받아오는 매개변수 정점이 됨.

//		visited[current] = true;	아니면 호출받자마자 재귀할거냐.
		//어차피 들어갈 때 하나 호출을 받을 떄 하나 차이가 없다. 순서를 기다렸다는게 아니라 visited하고하냐 하고 들어가냐 차이

		System.out.println((char)(current+65));

		for(Node temp = adjList[current]; temp!= null; temp = temp.next) {
			if(!visited[temp.vertex]) {
				visited[current] = true;	//체크하고 함수(메서드)타러 갈거냐 / 아니면 호출받자마자 재귀할거냐.(위에거)
				//혹시나 방ㅁ문체크코드 아까처럼 짜고싶으면  항상 호출하는 코드에 쌍이 되게 넣어주자.
				//들어갈떄 하느냐 문 열자마자 차이(사실 차이 없음)
				dfs(temp.vertex);
			}
		}
	}
}







© 2021.03. by yacho

Powered by github