// 네트워크 플로우

// 네트워크 상에서 최대한 얼마나 많은 양의 유량을 흘려보낼수 있는가를 묻는 알고리즘
// 일반적으로 BFS를 이용한 에드몬드 카프 알고리즘을 이용한다
// 에드몬드 카프 알고리즘은 음의 유량을 반드시 고려해주어야 한다
// 플로우의 경우 순서는 상관없다
// 최대유량은 시작점에서 나가는 유량 == 도착점에서 들어오는 유량

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

#define MAX 10001
#define inf 987654321

int N;
ll cp[MAX][MAX], flow[MAX][MAX], pre[MAX]; // cp : capacity, flow : flow, pre : previous visit now
vector<int> v[MAX]; // 서로 연결된 정점

ll maxflow(int start, int end) {
	queue<int> q;
	ll ret = 0; // ret is maximum flow(answer)

	while (1) { // end 정점에 방문하지 못했다면 계속적으로 반복한다

		while (!q.empty()) q.pop();
		memset(pre, -1, sizeof(pre)); // 방문배열을 초기화!
		q.push(start);

		while (!q.empty()) {
			int num = q.front();
			q.pop();
			if (num == end) break; // 만약 끝정점에 도달했다면 탐색끝

			for (int i = 0; i < v[num].size(); i++) {
				// 흐를 수 있는 유량이 있고, 이 노드를 방문하지 않았다면
				if (cp[num][v[num][i]] - flow[num][v[num][i]] > 0 && pre[v[num][i]] == -1) {

					q.push(v[num][i]), pre[v[num][i]] = num;
				}
			}
		}
		if (pre[end] == -1) break;

		ll min_flow = inf; // 가장 많이 흐를 수 있는 유량을 확인하기 위해 INF로 초기화해서 min으로 값을 맞춰간다
		for (int i = end; i != start; i = pre[i]) {
			min_flow = min(min_flow, cp[pre[i]][i] - flow[pre[i]][i]);
		}
		for (int i = end; i != start; i = pre[i]) {
			flow[pre[i]][i] += min_flow; // 현재 노드에서 다음 노드로 흐르는 유량은 + 처리를 해준다
			flow[i][pre[i]] -= min_flow; // 다음 노드에서 현재 노드로 흐르는 유량은 - 처리를 해준다
		}
		ret += min_flow;// ret는 최대유량으로, min_flow를 계속 더해주면 된다
	}

	return ret;
}

int main() {

	while (1) {
		// a <-> b 를 서로 연결해준다
		v[a].push_back(b);
		v[b].push_back(a);
		// 용량은 양쪽으로 흐르기 때문에 a<->b 방향 모두 += w 처리를 해준다
		cp[a][b] += w;
		cp[b][a] += w;
	}
	
    maxflow(start, end);
}
// 위상정렬
// 순서가 정해져있는 작업을 차례대로 수행해야 할 때 사용
// non cycle & direct graph

// 알고리즘 순서
// 진입차수 : 한 정점으로 들어오는 다른 정점의 갯수
// 1. 진입차수가 0인 정점을 queue에 삽입
// 2. queue에서 정점을 꺼내 연결된 모든 간선을 제거
// 3. 간선 제거 이후에 진입차수가 0이 된 정점을 다시 queue에 넣어준다 ==> 항상 진입차수가 0인 정점만 큐에 삽입한다
// 4. 2,3번을 반복, 모든 정점을 방문하기 전에 queue가 빈다면 사이클이 존재하는 것이고, 모든 정점을 방문했다면 queue에서 꺼낸 순서가 위상정렬의 순서

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

#define MAX 10

int n, inDegree[MAX]; // inDegree : 진입차수를 의미
vector<int> a[MAX];

void wsort() {
	int result[MAX];
	queue<int> q;

	//진입 차수가 0인 노드를 queue에 삽입
	for (int i = 1; i <= n; i++) {
		if (inDegree[i] == 0) q.push(i);
	}

	//위상 정렬이 완전히 수행하려면 정확히 N개의 노드를 방문
	for (int i = 1; i <= n; i++) {
		// N개를 방문하기 전에 queue가 빈다면 사이클이 발생 -> 위상정렬 불가능
		if (q.empty()) return;

		int x = q.front();
		q.pop();
		result[i] = x; // queue에서 꺼낸 x는 위상정렬의 순서를 지키고 있는 노드
		for (int i = 0; i < a[x].size(); i++) {
			int y = a[x][i];
			if (--inDegree[y] == 0) q.push(y); // 진입을 제거해주고 차수가 0이면 queue에 삽입
		}
	}

	// 순서 출력
	for (int i = 1; i <= n; i++) {
		printf("%d ", result[i]);
	}
}
// SCC strong connected component
// 방향 그래프일때만 고려가능하다
// 사이클이 발생하는 경우 무조건 SCC에 해당한다
// 타잔(tarjan)알고리즘
// 모든 정점에 대해서 DFS를 수행해서 SCC를 찾는다
// 한 정점에서 출발해서 다시 출발 정점으로 돌아올 수 있는 경로에 한해서 SCC가 존재한다

