[Algo] 유량 네트워크 (4) 디닉 알고리즘(Dinic's Algorithm)

Park Yeongseo·2024년 11월 6일

Algorithm

목록 보기
17/20
post-thumbnail

1. 에드먼즈-카프 리뷰

에드먼즈-카프 알고리즘은 다음과 같이 진행했다.

  1. BFS를 통해 잔여 네트워크 GfG_f에서의 s->t 최단 경로를 찾아, 이를 증가 경로로 설정한다.
  2. 증가 경로 상의 간선 중 최소 잔여 용량을 가지는 것을 골라, 그 용량만큼을 흘려 보낸다.
  3. 더 이상 증가 경로를 찾을 수 없을 때까지 1-2를 반복

s→ts \rightarrow t 최단 경로를 하나 찾아 흐름을 보내는 데에 O(∣E∣)O(|E|)의 시간이 걸리고, 최단 경로의 개수는 O(∣V∣∣E∣)O(|V||E|)개 이므로 전체 시간복잡도는 O(∣V∣∣E∣2)O(|V||E|^2)이다.

그런데 굳이 증가 경로를 하나씩 찾지 않고, 한 번에 여러 개를 찾아 흐름을 보낼 수 있다면 조금 더 빠르게 최대 유량을 찾아낼 수도 있지 않을까? 우리는 다음의 사실을 알고 있다.

에드먼즈-카프 알고리즘을 수행하는 경우(즉 s→ts \rightarrow t 최단 경로를 증가 경로 pp로 두고, fpf_p를 추가로 흘려보내는 경우), 잔여 네트워크 GfG_f에서의 s,ts, t 최단 거리 δf(s,t)\delta_f(s,t)는 단조 증가한다.

경로 s→ts \rightarrow t의 길이는 길어봐야 ∣V∣−1|V| - 1이기 때문에, 경로의 길이가 같은 것들을 묶어 한 번에 처리할 수 있다면 O(∣V∣)O(|V|)번만 반복해도 최대 유량 문제를 해결할 수 있다.

디닉 알고리즘이 바로 δf(s,t)\delta_f(s, t)가 같은 것들을 묶어 한 번에 처리하는 알고리즘으로, O(∣V∣2∣E∣)O(|V|^2|E|)의 시간복잡도를 가진다. ∣E∣|E|는 적어도 O(∣V∣|V|)에 많으면 O(∣V∣2)O(|V|^2)이니, 에드먼즈-카프 알고리즘보다는 좀 더 빠르다.

2. 디닉 알고리즘(Dinic's Algorithm)

우리에게는 "디닉 알고리즘"으로 잘 알려져 있지만, 사실은 이 알고리즘을 고안한 디니츠(Dinitz)의 이름이 잘못 알려져서라고 한다.

(1) Layered Network(Level Graph)

임의의 유향 그래프 H=(V,E)H=(V, E)와 시작 정점 ss를 생각해보자. ss에서 시작해 BFS를 진행하면 ss와 다른 모든 정점 사이의 최단 거리를 구할 수 있고, 이를 기준으로 VV와 EE를 몇 개의 부분 집합으로 나눌 수 있다. δ(u,v)\delta(u, v)는 uu에서 vv로의 최단 거리다.

정점 레이어(vertex layer)
ii번째 정점 레이어 ViV_i는 ss로부터의 최단 거리가 ii인 모든 정점들의 집합이다.

Vi={v:v∈V,δ(s,v)=i}V_i = \{v : v \in V, \delta(s, v) =i\}

간선 레이어(edge layer)
ii번째 간선 레이어 EiE_i는 Vi−1V_{i-1}에서 ViV_i로 이어지는 모든 간선들의 집합이다.

Ei={(u,v):(u,v)∈E,u∈Vi−1,v∈Vi}E_i = \{(u, v): (u, v) \in E, u \in V_{i - 1}, v \in V_i\}

이렇게 V,EV, E의 부분 집합들을 가지고 새롭게 유향 그래프 L(s)L(s)를 정의해보자.

레이어드 네트워크(layered network)
유향 그래프 H=(V,E)H = (V, E)와 시작 정점 ss가 주어졌을 때, 레벨 그래프 L(s)L(s)는 ss로부터 도달할 수 있는 HH의 모든 정점들과 해당 정점들로의 최단 경로 상에 있는 모든 간선들로 이루어진 유향 그래프다.

L(s)=(∪Vi,∪Ui)L(s) = (\cup V_i, \cup U_i)

어떤 정점 t∈Vt \in V가 있다고 해보자. L(s)L(s)를 이용하면 모든 s→ts \rightarrow t 최단 경로들도 찾을 수 있다. ss에서 시작해 tt로 끝나는 최단 경로들에 포함된 모든 정점들과 간선들로 이루어진 그래프를 L^(s,t)\hat{L}(s, t)라 하자. L^(s,t)\hat{L}(s, t)는 L(s)L(s)를 구성한 후, tt에서 역방향으로 BFS를 수행하면 얻을 수 있다.

Vi^={v∈V:δ(s,v)=i,δ(v,t)=δ(s,t)−i}Ei^={(u,v)∈E:u∈Vi,v∈Vi+1}L^(s,t)=(∪Vi^,∪Ei^)\begin{aligned} \hat{V_i} &= \{v \in V:\delta(s, v) = i, \delta(v, t) = \delta(s, t) -i\}\\ \hat{E_i} &= \{(u, v) \in E: u \in V_i, v \in V_{i + 1}\}\\ \hat{L}(s,t) &= (\cup \hat{V_i}, \cup\hat{E_i}) \end{aligned}

(2) 정리와 증명들

ss를 소스, tt를 싱크로 갖는 유량 네트워크 G=(V,E)G = (V, E)와 흐름 ff가 주어졌을 때, 잔여 네트워크 GfG_f를 가지고 레이어드 네트워크 Lf^(s,t)=(Vf^,Ef^)\hat{L_f}(s, t) =(\hat{V_f}, \hat{E_f})를 만들었다고 해보자(이후 (s,t)(s,t) 생략).

에드먼즈-카프 알고리즘에서 그랬듯 GfG_f의 s→ts \rightarrow t 최단 경로를 하나 골라 증가 경로 pp로 선택하자. 이 경로는 당연히 Lf^\hat{L_f}에서 찾을 수 있다. 이 경로를 따라 fpf_p의 유량을 추가로 흘렸을 때 유량의 값은 ∣f′∣|f'|가 된다고 해보자.

∣f′∣=∣f↑fp∣|f'| = |f \uparrow f_p|

이제 Gf′G_{f'}에서 다음 증가 경로 p′p'를 찾아야 한다. 증가 경로의 길이는 단조 증가하므로 p′p'의 길이는 pp보다 길거나 같은데, 두 경로의 길이가 서로 같은 경우를 생각해보자.

만약 에드먼즈-카프 알고리즘을 사용했다면 또 O(∣E∣)O(|E|)의 시간을 들여 증가 경로를 찾아야 했겠지만, 다행히 우리는 이미 찾아놓았던 Lf^\hat{L_f}를 활용할 수 있다.

Theorem 1

GfG_f의 레이어드 네트워크 Lf^\hat{L_f}에서 pp의 핵심 간선들을 제거한 것을 Lf′^=(Vf′^,Ef′^)\hat{L'_f} = (\hat{V'_f},\hat{E'_f})라 하자. GfG_f에 ∣fp∣|f_p|의 흐름을 추가로 보낸 후의 잔여 네트워크를 Gf′=(Vf′,Ef′)G_{f'} = (V_{f'}, E_{f'})라 할 때 다음이 성립한다.

Ef′^⊆Ef′\hat{E'_f} \subseteq E_{f'}

위 정리를 이용하면 Ef′^\hat{E'_f}에서 찾은 s→ts \rightarrow t 경로를 Gf′G_{f'}의 증가 경로로도 사용할 수 있다.

pf.pf.
GfG_f에 Lf^\hat{L_f}에서 찾은 s→ts \rightarrow t 최단 경로 pp를 따라 fpf_p의 유량을 추가로 흘려보냈을 때 어떤 일이 일어나는지를 생각해보자. (u,v)(u, v)가 pp 위의 간선일 때,

  1. (u,v)(u, v)가 pp의 핵심 간선이라면, 잔여 용량이 cf(u,v)=0c_f(u, v) = 0이 되어 잔여 네트워크에서 사라진다.
  2. GfG_f에서 f(u,v)=0f(u, v) = 0이었다면, 유량 증가 이후 간선 (v,u)(v, u)가 cf′(v,u)=fpc_{f'}(v, u) = f_p의 용량을 가지고 잔여 네트워크에 추가된다.

Ef′E_{f'}가 EfE_f와 달라지는 경우는 위의 두 경우 밖에 없다. 즉 EfE_{f}에서 pp의 핵심 간선들을 삭제하고, pp 위의 간선 몇 개의 역방향 간선이 추가하면 Ef′E_{f'}를 만들 수 있다. 한편 Ef′^\hat{E'_f}는 EfE_f의 부분집합이며, pp의 핵심 간선들을 삭제하기만 한 것이기 때문에 Ef′E_{f'}의 부분집합이 된다.

Lemma

GG에서 에드먼즈-카프 알고리즘을 실행할 때, 모든 정점 v∈Vv \in V에 대해 매 유량 증가마다 잔여 네트워크 GfG_f에서의 δf(s,v)\delta_f(s, v)와 δf(v,t)\delta_f(v, t)는 단조 증가한다.

pf.pf.
이전 글에서 매 유량 증가마다 δf(s,v)\delta_f(s,v)가 단조 증가함은 보인 적이 있다. 마찬가지의 방법으로 매 유량 증가마다 δf(v,t)\delta_f(v, t) 또한 단조 증가함을 보일 수 있다.

Theorem 2

Gf′G_{f'}의 레이어드 네트워크를 Lf′^=(Vf′^,Ef′^)\hat{L_{f'}} = (\hat{V_{f'}}, \hat{E_{f'}})라 할 때, δf′(s,t)=δf(s,t)\delta_{f'}(s, t) = \delta_f(s, t)라면 Ef′^⊆Ef′^\hat{E_{f'}} \subseteq \hat{E'_f}다.

이제 Gf′G_{f'}를 가지고 레이어드 네트워크 Lf′^\hat{L_{f'}}를 만든다고 해보자. Gf′G_{f'}의 모든 s→ts \rightarrow t 최단 경로는 Lf′^\hat{L_{f'}}를 통해 찾을 수 있다. 만약 δf′(s,t)=δf(s,t)\delta_{f'}(s, t) = \delta_f(s, t)이면서 Lf′^\hat{L_{f'}}에서 찾을 수 있는 모든 임의의 s→ts \rightarrow t 최단 경로를 Lf′^\hat{L'_f}에서도 찾을 수 있다면, Lf′^\hat{L'_f}만으로도 Gf′G_{f'}의 증가 경로를 찾을 수 있게 될 것이다.

pf.pf.
Lf′^\hat{L_{f'}}에서는 찾을 수 있지만, Lf′^\hat{L'_f}에서는 찾을 수 없는 s→ts \rightarrow t 최단 경로가 있다고 가정하자. 만약 그렇다면 이 경로에는 Ef′^\hat{E_{f'}}에서는 찾을 수 있지만 Ef′^\hat{E'_f}에서는 찾을 수 없는 간선이 하나 이상 포함되어 있다. 그 중 하나를 (u,v)(u, v)라 한다면 다음의 두 경우가 가능하다.

  1. (u,v)∈Ef(u, v) \in E_f인 경우

만약 (u,v)∈Ef^(u, v) \in \hat{E_f}였다면 이 간선은 GfG_f에서 찾았던 증가 경로 pp를 따라 흐름을 보내면서 삭제된 간선이어야 한다. 하지만 이렇게 삭제된 간선은 Gf′G_{f'}에서도 여전히 삭제된 상태이므로 Ef′^\hat{E'_f}에 포함될 수 없다.

그렇다면 (u,v)∉Ef^(u, v) \not\in \hat{E_f}라 해보자. 이는 GfG_f에서 찾을 수 있는 s→ts \rightarrow t 최단 경로들에 (u,v)(u, v) 간선이 포함되지 않는다는 것이다. (u,v)(u, v)를 포함하는 s→ts \rightarrow t 경로 중 가장 짧은 것은 s→us \rightarrow u 최단 경로, (u,v)(u, v), v→tv \rightarrow t 최단 경로를 거치는 것이고, 그 길이는 δf(s,u)+δf(v,t)+1\delta_f(s, u) + \delta_f(v, t) + 1이다.

한편 (u,v)∈Ef′^(u, v) \in \hat{E'_f}이므로 이 간선은 Gf′G_{f'}에서의 s→ts \rightarrow t 최단 경로들 중 적어도 하나에는 포함되고, 그 길이는 δf′(s,u)+δf′(v,t)+1\delta_{f'}(s, u) + \delta_{f'}(v, t) + 1이다.

그런데 위 Lemma에 따르면 δf(s,u)≤δf′(s,u)\delta_f(s, u) \leq \delta_{f'}(s, u)이고 δf(v,t)≤δf′(v,t)\delta_f(v, t) \leq \delta_{f'}(v, t)여야 한다. 이는 δf(s,t)=δf′(s,t)\delta_f(s,t) = \delta_{f'}(s,t)라는 가정과 모순이다.

δf(s,t)<δf(s,u)+δf(v,t)+1≤δf′(s,u)+δf′(v,t)+1=δf′(s,t)=δf(s,t)\delta_f(s, t) < \delta_f(s, u) + \delta_f(v, t) + 1 \leq \delta_{f'}(s, u) + \delta_{f'}(v, t) + 1 = \delta_{f'}(s, t) = \delta_f(s, t)

따라서 (u,v)(u, v)는 EfE_f의 원소일 수 없다.

  1. (u,v)∉Ef(u, v) \not\in E_f인 경우

(u,v)(u, v)는 Ef′^\hat{E_{f'}}의 원소이므로 (u,v)∈Ef′(u, v) \in E_{f'}다. (u,v)∉Ef(u, v) \not\in E_f였다가 유량 증가와 함께 (u,v)∈Ef′(u, v) \in E_{f'}가 됐을 때, 가능한 경우는 f(v,u)=0f(v, u) = 0인 간선 (v,u)(v, u)가 경로 pp 위에 존재하다, 유량이 증가하면서 cf(u,v)=fpc_f(u, v) = f_p인 간선 (u,v)(u, v)가 잔여 네트워크에 추가되는 경우 밖에 없다.

(v,u)(v,u)가 pp에 포함되는 간선이므로 다음과 같이 쓸 수 있다.

δf(s,v)+δf(u,t)+1=δf(s,t)\delta_f(s, v) + \delta_f(u, t) + 1 = \delta_f(s, t)

또한 (u,v)∈Ef′^(u, v) \in \hat{E_{f'}}로부터 다음과 같이 쓸 수 있다.

δf′(s,u)+δf′(v,t)+1=δf′(s,t)\delta_{f'}(s, u) + \delta_{f'}(v, t) + 1 = \delta_{f'}(s, t)

이 둘을 더하면 아래와 같다(δf(s,t)=δf′(s,t)\delta_f(s,t) = \delta_{f'}(s, t)를 가정했으므로).

δf(s,v)+δf′(v,t)+δf′(s,u)+δf(u,t)+2=2δf(s,t)\delta_f(s, v) + \delta_{f'}(v,t) + \delta_{f'}(s, u) + \delta_{f}(u, t) + 2 = 2\delta_f(s,t)

위 Lemma에 따라 δf(s,v)+δf(v,t)=δf(s,t)≤δf(s,v)+δf′(v,t)\delta_f(s, v) + \delta_f(v, t) = \delta_f(s, t) \leq \delta_f(s, v) + \delta_{f'}(v, t)이고, δf(s,u)+δf(u,t)=δf(s,t)≤δf′(s,u)+δf(u,t)\delta_f(s, u) + \delta_f(u, t) = \delta_f(s, t) \leq \delta_{f'}(s,u) + \delta_f(u, t)이므로, 다음과 같은 모순이 발생한다.

δf(s,v)+δf(v,t)+δf(s,u)+δf(u,t)+2=2δf(s,t)+2≤δf(s,v)+δf′(v,t)+δf′(s,u)+δf(u,t)+2=2δf(s,t)\begin{aligned} \delta_f(s,v) + \delta_f(v, t) + \delta_f(s, u) + \delta_f(u, t) + 2 &= 2\delta_f(s, t) + 2\\ &\leq \delta_f(s, v) + \delta_{f'}(v,t) + \delta_{f'}(s, u) + \delta_{f}(u, t) + 2\\ &= 2\delta_f(s, t) \end{aligned}

따라서 (u,v)∉Ef(u, v) \not\in E_f도 있을 수 없다.

어떠한 경우에도 모순이므로 (u,v)∈Ef′^(u, v) \in \hat{E_{f'}}이면서 (u,v)∉Ef′^(u, v) \not\in \hat{E'_f}인 간선은 존재할 수 없다. 따라서 Ef′^⊆Ef′^\hat{E_{f'}} \subseteq \hat{E'_f}다.

(3) 중간 정리와 데드엔드

지금까지 알아낸 사실들을 정리해보자.

중간 정리
1. 잔여 네트워크 GfG_f가 주어진다면 레이어드 네트워크 Lf^\hat{L_f}를 만든다. Lf^\hat{L_f}에서 s→ts \rightarrow t 최단 경로 pp를 찾을 수 있다면 이는 GfG_f의 증가 경로다.
2. pp를 따라 fpf_p의 흐름을 보냈을 때, 유량이 f′f'로 증가했다고 하자. 잔여 네트워크 Gf′G_{f'}에 존재하는 최단 증가 경로 p′p'의 길이가 pp와 같다면, 그 p′p' 도 Lf^\hat{L_f}(정확히는 Lf^\hat{L_f}에서 pp의 핵심 간선들이 제거된 Lf′^\hat{L'_f})에서 찾아낼 수 있다(Theorem 1과 2).

어떠한 흐름도 없는 초기 상태에서부터 더 이상 유량을 증가시킬 수 없을 때까지 잔여 그래프의 s→ts \rightarrow t 최단 경로를 따라 유량을 증가시키기를 반복할 때, 그 증가 경로의 길이는 단조 증가한다.

이때 증가 경로의 길이가 같은 것들을 묶은 것을 페이즈(phase)라고 해보자. 증가 경로의 길이가 ℓ\ell인 페이즈의 맨 첫 번째 잔여 그래프를 Gℓ=(Vℓ,Eℓ)G_\ell = (V_\ell, E_\ell)이라 하고, 그 레이어드 네트워크는 Lℓ^=(Vℓ^,Eℓ^)\hat{L_\ell} = (\hat{V_\ell}, \hat{E_\ell})라 해보자. 다음의 과정을 통해 길이 ℓ\ell의 증가 경로를 따라 보낼 수 있는 유량을 모두 보낼 수 있다.

  1. Lℓ^\hat{L_\ell}에서 증가 경로 pp를 찾고, 흐름을 보낸다.
  2. 그 다음 잔여 네트워크는 Gℓ′G_{\ell'}이라 하고, 그 최단 증가 경로의 길이도 ℓ\ell이라 하자. Gℓ′G_{\ell'}의 레이어드 네트워크 Lℓ′^\hat{L_{\ell'}}은 Lℓ^\hat{L_\ell}에서 pp의 핵심 간선이 삭제된 Lℓ′^\hat{L'_\ell}의 부분집합이고, 따라서 Gℓ′G_{\ell'}의 증가 경로도 Lℓ′^\hat{L'_\ell}에서 찾을 수 있다.
  3. 또 다음 잔여 네트워크는 Gℓ′′G_{\ell''}라 하고, 그 최단 증가 경로의 길이도 ℓ\ell이라 하자. Gl′′G_{l''}의 증가 경로 p′′p''도 마찬가지로 Lℓ′′^\hat{L'_{\ell'}}에서 찾을 수 있다. Lℓ′^\hat{L'_\ell}에서 p′p'의 핵심 간선을 제거한 것을 Lℓ′′^\hat{L''_\ell} 이라 하면, Lℓ′′^⊆Lℓ′′^\hat{L'_{\ell'}} \subseteq \hat{L''_\ell}이므로 p′′p''는 Lℓ′′^\hat{L''_\ell}에서 찾을 수 있다.
  4. 마찬가지의 방법을 더 이상 길이 ℓ\ell의 증가 경로를 찾을 수 없을 때까지 반복한다.

데드엔드

하나 짚고 넘어가야 할 점은 Lf^\hat{L_f}에서 경로의 핵심 간선을 제거한 Lf′^\hat{L'_f}는 레이어드 네트워크가 아닐 수 있다는 것이다. 레이어드 네트워크는 데드엔드(dead-end)가 없다는 성질을 가져야 한다.

데드엔드(dead-end)
ss가 아닌 시작점이거나 tt가 아닌 종료점인 정점. 다시 말해 ss가 아니면서 자신으로 들어오는 간선이 없는 정점, 혹은 tt가 아니면서 자신으로부터 나가는 간선이 없는 정점.

Lf^\hat{L_f}의 경우 GfG_f의 s→ts \rightarrow t 최단 경로를 모아 만들기 때문에 ss가 아닌 시작점이나 tt가 아닌 종료점이 존재하지 않지만, 이와 달리 Lf′^\hat{L'_f}는 레이어드 네트워크인 Lf^\hat{L_f}에서 어떤 s→ts \rightarrow t 경로의 핵심 간선들을 삭제해 만들기 때문에, 핵심 간선들을 삭제하면서 몇 개의 데드엔드가 발생할 수 있다.

아래와 같은 Lf^\hat{L_f}가 있다고 하자. 간단하게, s,ts, t를 제외한 각 레이어의 정점들은 말 그대로 바로 옆에 있는 한 정점으로의 간선만을 가진다고 해보자.

DFS를 통해 증가 경로를 찾았을 때, 간선 (d,t)(d, t)가 핵심 간선이 되어 사라졌다고 하자. 정점 dd는 데드엔드가 되었다.

이제 다음 증가 경로를 찾아보자. DFS를 통해 s→a→ds \rightarrow a \rightarrow d 최단 경로를 따라 가다, dd가 데드엔드임을 확인한다. 쭉 돌아와 s→b→e→ts \rightarrow b \rightarrow e \rightarrow t 경로를 최단 경로로 찾는다고 하자. 이 경로의 핵심 간선은 (e,t)(e, t)였고, 흐름이 추가됨과 함께 사라진다. 이제는 ee도 데드엔드다.

또 증가 경로를 찾으면 경로 s→a→ds \rightarrow a \rightarrow d와 s→b→es \rightarrow b \rightarrow e를 탐색한 후, s→c→f→ts \rightarrow c \rightarrow f \rightarrow t의 경로를 찾게 된다.

만약 데드엔드로 향하는 경로를 적절히 사라지게 했다면 어땠을까? dd가 데드엔드가 됐을 때, dd로 향하는 경로를 없애보자.

s→b→e→ts \rightarrow b \rightarrow e \rightarrow t의 최단 경로를 곧바로 찾을 수 있다. 마찬가지로 (e,f)(e, f)가 핵심 간선이라 ee가 데드엔드가 됐다고 하자. ee로 향하는 경로를 없애면,

s→c→f→ts \rightarrow c \rightarrow f \rightarrow t의 경로를 곧바로 찾을 수 있다.

사실 데드엔드가 남아있는 Lf′^\hat{L'_f}를 그대로 사용하더라도 최단 경로는 (존재한다면) 반드시 찾을 수 있다. 하지만 데드엔드로의 경로를 매번 탐색하게 되면 시간복잡도가 어마무시하게 높아져버릴지도
모른다.

데드엔드를 삭제하는 과정은 다음과 같다. 유량 증가와 함께 간선 (u,v)(u, v)가 삭제된다고 하자.

  1. 만약 uu로부터 나가는 간선이 더 이상 존재하지 않는다면, uu는 데드엔드가 된다.
    • uu가 데드엔드라면, uu로 향하는 모든 간선들을 삭제하고, 해당 간선들에 대해서도 동일한 작업을 수행한다.
  2. 마찬가지로 vv로 들어오는 간선이 더 이상 존재하지 않는다면, vv는 데드엔드가 된다.
    • vv가 데드엔드라면 vv로부터 나가는 모든 간선들을 삭제하고, 해당 간선들에 대해서도 동일한 작업을 수행한다.

이렇게 Lf′^\hat{L'_f}로부터 발생할 수 있는 데드엔드로의 경로를 모두 삭제하면, s→ts \rightarrow t 최단 경로들에 포함되는 간선들만이 남게 되어 Gf′G_{f'}의 레이어드 네트워크 Lf′^\hat{L_{f'}}이 된다.

(4) 최대 유량 찾기

이제 최대 유량을 찾아보자. 최대 유량을 반환할 때까지 아래를 반복한다.

  1. BFS를 통해 잔여 네트워크 GfG_f에 대한 레이어드 네트워크 Lf^\hat{L_f}를 구성한다.
    • s→ts \rightarrow t의 경로를 찾을 수 없다면, 더 이상 증가 경로가 없다는 뜻이므로, ∣f∣|f|를 반환하며 종료한다.
    • 그렇지 않은 경우 자료 구조 LL에 Lf^\hat{L_f}를 넣는다.
  2. LL이 빌 때까지,
    1. LL에서 s→ts \rightarrow t 최단 경로 pp를 찾는다.
    2. pp를 따라 유량을 보낸다.
    3. 더 이상 유량을 흘려 보낼 수 없게 된 pp의 핵심 간선들을 LL에서 삭제한다.
    4. pp의 핵심 간선들을 삭제하면서 데드엔드가 되는 정점들이 있다면, 해당 정점으로의 경로가 존재하지 않도록 만든다.

시간복잡도

디닉 알고리즘의 각 페이즈를 종료하는 데 걸리는 시간은 O(∣V∣∣E∣+∣E∣)=O(∣V∣∣E∣)O(|V||E| + |E|) = O(|V||E|)이다.

각 페이즈에서,

  1. BFS를 통해 잔여 네트워크 GℓG_\ell의 레이어드 네트워크 Lℓ^\hat{L_\ell}를 구성하는 데에는 O(∣E∣)O(|E|)의 시간이 걸린다.
  2. LL이 빌 때까지,
    1. LL에서 s→ts \rightarrow t 최단 경로를 찾아 유량을 보내는 데에는 경로의 길이만큼의 시간이 걸린다. 경로의 길이는 길어봐야 ∣V∣−1|V| - 1이니 O(∣V∣)O(|V|)의 시간이 걸린다.
    2. pp에서 핵심 간선은 많아봐야 경로의 길이만큼 있으니, 이를 제거하는 데에는 O(∣V∣)O(|V|)의 시간이 걸린다.
    3. 데드엔드로의 경로를 제거하는 데에는 O(∣E∣)O(|E|)의 시간이 걸린다.
      • (u,v)(u, v)가 핵심 간선이면서 해당 레이어의 유일한 간선이라고 생각해보자. (u,v)(u, v)를 삭제하는 순간 레이어드 네트워크의 모든 간선이 사라진다.

위 분석대로라면, 2번을 한 번 수행하는 데에는 최대 O(∣V∣+∣E∣)O(|V| + |E|)의 시간이 걸린다. 한 페이즈에서 증가 경로는 많아야 핵심 간선의 수만큼 있고, 핵심 간선은 많아야 O(∣E∣)O(|E|)개 있으므로, 2번은 O(∣E∣)O(|E|)번 반복될 수 있다. 따라서

디닉 알고리즘의 한 페이즈가 종료되는 데 걸리는 시간은 O(∣E∣+∣V∣∣E∣+∣E∣2)=O(∣E∣2)O(|E| + |V||E| + |E|^2) = O(|E|^2)다.

가 아니라, 사실 2-3에서 사라진 간선들은 이후 같은 페이즈의 반복에서는 다시 나타나지 않는다. 때문에 2-3은 한 페이즈 전체에서 많아봐야 O(∣E∣)O(|E|)번 수행될 수 있다. 그러므로 사실 한 페이즈를 종료하는 데 걸리는 시간은 O(∣E∣+∣V∣∣E∣+∣E∣2)O(|E| + |V||E| + |E|^2)가 아니라 O(∣E∣+∣V∣∣E∣+∣E∣)=O(∣V∣∣E∣)O(|E| + |V||E| + |E|) = O(|V||E|)다.

디닉 알고리즘의 총 시간복잡도는 O(∣V∣2∣E∣)O(|V|^2|E|)다.

한 페이즈는 s→ts \rightarrow t 최단 경로의 길이가 같은 것들을 묶은 것이다. 이 경로의 길이는 길어봐야 ∣V∣−1|V| - 1이므로, 서로 다른 페이즈의 개수도 O(∣V∣)O(|V|)다.

한 페이즈가 종료되는 데 O(∣V∣∣E∣)O(|V||E|)의 시간이 걸리니, O(∣V∣)O(|V|)개의 페이즈가 종료되기 위한 총 시간복잡도는 O(∣V∣2∣E∣)O(|V|^2|E|)다.

3. Shimon Even & Alon Itai의 구현

Shimon Even과 Alon Itai의 버전의 디닉 알고리즘은, 지금까지 이야기 해온 디니츠의 오리지널 버전과는 조금 차이가 있다. 가장 큰 차이는 L^(s,t)\hat{L}(s, t)를 사용하지 않고 L(s)L(s)를 사용한다는 점이다.

오리지널 버전에서는 ss에서 BFS를 수행해 L(s)L(s)를 만들고, 다시 tt에서 BFS를 수행해 s→ts \rightarrow t의 최단 경로만을 남겼다.

그런데 사실 꼭 L^(s,t)\hat{L}(s, t)를 어떤 자료구조로 구현해야 할 필요는 없다. s→ts \rightarrow t의 최단 경로 탐색이 가능하기만 하면 되고, 이는 L(s)L(s)만으로도 충분하다.

이 구현에서는 이미 증가 경로가 아님이 확인된 경로를 재탐색하지 않기 위한 백트래킹 테크닉을 하나 사용한다. 이를 이용하면 L(s)L(s)에 있는, tt가 아닌 종점들로 향하는 경로를 탐색하는 일을 막을 수 있고, 또한 간선 삭제로 인해 발생하는 데드 엔드 경로도 굳이 삭제할 필요가 없어진다.

4. 코드(C++)

이번에는 2367번 파티를 디닉 알고리즘을 이용해 풀어보자. Shimon Even과 Alon Itai의 구현을 따른다.

#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;

#define fastio cin.tie(NULL); cout.tie(NULL); ios_base::sync_with_stdio(false)
#define all(v) v.begin(), v.end()

const int INF = 1e9;

struct Flow_Network{

	struct Edge {//간선
		int to;//어디로 가는 간선인지
		int capacity;//용량
		int flow;//흐름

		Edge(int _to, int _cap) : to(_to), capacity(_cap), flow(0) {}

		Edge *reverse;//역방향 간선

		void push(int f) {//(u,v)로 흐름 증가. 역방향 간선인 (v,u)에는 흐름 감소
			flow += f;
			reverse->flow -= f;
		}
	};

	vector<vector<Edge*>> edges;//한 정점으로부터 시작하는 간선들의 리스트
	vector<int> layer, work;//정점들이 속한 레이어, 가장 최근에 본 간선 인덱스
	int source = 0, sink = 0;

	Flow_Network(int N): source(0), sink(N + 1), edges(N + 2), layer(N + 2), work(N + 2) {}

	void add_edge(int u, int v, int c) {//용량이 c인 간선 (u, v)
		Edge *edge = new Edge(v, c);//v로 향하는 용량 c의 간선
		Edge *reverse_edge = new Edge(u, 0);//역방향 간선 (v, u)

		//두 간선의 역방향 관계 설정
		edge->reverse = reverse_edge;
		reverse_edge->reverse = edge;

		edges[u].emplace_back(edge);//정점 u에 간선을 추가
		edges[v].emplace_back(reverse_edge);//정점 v에 간선을 추가
	}

	bool find_vertex_layer() {//레이어드 네트워크 L(s)의 정점 레이어 구성
		queue<int> q;
		q.push(source);

		fill(all(layer), -1);//미방문한 정점 표시
		layer[source] = 0;//소스는 V_0에 속함.

		//BFS를 통한 레이어드 네트워크 구성
		while (!q.empty()) {
			int from = q.front();
			q.pop();

			for (auto &edge: edges[from]) {//연결된 간선들을 보면서
				int to = edge->to;
				if (layer[to] != -1) continue;//방문한 정점인 경우는 제외
				if (edge->capacity - edge->flow > 0) {//잔여 용량이 있는 경우
					q.push(to);//큐에 추가
					layer[edge->to] = layer[from] + 1;//d(v) = d(u) + 1
				}
			}
		}

		return layer[sink] != -1;//싱크에 방문하지 못했다면 더 이상 증가 경로를 찾을 수 없음.
	}


	int augment_flow(int cur, int flow) {//증가 경로를 찾고 해당 경로로 보낼 수 있는 흐름을 찾아 보냄
		if (cur == sink) return flow;

		//현재 정점과 연결된 간선들을 보되, 이미 지나간 건 다시 볼 필요가 없음.
		for (int &i = work[cur]; i < edges[cur].size(); i++) {
			Edge *edge = edges[cur][i];
			int to = edge->to;
			int residual_capacity = edge->capacity - edge->flow;

			if (layer[to] != layer[cur] + 1) continue;//L_f에 포함된 간선이 아니면 제외
			if (residual_capacity <= 0) continue;//잔여 용량이 없으면 제외

			//이후 t로의 경로에 흘려보낼 수 있는 흐름을 찾기
			int f_p = augment_flow(to, min(flow, residual_capacity));

			if (f_p > 0) {//보낼 수 있다면
				edge->push(f_p);//보냄
				return f_p;
			}
		}
		return 0;
	}

	int dinic() {
		int max_flow = 0;//최대 유량

		while (find_vertex_layer()) {//s->t 최단 경로를 찾을 수 있나?
			//최단 경로가 존재한다면

			fill(all(work), 0);//각 정점에서 최근에 본 간선 인덱스를 0으로 설정 
		
			while (true) { 
				//증가 경로를 찾고, 해당 경로로 보낼 수 있는 흐름을 찾아 보냄.
				int f = augment_flow(source, INF);

				//최대 유량 추가
				max_flow += f;
		
				//더 이상 보낼 수 없다면 정지
				if (!f) break;
			}
		}

		return max_flow;
	}
};

int main() {
	fastio;

	int N, K, D;
	cin >> N >> K >> D;

	Flow_Network network(N + D);

	for (int i = 1; i <= D; i++) {
		int M;
		cin >> M;
		network.add_edge(N + i, network.sink, M);
	}

	for (int i = 1; i <= N; i++) {
		network.add_edge(network.source, i, K);

		int Z;
		cin >> Z;

		for (int j = 0; j < Z; j++) {
			int food;
			cin >> food;
			network.add_edge(i, N + food, 1);
		}
	}

	cout << network.dinic();
}

main은 이전과 동일하다. struct Flow_Network 내부만 보자.

struct Edge

struct Edge {//간선
	int to;//어디로 가는 간선인지
	int capacity;//용량
	int flow;//흐름

	Edge(int _to, int _cap) : to(_to), capacity(_cap), flow(0) {}

	Edge *reverse;//역방향 간선

	void push(int f) {//(u,v)로 흐름 증가. 역방향 간선인 (v,u)에는 흐름 감소
		flow += f;
		reverse->flow -= f;
	}
};

Edge는 간선을 담기 위한 구조체다. (u,v)(u, v) 간선이라고 한다면, 이 간선은 edges[u]에 to = v인 간선을 추가한다. push()를 통해 (u,v)(u, v) 간선을 따라 흐름을 보낸다.

add_edge()

void add_edge(int u, int v, int c) {//용량이 c인 간선 (u, v)
		Edge *edge = new Edge(v, c);//v로 향하는 용량 c의 간선
		Edge *reverse_edge = new Edge(u, 0);//역방향 간선 (v, u)

		//두 간선의 역방향 관계 설정
		edge->reverse = reverse_edge;
		reverse_edge->reverse = edge;

		edges[u].emplace_back(edge);//정점 u에 간선을 추가
		edges[v].emplace_back(reverse_edge);//정점 v에 간선을 추가
	}

크게 볼 것은 없다.

dinic()

int dinic() {
	int max_flow = 0;//최대 유량

	while (find_vertex_layer()) {//정점 레이어를 구성

		fill(all(work), 0);//각 정점에서 최근에 본 간선 인덱스를 0으로 설정 
	
		while (true) { 
			//증가 경로를 찾아 해당 경로로 보낼 수 있는 흐름을 찾아 보낸다.
			int f = augment_flow(source, INF);

			//최대 유량 추가
			max_flow += f;
	
			//더 이상 보낼 수 없다면 정지
			if (!f) break;
		}
	}

	return max_flow;
}

find_vertex_layer()

bool find_vertex_layer() {//레이어드 네트워크 L(s)의 정점 레이어 구성
	queue<int> q;
	q.push(source);

	fill(all(layer), -1);//미방문한 정점 표시
	layer[source] = 0;//소스는 V_0에 속함.

	//BFS를 통한 레이어드 네트워크 구성
	while (!q.empty()) {
		int from = q.front();
		q.pop();

		for (auto &edge: edges[from]) {//연결된 간선들을 보면서
			int to = edge->to;
			if (layer[to] != -1) continue;//방문한 정점인 경우는 제외
			if (edge->capacity - edge->flow > 0) {//잔여 용량이 있는 경우
				q.push(to);//큐에 추가
				layer[edge->to] = layer[from] + 1;//d(v) = d(u) + 1
			}
		}
	}

	return layer[sink] != -1;//싱크에 방문하지 못했다면 더 이상 증가 경로를 찾을 수 없음.
}

BFS를 통해 레이어드 네트워크 Lf(s)L_f(s)의 정점 레이어를 구성한다. 소스부터 시작해 연결된 각 간선들을 본다. 미방문한 정점인 경우, 잔여 용량이 있는지 확인한다. 잔여 용량이 있다면 큐에 추가하고, 해당 정점이 어떤 레이어에 속하는지를 기록한다.

만약 layer[sink] == -1이라면 더 이상 싱크로의 경로가 존재하지 않는다는 것, 즉 더 이상의 증가 경로가 없다는 말이다. 여기서 디닉 알고리즘은 종료된다.

GfG_f의 s→ts \rightarrow t 최단 경로들을 담은 Lf^(s,t)\hat{L_f}(s, t)가 아니라, ss에서 모든 정점으로의 최단 경로를 담은 Lf(s)L_f(s)다.

augment_flow()

int augment_flow(int cur, int flow) {//증가 경로를 찾고 해당 경로로 보낼 수 있는 흐름을 찾아 보냄
	if (cur == sink) return flow;//싱크인 경우는 종료한다.

	//현재 정점과 연결된 간선들을 보되, 이미 지나간 건 다시 볼 필요가 없음.
	for (int &i = work[cur]; i < edges[cur].size(); i++) {
		Edge *edge = edges[cur][i];
		int to = edge->to;
		int residual_capacity = edge->capacity - edge->flow;

		if (layer[to] != layer[cur] + 1) continue;//L_f(s)에 포함된 간선이 아니면 제외
		if (residual_capacity <= 0) continue;//잔여 용량이 없으면 제외

		//이후 t로의 경로에 흘려보낼 수 있는 흐름을 찾기
		int f_p = augment_flow(to, min(flow, residual_capacity));

		if (f_p > 0) {//보낼 수 있다면
			edge->push(f_p);//보냄
			return f_p;
		}
	}
	return 0;
}

증가 경로를 찾고 해당 경로로 흐름을 보내는 함수다. 파라미터 flow는 현재 정점 cur까지의 경로에서 흐를 수 있는 흐름으로, 초기값은 INF로 둔다.

현재 보고 있는 정점과 연결된 간선들을 보자. 해당 간선을 (cur,to)(cur, to)라 할 때, 이 Lf(s)L_f(s)에 포함된 간선인지는 layer[to] = layer[cur] + 1인지, 그리고 잔여 용량이 남아있는지를 통해 확인할 수 있다. 만약 그렇다면 flow와 간선의 잔여 용량 중 작은 것을 이후 경로로 흘려보내기를 시도할 수 있다.

DFS가 Lf^\hat{L_f}가 아닌 LfL_f 위에서 수행되므로, tt가 아닌 다른 정점이 DFS의 종점이 되는 경우가 발생할 수 있다. 이 경우에는 아래에서 00이 반환되기에 걸러진다.

눈여겨 볼 것은 vector<int> work를 통해 이미 본 탐색했던 간선들을 스킵하는 테크닉이다. 위 for문에서 i가 증가하는 경우는 edges[cur][i]에 해당하는 간선을 지났을 때 tt로의 증가 경로를 찾지 못하는 경우 밖에 없다. 이 정보를 work[cur]에 저장해둔다면, tt로의 증가 경로가 존재하지 않는 것이 확실한 간선들에 대한 탐색을 걸러낼 수 있기에 실행 시간을 훨씬 줄일 수 있다.

References

0개의 댓글