[알고리즘 #6] 그래프 이론
Graph 기초
: Graph G는 vertex의 집합 V(G)와 두 vertex의 pair 인 edge들의 집합 E(G)로 구성되어 있는 구조이다. G=(V(G), E(G)) 로 표기한다.
- Directed Graph
E(G)의 각 원소들은 ordered pair임( 방향성이 있음 )
- Undirected Graph
E(G)의 각 원소들은 unordered pair임( 방향성이 없음 )
- adjacent
(u,v) E E(G) 일때 U, V는 adjacent 하다고 한다.
- Degree of u (deg(u))
G에서 vertex u와 adjacent 한 vertex 수이다.
- Edga (u,v) 가 있는 경우 v는 u의 neighbor 이다.
- Directed graph 의 경우 in-degree 와 out-degree를 따로 정의할 수 있다.
( 그 vertex 기준 들어오는 화살표를 in-degree 나가는 화살표를 out-degree라고 한다)
- Connectivity
(Undirected) Graph G 에서 G = (V(G), E(G)) 에서
- G 는 connected 히다 : 임의의 서로 다른 두 vertex v, w E V(G) 에 대해 v에서 w로의 path 가 존재한다. 이 떄 G를 connected Graph 라고 한다.
- connected 하지 않은 그래프는 disconnected라고 한다.
그래프 저장 방법
- Adjacency matrix
- Adfacency list
Adjacency matrix
Vertex n개인 Graph G를 nxn matrix(array) 를 이용하여 저장
Weighted graph 일 경우, array 의 각 (i, j) 번째 entry 에 edge( I, j) 의 weight 를 저장한다.
(* github 구현 *)
Adfacency list
Vector의 array로 쉽게 구현 가능하다.
Weighted Graph 인 경우, 각각의 구성 요소가 pair 인 vector v의 list로 관리할 수 있으며, 이 경우 v[i] 가 pair(j, b) 를 포함하는 것은 edge (i, j) 의 weight 가 b임을 나타냄
(* github 구현 *)
Adjacency Matrix | Adjacenc List | |
Space | O(n^2) | O(n+m) |
Vertex u와 v가 adjacent 한지 체크 | O(1) | O(deg(u)) |
Vertex u와 adjacent한 모든 vertex 탐색 | O(n) | O(deg(u)) |
deg(u) 답하기 | O(n) | O(deg(u)) |
Graph Traversal
DFS(Depth-First Search) : 시작 vertex 에서 시작하여 해당 vertex에서 reachable(도달 가능한) vertex를 recursive하게 방문하는 방법
이미 방문한 vertex 를 재방문하지 않기 위해 visited 라 하는 array 로 이미 방문한 vertex 의 목록을 관리한다.
Adjacency list 를 사용할 경우 O(n+m)이다. ( matrix 는 O(n^2))
Time Complexity
1. List 사용시 : O(m+n) 2. Array 사용시 : O(n^2)
(* github 구현 *)
BFS(Breadth-Fisrt Search) : 임의의 시작 vertex에서 시작하여 해당 vertex와 거리(= 두 vertex간의 path에 존재하는 edge 수) 가 가까운 순서대로 방문하는 방법
- 기준점에서 거리가 가까운 순서대로 방문
- 트리로 나타낼 수 있다
- 다음에 방문할 vertex를 큐로 관리한다.
- Unweighted Shortest Path 구할 때는 BFS 사용 하는 것이 유리하고, 그 외 보통은 DFS를 주로 사용한다.
Time Complexity
1. List 사용시 : O(m+n) 2. Array 사용시 : O(n^2)
(* github 구현 *)
Graph Traversal 응용
* maximal : 더이상 추가가 안된다는 것 / 더이상 vertex 추가할 수 없을
* maximum : 포함 관계에 따라 최대인/ 극대의 / Maximum인 것은 maximal이기도 하다.
1. Connectivity Check
- reachable 하지 않다면 path 없다. connected 하지 않다.
- 아무 vertex 로부터 traversal 을 시작해서 한번에 graph 의 모든 vertex를 방문할 경우 connected 위 과정을 반복함으로써 graph의 모든 connected componen 를 반환할 수 있다.
(* github 구현 *)
2. Cycle Check
- DFS 트리에서 어떤 vertex V와 adjacent 하는 애들 중에서, adjacent 한데 방문을 이미 한 vertex들이 있으면 , cylcle 이 생긴다.
- 트리에서는 항상 [V] - 1 = [E] 이다. (*증명: 트리의 리프들은 연결된 edge가 항상 한 개이다. 만약 리프는 제거하더라도 여전히 트리이다. vertex 개수가 k+1 개인 그래프를 생각해보자. edge는 k개 인가? // vertex 개수를 n이라고 가정하자 Base case) n=1 이면, 선의 개수는 0개이다. (성립) n=k 일때 참이라고 가정하자. / vertex가 k+1 개인 임의의 그래프 T를 잡았을 때, 이 그래프는 vertex가 두 개이상이므로 리프가 존재한다. 여기서 리프 v를 제거하고 난 그래프 T' 을 가정했을 때, vertex는 k개이다. n=k일 때 성립함을 가정했으므로 edge는 k-1개이다. T 기준 제거한 v가 리프노드였기 때문에 연결된 edge는 한 개이다. 따라서 T에서의 edge개수는 T'의 edge개수에서 한 개를 더한 것이 된다. 따라서 수학적 증명법에 의해, vertex가 n+1 개인 나무는 n개의 edge를 갖는다 )
- 트리의 성질 이용하면 vertex 가 c개인 connected component 에 대해 edge가 c-1 개보다 많다면 무조건 cycle 이 존재하낟.
(* github 구현 *)
3. Bipartiteness Check
- 두 개의 partition을 지정하여 vertex set을 partitioning 할 수 있는 경우 같은 색깔로 칠해진 두 vertex 사이에 edge가 없도록 색칠이 가능한 경우, 해당 그래프는 Bipartite graph 이다.
- 내가 현재 빨간색이면 이웃한 인접한 애는 파란색으로 칠하고 이런식으로 돌면서 체크한다. 다음에 순회할 때 만약 red 와 red 사이 back edge 있다면 이 그래프는 bipartite grapch 가 아니다. 충돌안생기면서 끝까지 체크할 수 있다면 bipartite grapch 이다.
- bipartite graph 가 아니다 = odd cycle(길이가 odd인 cycle) 이 있다는 사실과 equvalent 함.
(* github 구현 *)
ex) Uva 11953 Battleships
nxn 바둑판 형식의 그리드가 주어진다. battleship 은 horizontal 하게 있거나 vertical 하게 존재한다. 번갈아 가면서도 가능하다. 그러나 대각선으로는 안된다. 각 배틀십은 고유한 공간을 차지하고 있다. 배틀십 개수를 구하여라
입력 : 테이스 케이스 T 입력 -> N(그리드 사이즈) 입력 -> N개의 캐릭터들 각각은 플레잉 중인 그리드를 묘사한다. '.'이면 비어있는 셀이고 'x'이면 쉽을 포함하거나 그것의 부분을 포함한다. '@'은 이미 쉽이 친 부분이다.
=> 연속된 x와 @로 이루어진 path 그래프 이다. x로만 이루어진 path 찾아야한다.
Flood Fill기법 사용(방문한 모든 cell 들을 덧칠하여 시간과 공간을 절약할 수 있다)
-> DFS 돌면서 'x' 발견할때만 dfs 하면 된다. 'x' 로 connected 된 덩어리들의 개수가 정답이므로 첫 dfs 실행될때 res 값을 ++ 해준다. DFS 로 방문할때 방문한 모든 vertex를 '.'으로 업뎃하여 다음엔 방문 안하도록한다.
(* github 구현 *)
Shortest Path
1. Unweighted case : BFS 이용
(* github 구현 *)
2. Weighted case
- Weight 가 모두 non-negative 일 때 - O((n+m)log n) , Dijkstra
-> 보통 많이 사용한다. single source shortest path
- Negative cycle 이 존재하지 않을 때 - O(nm) , Bellman-Ford
-> Negative Cycle 일때이고 single source shortest path
- Negative cycle 이 존재하지 않을 때 - O(n^3) , Floyd-Warshall
-> 임의의 두 점간의 shortest parh 구할 수 있다.(All-pairs shortest path) 입력값작은 경우 주로 사용되고 실전에서 은근 많이 사용된다.
Dijkstra
- 내가 방문안한애들중에 가장 shortest 한애를 다음 vertex 로 계속 update 해 나가는 greedy Algorithm 이다.
- 아직 처리하지 않은 vertex 들 중 source 로부터 길이가 가장 짧은 vertex는 실제 shortest path 의 길이와 일치한다는 사실을 이용한다.( negative weight 인 edge 존재할 때는 성립안한다.)
- 각 element 가 source 에서 x까지 shortest path 의 길이의 pair 로 구성된 priority queue 를 이용하면 O((n+m)log m) 시간에 구현이 가능하다.
- C++ 의 경우 priority 성질로 인해 -1 곱한 값을 저장해야한다. (* C++ priority queue는 자동으로 내림차순이기 떄문)
(* github 구현 *)
다익스트라 negative edge있을때 성립하지 않는 이유 : 현재 방문하지 않은 노드 중 가장 짧은게 shortest path인데 노드 u에서 v로 가는데 weight 를 w라고 놓자. 만약 u와 v사이에 노드 m이 있고 e(u, m) 의 weight를 w1, e(m,v) 의 weight 는 negative 이며 이를 n이라고 하자. w1>w 이면서 w1-n < w 일때 다익스트라 알고리즘은 그리디하게 w를 택하여 v로 도착할 것이다. 그러나 실제로는 w1-n < w 이므로 negative edge를 거쳐서 가는 것이 더 shortest path 이다 따라서 negative edge 있다면 shortest path를 보장하지 않는다.
Bellman-Ford
- DP 기반 shortest path Algorithm
- dist(i, u) 라는 함수를 정의한다. i번째 round 까지 끝난 뒤에 시작점에서 u까지의 shortest path 이다. i번째 round 는 edge를 i개 이하로 사용하는 sorce 에서 각 vertext간의 shortest path의 길이로 정의된다.
- 모든 case 탐색하므로 느리다.
- dist(i, u) = min(dist(i-1, u), min(dist(i-1, u' ) + w(u', u))
- negative cycle 있으면 길이가 한없이 줄어들어서 동작하지 않는다. ( *증명*) -> 한없이 줄어드는 게 있다면 해당 graph는 negative cycle 가진다고 판별할 수 있다.
(* github 구현 *)
+ Bellman-Ford 복습
v0 -> v1 -> v2 -> .. -> vk가 s에서 도달할 수 있는 negative cycle 이라고 가정하자. 여기서 v0 = vk이다. 포말하게 이것은 우리 (i=1부터 k까지)∑ w(v_i-1, v_i) < 0 임을 의미한다. contradiction 으로 진행하면 우리가 모든 i = 1, . . . , k.에 대해 dn−1(vi) ≤ dn−1(vi−1) + w(vi−1, vi) 일때 사이클에서 각각의 vertex에대해 부등식을 더하면
( 모든 시그마 i=1부터 k까지) ∑ dn−1(vi) ≤∑ dn−1(vi-1) + ∑w(vi−1, vi) 이다
v0 = vk 이기 때문에
(i=1부터 k까지) ∑ dn−1(vi-1) = (i=0부터 k-1까지)∑dn−1(vi) = dn−1(v0) + (i=1부터 k-1까지)∑dn−1(vi) = (i=1부터 k까지) dn−1(vi) 가 성립한다.
앞 두개가 같으므로 그것들을 cancel하면
(i=1부터 k까지) w(vi-1, v i) >= 0
모순이 발생하므로 가정 v0 -> v1 -> v2 -> .. -> vk 는 negative cycle 이다.
Floyd-Warshall
- DP 기반으로 한다.
- recurrence 를 다르게 정의한다. 시작점이 고정이 아니기 때문에 parameter 3개 필요하다. Vertext set V={1,2,..,n} 이라 할 때 i 번째 round 는 'vertex {1...i} 만을 중간지점으로 사용하는 all-pairs shortest path 의 길이를 저장한다.
- dis(i, j, k) 가 k-th round 가 지난 이후 저장된 vertex i 에서 j 로의 shortest path 의 길이라 하면 다음 recurrence 성립한다
- dist(i, j, k) = min(dist (i, k , k-1) + dist (k, j, k-1), dist(i,j,k-1))
- 2차원 array 사용하여 쉽게 구현할 수 있다.
(* github 구현 *)
ex) Uva 01056 - Degrees of Separation
(* github 구현 *)