// DFS 중에 발견한 정점이 스택내에 들어가있다면 이 정점이 나의 부모정점이다 라는 의미가 된다
// 이때부터 리턴을 하다가 자기 자신과 부모가 같은 정점으로 되돌아오면 출발 정점이 됨을 의미한다
// 부모값이 자신보다 더 작은 값으로 갱신되면서 부모정점으로 되돌아가게 되는 탐색

#include <iostream>
#include <vector>
#include <algorithm>
#include <stack>
using namespace std;
#define MAX 10000

bool finished[MAX]; // 현재 특정한 노드에 대한 DFS가 끝났는지 확인
int id, d[MAX]; // id : 각 노드마다 고유한 번호를 할당할 변수, d : 정점마다 할당된 번호를 담기 위함
vector<int> a[MAX];
vector<vector<int> > SCC;
stack<int> s;

// DFS는 총 정점의 갯수만큼 실행
int dfs(int x) {
	d[x] = ++id; // 노드마다 고유한 번호를 할당 (맨처음 부모로 설정된 값)
	s.push(x);

	int parent = d[x]; // 초기에는 id 자체를 고유 번호로 지정

	for (int i = 0; i < a[x].size(); i++) {
		int y = a[x][i]; // x와 연결된 정점 y

		// ** 더 작은 값으로 부모를 가리키도록 탐색한다 **

		// 해당노드를 방문한 적이 없다면 dfs를 수행
		if (d[y] == 0) parent = min(parent, dfs(y));
		// 방문은 했는데 처리가 끝나지 않은 정점이라면
		else if (!finished[y]) parent = min(parent, d[y]);
	}
	
	// 부모노드가 자기 자신인 경우
	if (parent == d[x]) {
		// 스택에서 자기자신이 나올떄까지 뽑아서 SCC에 저장
		vector<int> scc;
		while (1) {
			int t = s.top();
			s.pop();
			scc.push_back(t);
			finished[t] = true; // 처리완료됨을 의미
			if (t == x) break; // 자기자신을 만나면 종료
		}
		SCC.push_back(scc);
	}

	return parent;
}

int main() {

	int N; // N is node_cnt

	// for dfs i 1 to N
	for (int i = 1; i <= N; i++) {
		// check ths not visit node
		if (d[i] == 0) dfs(i);
	}

}

 

과제를 하면서 알게된 방법을 적어보려고 한다

 

과제는 트리의 단말노드 <-> 단말노드 사이의 가중치 합에 대한 최댓값을 구하는 문제인데

처음에는 백준에서 풀었었던 트리의 지름(1967번, 1167번) 이 기억나서 

dfs 2번의 과정이면 충분히 해결할 수 있을거라고 생각하고 문제를 접근했다.

결과는 WA

 

트리의 지름의 경우 "임의의 노드"를 기준으로 하기 때문에 

단말노드와 단말노드 간의 거리를 구하기 위해 내부코드만 살짝 바꿔주었는데

뭔가 반례가 있나보다

단말노드 간의 정확한 계산은 dfs 을 2번 거치는 트리의지름 방법으로는 해결할 수 없다.

 

단말노드간의 정확한 계산방법을 풀이한다

계산방법은 "후위순회의 접근방법을 통한 자식을 관찰?하는 방법" 이다

후위 순회는 좌->우 자식을 전부 확인하고 자신을 확인하는 순회법이다

좌, 우 자식의 반환값을 통해 현재 노드의 정보를 갱신해가는 방법으로 해결할 수 있다.

 

다음의 트리를 기준으로 최댓값을 찾아보자

 

루트노드인 -15의 가중치를 가진 노드부터 시작해서

후위순회하듯이 좌측, 우측 자식의 반환값을 받을 것이다

반환값의 형태는 ( a, b ) 의 2개의 값을 가지는 한 쌍이다

 

( a, b ) 의 형태에서

a 는 두 자식중에서 큰 가중치를 골라 현재 가중치와 더하는

즉 현재노드의 자식에 위치한 단말노드까지의 최댓값을 의미하고

b 는 두 자식의 가중치와 현재 가중치를 모두 더하는

즉 현재노드의 자식에 위한 단말노드 간의 최댓값을 의미한다

 

좀더 간단하게 말하자면

a는 현재노드부터 좌/우 자식중 가장 큰 가중치를 갖는 경로,

b는 a를 통해 좌, 우자식을 둘다 연결하는 => 단말노드와 단말노드 간의 최댓값 경로가 된다

 

이 방법을 이용하여 -8 의 가중치를 갖는 노드의 리턴값을 예를 들면 다음과 같다

단말노드는 자식자체가 없기 때문에 가중치값을 쌍으로 반환하게 된다

비교대상은 항상 a(좌측) 값으로 이루어진다

2, 6 중에서 6이 크므로 -8과 더해서 a:-2 이라는 값이 만들어지고,

2, 6, -8을 더한 b:0 이라는 값이 만들어진다

이떄 -2는 -8 노드 기준 우측자식의 단말노드부터 현재노드까지의 최댓값을 의미하고

0은 -8 노드 기준 좌/우측 자식의 단말노드부터 현재노드까지의 최댓값을 의미하게 된다

 

이런식으로 재귀를 통해 다음과 같은 결과를 얻을 수 있다.

최댓값에 대한 비교는 항상 b로 이루어지고, 다음 결과를 통해 27이 정답이 됨을 알 수 있다

+ Recent posts