It will update soon...
13 Jul 2018 | usage곧 업데이트됩니다…
분류 | URL |
---|---|
문제 | 최대 유량 |
참조 라이브러리 | dinic.h, sharifa_header.h |
이 글에서 설명하는 코드 | 06086_최대 유량 |
이 문제는 생긴 것부터가 네트워크 플로우 문제이다. Maximum Flow를 찾는 많은 알고리즘이 있지만, 디닉 알고리즘이 가장 빠르기 때문에 필자는 이 알고리즘을 사용하도록 하겠다.
네트워크 플로우 문제는 모델링이 굉장히 중요한데, 이 문제는 모델링이 어렵지는 않다.
주의할 점으로는 간선을 입력받아 그래프를 생성할 때, 각 파이프가 양방향 수송이 가능하다고 하였으므로 최대 유량을 똑같이 (0이 아닌) cap으로 설정해주어야 한다는 것이다. 또 문자를 정점 번호로 변환할 때 번호가 겹치지 않도록 주의한다.
#include "../library/dinic.h"
#define MAX_V 52
#define S 0 // source
#define T 25 // sink
#define INF 1000000009
int E, cap, u, v;
Dinic network(MAX_V, S, T);
int main_06086() {
ios::sync_with_stdio(false); cin.tie(NULL);
cin >> E;
while (E--) {
char x, y;
cin >> x >> y >> cap;
u = 'A' <= x && x <= 'Z' ? x - 'A' : x - 'a' + 26;
v = 'A' <= y && y <= 'Z' ? y - 'A' : y - 'a' + 26;
network.addEdge(u, v, cap, true);
}
int ans = 0;
while (network.bfs()) {
network.reset_next_v();
while (true) {
int flow = network.dfs(S, INF);
if (!flow)
break;
ans += flow;
}
}
cout << ans;
return 0;
}
주의: 이 코드를 그대로 복붙하여 채점 사이트에 제출하면 당연히 틀린다. 저런 헤더 파일이 채점 사이트에 있을까?
분류 | URL |
---|---|
문제 | 최대 유량 |
응용 문제 | 스포일러 1 |
참조 라이브러리 | sharifa_header.h, bit_library.h |
이 글에서 설명하는 라이브러리 | dinic.h |
그림 출처: wikipedia
이 글에서는 네트워크 플로우(Network Flow) 분야에서 Maximum Flow를 구하는 알고리즘인 디닉 알고리즘에 대해서 설명한다.
Maximum Flow를 구하는 다른 대표적인 알고리즘으로 에드몬드-카프 알고리즘(Edmonds–Karp algorithm), 포드-풀커슨 알고리즘(Ford–Fulkerson algorithm)이 있지만, 현재 PS에서 구현할 만한 알고리즘 중 가장 빠른 알고리즘은 디닉 알고리즘이다. 그래서 여러 개 외울 필요 없이 알고리즘 하나만 알아두는 것이 좋을 듯 하다.
네트워크 플로우에 대한 기본적인 설명을 조금 적어 두려고 한다. 예를 하나 들면, Network Flow는 파이프가 복잡하게 연결되어 있고, 각 파이프는 물이 흐를 수 있는 최대 양이 정해져 있고, source에서 sink방향으로 물이 흐를 때, 물이 흐를 수 있는 최대 양을 구하는 것이라고 보면 된다.
네트워크 플로우의 Maximum Flow는 Minimum Cut과 깊은 연관이 있다.
디닉 알고리즘은 크게 두 단계로 이루어진다.
레벨 그래프는 각 정점들에 source로부터의 최단 거리를 레벨 값을 할당한 그래프이다. 아래 그림에서 source의 레벨은 0이되고 source와 인접한 1번과 2번 정점의 레벨은 1, 이후는 2… 등이 된다. 빨간색 숫자로 적혀 있는 것이 레벨이다.
레벨 그래프는 BFS로 구현한다.
디닉 알고리즘에서는, 유량을 흘려보낼 때 레벨 차이가 딱 1이 나는 정점으로만 유량을 보낼 수 있다. 즉 바로 위의 그림에서의 간선과 같은 곳으로만 보낼 수 있다. 레벨이 같아도 안 된다.
유량을 흘려보내는 것은 DFS로 구현한다. source에서 시작하여, 차단 유량 규칙을 만족하는 정점으로만 따라가면서 최종적으로 sink에 도달할 때까지 탐색하는 과정을 반복한다.
위의 레벨 그래프에서는 다음 세 경로를 DFS로 찾을 수 있다.
(s, 1, 3, 4): 유량 4
(s, 1, 4, t): 유량 6
(s, 2, 4, t): 유량 4
BFS 1번 그리고 DFS를 한번씩 해서는 최대 유량이 찾아지지 않는다. 다만 복잡한 것은 아니고, 위의 과정을 반복해주면 된다.
다시 BFS를 돌려 레벨 그래프를 새로 생성한다.
위의 레벨 그래프에서는 다음 경로를 DFS로 찾을 수 있다.
(s, 2, 4, 3, t): 유량 5
다시 레벨 그래프를 그리면, 더 이상 sink로 가는 경로가 없음(sink의 레벨이 $\inf$)을 알 수 있다. 알고리즘을 종료한다.
BFS는 어려운 부분이 아니기 때문에 설명은 생략하도록 하겠다.
DFS의 구현은 조금 까다롭다.
max_flow
라는 것을 리턴한다. 이는 Network Flow에서 수송량은 경로에 포함된 파이프 최대 유량의 최솟값이기 때문이다. flow
의 계산식을 잘 보면 최대 유량과 min 연산을 취하는 것을 볼 수 있다.int &i
로 되어 있다. i가 레퍼런스로 선언되어 있기 때문에 for loop의 i++
구문에 따라 같이 증가한다(i는 next_v[u]와 값을 공유한다) int dfs(int u, int max_flow) {
if (u == T)
return max_flow;
for (int &i = next_v[u]; i < edges[u].size(); i++) {
int v = edges[u][i].v, cap = edges[u][i].cap;
if (level[u] + 1 == level[v] && cap > 0) {
int flow = dfs(v, min(max_flow, cap));
if (flow > 0) {
edges[u][i].cap -= flow;
edges[v][edges[u][i].ref].cap += flow;
return flow;
}
}
}
return 0;
}
addEdge 함수의 inv는 각 간선이 양방향 수송이 가능하면 true
로 지정하면 된다.
sparse graph일 경우를 대비해 edges를 2차원 배열로 표현하지 않고 대신 역방향 간선에 대한 참조를 저장하고 있다.
이러면 정점이 많을 경우에도 메모리 사용량을 줄일 수 있다.
구현은 다음과 같다.
#pragma once
#include "sharifa_header.h"
struct Edge { // u -> v
int v, cap, ref;
Edge(int v, int cap, int ref) :v(v), cap(cap), ref(ref) {}
};
class Dinic {
int S, T;
vector<vector<Edge> > edges; // graph
// level: 레벨 그래프, next_v: DFS에서 flow 계산 시 역추적에 사용
vector<int> level, next_v;
public:
Dinic(int MAX_V, int S, int T):S(S), T(T) {
edges.resize(MAX_V);
level.resize(MAX_V);
next_v.resize(MAX_V);
}
void addEdge(int u, int v, int cap, bool inv) {
edges[u].emplace_back(v, cap, (int)edges[v].size());
edges[v].emplace_back(u, inv ? cap : 0, (int)edges[u].size() - 1);
}
bool bfs() {
fill(level.begin(), level.end(), -1);
queue<int> q;
level[S] = 0;
q.push(S);
while (!q.empty()) {
int u = q.front(); q.pop();
for (auto edge : edges[u]) {
int v = edge.v, cap = edge.cap;
if (level[v] == -1 && cap > 0) {
level[v] = level[u] + 1;
q.push(v);
}
}
}
return level[T] != -1;
}
void reset_next_v() {
fill(next_v.begin(), next_v.end(), 0);
}
int dfs(int u, int max_flow) {
if (u == T)
return max_flow;
for (int &i = next_v[u]; i < edges[u].size(); i++) {
int v = edges[u][i].v, cap = edges[u][i].cap;
if (level[u] + 1 == level[v] && cap > 0) {
int flow = dfs(v, min(max_flow, cap));
if (flow > 0) {
edges[u][i].cap -= flow;
edges[v][edges[u][i].ref].cap += flow;
return flow;
}
}
}
return 0;
}
};
문제: 최대 유량
문제: 스포일러 문제
풀이: 스포일러 풀이
분류 | URL |
---|---|
문제 | 구간 합 구하기 3 |
참조 라이브러리 | fenwick_tree_2D_BIT.h, sharifa_header.h |
이 글에서 설명하는 코드 | 11658_구간 합 구하기 3 |
이 문제도 역시 인덱스 트리라고 부르는 자료구조를 써도 풀리긴 하지만, 펜윅 트리가 굉장히 간단하기 때문에 이것으로 문제를 풀었다.
원소가 하나씩 업데이트될 때, 구간의 합을 구하라는 문제는 그냥 펜윅 트리를 쓰라는 문제이다. 왜냐하면 BIT가 그에 최적화되어 있기 때문이다.
이 문제는 구간 합 구하기와는 달리 2차원이기 때문에, data 배열도 2차원으로 잡아야 한다. 즉, data[][] 형식이 될 것이다.
구간 합을 구하는 것은 1차원을 2차원으로 확장한 것에 불과하다.
업데이트 연산 역시 2차원으로 확장한 것에 불과하다.
이 문제는 2차원이기 때문에 sum과 update에서 중첩 while문을 볼 수 있을 것이다.
그리고 BOJ 02042(구간 합 구하기) 문제 풀이와는 달리 arr 배열을 만들지 않고 풀이를 적었다.
#include "../library/sharifa_header.h"
#include "../library/fenwick_tree_2D_BIT.h"
int N, m;
int main_11658() {
ios::sync_with_stdio(false); cin.tie(NULL);
cin >> N >> m;
FenwickTree2D BIT(N);
int w, x1, y1, x2, y2, val;
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= N; j++) {
cin >> val;
BIT.update(i, j, val);
}
}
while(m--){
cin >> w >> x1 >> y1;
if (w){
cin >> x2 >> y2;
cout << BIT.sum(x1, y1, x2, y2) << '\n';
} else {
cin >> val;
BIT.update(x1, y1, val);
}
}
return 0;
}
주의: 이 코드를 그대로 복붙하여 채점 사이트에 제출하면 당연히 틀린다. 저런 헤더 파일이 채점 사이트에 있을까?
분류 | URL |
---|---|
문제 | 나무 심기 |
이 글에서 설명하는 코드 | 01280_나무 심기 |
순서대로 하나씩 주어지고, $i$번째에 대한 비용이나 시간이 1부터 $(i-1)$번째까지에만 의존하는 형식이라면 BIT, IT, 삽입 정렬, 우선순위 큐를 한번쯤 떠올려 보는 것이 중요하다.
우선 문제의 정의에 따라, 문제의 정답(=2번 나무부터 N번 나무까지 심는 비용의 곱)을 수식으로 정리하면 다음과 같다.
$ A := $ 문제의 정답
$ p_i := i$번 나무가 심어진 위치
$ c_i := i$번 나무를 심는 비용
$ c_i = \displaystyle\sum_{j=1}^{i-1}{\vert(p_i - p_j)\vert} $
$ A = \displaystyle\prod_{i=2}^n{c_i} $
이제 약간의 수식 변형을 시키자.
$ c_i
= \displaystyle\sum_{j=1}^{i-1}{|(p_i - p_j)|} $
$ = \displaystyle\sum_{j: p_j \le p_i, \ 1 \le j \le i-1}{(p_i - p_j)} +
\displaystyle\sum_{j: p_j \gt p_i, \ 1 \le j \le i-1}{(p_j - p_i)} $
여기서 $ n_i := p_j < p_i $인 $j$의 개수라고 하자. 그러면,
$ c_i = ( n_i \cdot p_i - \displaystyle\sum_{j: p_j \le p_i, \ 1 \le j \le i-1}{p_j} ) + ( \displaystyle\sum_{j: p_j \gt p_i, \ 1 \le j \le i-1}{p_j} - (i-1-n_i) \cdot p_i )$
$ = (2n_i+1-i)\cdot p_i + \displaystyle\sum_{j: p_j \gt p_i, \ 1 \le j \le i-1}{p_j} - \displaystyle\sum_{j: p_j \le p_i, \ 1 \le j \le i-1}{p_j} $
그러면 이제 문제는
을 알아야 하는 문제로 바뀐다($p_i$와 같은 나무는 어느 쪽에 넣어도 상관없다).
이제 펜윅 트리를 쓸 차례다. 펜윅 트리를 두 개 만든다. 하나는 위치에 따른 나무 수의 합을 세는 것이고, 하나는 위치에 따른 나무 좌표의 합을 세는 것이다. 맨 아래의 구현에서 각각 count[]와 summ[]으로 구현되어 있다.
그리고 $i$번째 나무의 위치로 $p_i$가 들어오면, 왼쪽에 있는 나무의 수는 sum($p_i$, count=true)으로, 왼쪽에 있는 나무의 좌표의 합은 sum($p_i$, count=false)로 바로 구할 수 있다.
그리고 업데이트를 할 때는 위치는 $p_i$로 하고 count[]는 +1, summ[]에는 $p_i$만큼 더해 준다(두 배열의 정의에 의해).
오른쪽 나무에 대해 구할 때는 전체에서 왼쪽 나무들의 값을 빼면 어렵지 않게 구할 수 있다.
이번에는 펜윅 트리의 형태가 조금 다르기 때문에 따로 필자의 라이브러리를 사용하지 않았다.
주의 사항이 조금 있다.
#include <iostream>
#include <vector>
using namespace std;
#define ll long long
#define MOD 1000000007
#define MAX_X 200001
class FenwickTree {
public:
vector<ll> count, summ;
FenwickTree() {
count.resize(MAX_X + 1);
summ.resize(MAX_X + 1);
}
void update(int x, int val) {
while (x <= MAX_X) {
++count[x];
summ[x] += val;
x += (x&-x);
}
}
ll sum(int x, bool cnt) {
ll ret = 0;
while (x) {
ret += cnt? count[x] : summ[x];
x &= x-1;
}
return ret;
}
};
FenwickTree BIT;
int N, p_i;
int main_01280() {
//freopen("input.txt", "r", stdin);
ios::sync_with_stdio(false); cin.tie(NULL);
cin >> N >> p_i; ++p_i;
BIT.update(p_i, p_i);
ll ans = 1;
ll p_i_sum = p_i;
for(int i=2;i<=N;i++){
cin >> p_i; ++p_i;
ll n_i = BIT.sum(p_i, true);
ll smaller = BIT.sum(p_i, false);
ll cost = (2 * n_i + 1 - i) * p_i;
cost -= smaller;
cost += p_i_sum - smaller;
cost %= MOD;
p_i_sum += p_i;
ans = (ans*cost) % MOD;
BIT.update(p_i, p_i);
}
cout << ans;
return 0;
}
주의: 이 코드를 그대로 복붙하여 채점 사이트에 제출하면 당연히 틀린다. 저런 헤더 파일이 채점 사이트에 있던가?