포드 풀커슨은 잔여 용량과 역방향 간선으로 최대 유량을 어떻게 구하나요?
최대 유량은 출발점에서 도착점으로 보낼 수 있는 양을 간선 용량 안에서 최대로 만듭니다. 포드 풀커슨은 잔여 그래프에서 보낼 수 있는 경로를 찾고, 역방향 간선으로 이전 선택을 수정할 여지를 남깁니다.
적용 조건·범위: 이 구현의 용량은 0 이상의 안전한 정수이고 전체 용량 합도 안전한 정수 범위로 제한합니다. 정수이면 한 번 성공할 때 총 유량이 최소 1 증가하므로 최대 유량 F에 대해 성공 횟수는 F 이하입니다. 0 용량 간선이나 자기 루프는 증가 경로에 도움을 주지 않습니다. 경로가 전혀 없으면 총 유량은 0이고, 출발점과 도착점이 같은 입력은 이 문제의 계약에서 거절합니다. 소수·무리수 용량까지 포함하는 일반적인 종료 논의와 섞지 마세요.
본문 근거: Competitive Programmer’s Handbook — 20.1 Ford–Fulkerson algorithm
읽기 전에 알아두면 좋아요
아래 개념이 익숙하면 본문을 바로 읽으세요. 낯선 개념만 링크에서 복습하면 됩니다.
- 그래프 DFS: 방문 표시로 순환을 멈추고 연결을 따라가기깊이 우선 탐색(DFS)은 한 경로를 끝까지 따라간 뒤 돌아옵니다. 방문한 곳을 표시하는 방법을 확인합니다.
- JavaScript 그래프 구현: 인접 리스트·인접 행렬 비교와 BFS그래프는 점인 정점과 점 사이의 연결인 간선으로 관계를 나타냅니다.
개념과 예제는 실행하지 않아도 읽을 수 있습니다. 내 컴퓨터·터미널에서 직접 실행하는 과정은 선택 실습입니다.
과정 위치와 선택 복습 안내
과정 위치: 유량·상태 압축과 계산 복잡도 (선택) · 알고리즘 개념
과정 위치는 이 글이 속한 묶음입니다. 이 단계의 모든 개념을 먼저 읽을 필요는 없습니다. 위에 연결한 개념 중 낯선 내용만 복습하세요. 문제의 조건·상태·갱신 순서는 익숙한 언어로 설명하면 됩니다.
JavaScript로 문법·실행 복습하기 (선택)
개념 글의 코드는 JavaScript 예시입니다. 문제 화면에서는 JavaScript·Python·Java·C++·C# 중 익숙한 언어의 함수를 작성하세요. 본문의 Node.js 입출력 예시는 로컬 실행을 위한 별도 안내입니다.

최대 유량은 출발점에서 도착점으로 보낼 수 있는 양을 간선 용량 안에서 최대로 만듭니다. 포드 풀커슨은 잔여 그래프에서 보낼 수 있는 경로를 찾고, 역방향 간선으로 이전 선택을 수정할 여지를 남깁니다.
선수 지식: 방향 그래프·DFS 방문 표시·경로 추적·최솟값
- 용량 제한·중간 정점의 유량 보존 이해하기
- 증가 경로의 병목과 역방향 갱신 추적하기
- 평행 간선 보존과 최대 유량·최소 컷 확인하기
앞으로 더 보낼 양과 되돌릴 양을 함께 저장합니다
방향 간선 u→v의 용량이 c이고 현재 유량이 f이면, 앞으로 더 보낼 수 있는 잔여 용량은 c−f입니다. 반대 방향에는 이미 보낸 양을 취소할 수 있는 잔여 용량 f가 생깁니다. 역방향 잔여 간선은 물리적인 관이 새로 생겼다는 뜻이 아니라, 현재의 유량 배정을 수정할 수 있다는 뜻입니다. 중간 정점에서는 들어온 유량과 나간 유량이 같아야 하므로 경로 전체를 같은 양만큼 갱신합니다.

예제는 입력 간선 하나마다 전방·역방향 잔여 간선을 한 쌍씩 새로 만듭니다. 두 잔여 간선은 서로의 배열 위치를 저장합니다. u→v가 여러 개 있거나 원래의 v→u도 있는 경우에도 각각 독립된 쌍을 가지므로 덮어쓰지 않습니다. 단순히 목적지 번호를 객체의 키로 삼아 하나의 용량만 저장하면 평행 간선이 사라지거나, 원래의 반대 방향 간선과 취소용 간선이 섞일 수 있습니다.
경로에서 가장 작은 잔여 용량만큼 보냅니다
현재 잔여 용량이 양수인 간선만 따라 출발점에서 도착점까지 DFS를 합니다. 발견할 때 방문 표시와 부모 간선 번호를 저장하므로 순환을 반복하지 않습니다. 도착점에 닿으면 부모 간선을 거꾸로 따라가며 최소 잔여 용량을 구합니다. 이 값이 경로의 병목이며, 그보다 많이 보내면 적어도 한 간선의 용량을 넘습니다. 병목만큼 보내고 전방 잔여 용량은 줄이며 짝인 역방향 잔여 용량은 같은 양만큼 늘립니다.

간선 순서가 0→1, 1→2, 2→3, 0→2, 1→3이고 모든 용량이 1이라고 하겠습니다. DFS는 먼저 0→1→2→3을 선택할 수 있습니다. 그 뒤에는 0→2→1→3이 가능하며, 여기의 2→1은 첫 경로에서 보낸 1→2 유량을 취소하는 잔여 간선입니다. 마지막 유량은 0→1→3과 0→2→3에 각각 1이 되어 총 2입니다.
| 증가 단계 | 선택 경로 | 병목 | 1→2의 유량 | 총 유량 |
|---|---|---|---|---|
| 처음 | 없음 | 없음 | 0 | 0 |
| 1 | 0→1→2→3 | 1 | 1 | 1 |
| 2 | 0→2→1→3 | 1 | 0으로 취소 | 2 |
더 갈 수 없는 경계가 최대라는 근거가 됩니다
잔여 그래프에서 출발점으로부터 도착점까지 경로가 더는 없으면 알고리즘이 멈춥니다. 이때 출발점에서 아직 도달할 수 있는 정점 집합을 S라고 하면 도착점은 S 밖에 있습니다. 원래 그래프에서 S에서 바깥으로 향하는 간선들의 용량 합이 컷의 용량입니다. 어떤 유량도 이 경계를 넘는 용량보다 클 수 없으므로 모든 컷은 최대 유량의 상한입니다.

종료 시 S에서 바깥으로 나가는 전방 잔여 용량은 0입니다. 또한 반대편에서 S로 들어오는 원래 간선에 양의 유량이 남아 있다면 그 취소용 잔여 간선으로 바깥에 도달할 수 있으므로 그런 유량도 남지 않습니다. 따라서 이 경계를 넘는 순유량이 컷 용량과 같아집니다. 예제의 반환값에는 총 유량과 입력 간선별 유량, 집합 S를 뜻하는 sourceSide를 함께 담습니다. 작은 입력에서는 모든 S 후보의 컷 용량을 직접 비교해도 결과를 확인할 수 있습니다.
정수 용량과 경로 선택에 따른 시간 한계를 지킵니다
이 구현의 용량은 0 이상의 안전한 정수이고 전체 용량 합도 안전한 정수 범위로 제한합니다. 정수이면 한 번 성공할 때 총 유량이 최소 1 증가하므로 최대 유량 F에 대해 성공 횟수는 F 이하입니다. 0 용량 간선이나 자기 루프는 증가 경로에 도움을 주지 않습니다. 경로가 전혀 없으면 총 유량은 0이고, 출발점과 도착점이 같은 입력은 이 문제의 계약에서 거절합니다. 소수·무리수 용량까지 포함하는 일반적인 종료 논의와 섞지 마세요.

DFS 한 번에는 방문·부모 초기화와 잔여 간선 확인으로 O(V+E)가 들고, 마지막 실패 탐색도 있으므로 전체 상한은 O((V+E)(F+1))입니다. 이는 F의 크기에 의존하므로 큰 정수 용량에서는 느려질 수 있습니다. BFS로 증가 경로를 고르는 에드몬즈 카프와 시간 보장을 혼동하지 않습니다. 잔여 간선과 상태의 추가 공간은 O(V+E)이며, 재귀 DFS 깊이는 최악 O(V)이므로 큰 그래프에서는 명시적 스택이나 다른 구현을 고려해야 합니다.
전체 코드와 직접 실행
개념을 읽었다면 아래 예제를 직접 실행하며 상태 변화를 확인할 수 있습니다.
전체 JavaScript 코드와 실행 방법 펼치기
별도 패키지는 필요하지 않습니다. Node.js가 처음이라면 브라우저와 Node.js 시작 안내부터 확인하세요.
algorithm-ford-fulkerson-residual-flow.mjs 파일을 만들고 아래 전체 코드를 저장합니다.
function maxFlow(n, edges, source, sink) {
if (!Number.isInteger(n) || n < 2 || !Array.isArray(edges)) {
throw new RangeError('정점 수 2 이상과 간선 배열이 필요합니다.');
}
const validVertex = v => Number.isInteger(v) && v >= 0 && v < n;
if (!validVertex(source) || !validVertex(sink)) throw new RangeError('출발·도착 정점 범위를 확인하세요.');
if (source === sink) throw new RangeError('출발점과 도착점은 서로 달라야 합니다.');
const graph = Array.from({length:n}, () => []);
const residual = [];
let totalCapacity = 0;
for (const edge of edges) {
if (!Array.isArray(edge) || edge.length !== 3) throw new TypeError('간선은 [u,v,capacity]입니다.');
const [u,v,capacity] = edge;
if (!validVertex(u) || !validVertex(v)) throw new RangeError('간선 정점 번호가 범위를 벗어났습니다.');
totalCapacity += capacity;
if (!Number.isSafeInteger(capacity) || capacity < 0 || !Number.isSafeInteger(totalCapacity)) {
throw new RangeError('용량과 그 합은 0 이상의 안전한 정수여야 합니다.');
}
const index = residual.length;
residual.push({to:v,remaining:capacity,reverse:index+1});
residual.push({to:u,remaining:0,reverse:index});
graph[u].push(index);
graph[v].push(index+1);
}
let value = 0;
while (true) {
const seen = Array(n).fill(false);
const parentEdge = Array(n).fill(-1);
seen[source] = true;
function visit(u) {
if (u === sink) return true;
for (const index of graph[u]) {
const edge = residual[index];
if (edge.remaining <= 0 || seen[edge.to]) continue;
seen[edge.to] = true;
parentEdge[edge.to] = index;
if (visit(edge.to)) return true;
}
return false;
}
if (!visit(source)) {
return {
value,
flows:edges.map((edge,index) => edge[2]-residual[index*2].remaining),
sourceSide:Array.from({length:n}, (_,v) => v).filter(v => seen[v])
};
}
let amount = Infinity;
for (let v = sink; v !== source;) {
const edge = residual[parentEdge[v]];
amount = Math.min(amount,edge.remaining);
v = residual[edge.reverse].to;
}
for (let v = sink; v !== source;) {
const edge = residual[parentEdge[v]];
edge.remaining -= amount;
residual[edge.reverse].remaining += amount;
v = residual[edge.reverse].to;
}
value += amount;
}
}
const edges = [[0,1,1],[1,2,1],[2,3,1],[0,2,1],[1,3,1]];
console.log(JSON.stringify(maxFlow(4,edges,0,3)));
console.log(JSON.stringify(maxFlow(2,[[0,1,2],[0,1,3],[1,0,7]],0,1)));
파일이 있는 폴더의 터미널에서 실행합니다.
node algorithm-ford-fulkerson-residual-flow.mjs
기대 출력
{"value":2,"flows":[1,0,1,1,1],"sourceSide":[0]}
{"value":5,"flows":[2,3,0],"sourceSide":[0]}
비용과 적용 범위
정점 V개, 입력 방향 간선 E개, 정수 최대 유량 F일 때 DFS 기반 시간 상한 O((V+E)(F+1)), 추가 공간 O(V+E)입니다. 재귀 깊이는 O(V)입니다. 용량과 용량 합은 0 이상의 안전한 정수이며 출발점과 도착점은 달라야 합니다.

직접 확인해 보기
모든 용량이 1인 0→1, 1→2, 2→3, 0→2, 1→3에서 첫 경로를 0→1→2→3으로 골랐습니다. 다음 증가 경로는 무엇이며, 1→2의 최종 유량은 얼마일까요?

답과 이유 확인
0→2→1→3을 잔여 경로로 사용합니다. 2→1은 기존 1→2의 유량을 취소하므로 그 최종 유량은 0입니다. 두 단위가 각각 0→1→3과 0→2→3으로 흐릅니다.
함께 읽을 글
- JavaScript 그래프 구현: 인접 리스트·인접 행렬 비교와 BFS — 인접 목록과 방문 표시를 복습한 뒤 잔여 그래프 탐색에 적용합니다.
- 다익스트라의 원리: 거리 완화와 오래된 후보 건너뛰기 — 최단 비용 경로와 용량을 최대화하는 유량이 서로 다른 목적임을 비교합니다.
- 완전탐색과 쌍 비교: 중복 없는 후보 열거와 순위 계산 — 작은 정점 집합의 모든 컷을 열거해 독립적인 상한을 확인하는 관점으로 이어집니다.
참고 자료
- Competitive Programmer’s Handbook — 20.1 Ford–Fulkerson algorithm — 잔여 경로·역방향 갱신·최대 유량과 최소 컷, 정수 용량에서의 증가 경로 논리.
이 글이 도움이 되었나요?
이어서 공부하기
체크포인트: 예제의 상태 변화를 손으로 추적하고 아래 문제에 적용하세요. 값이 없는 경우와 원소 하나인 경우도 설명해 보세요. 작은 기준 풀이와 비교하고 경로 복원·역방향 취소·입력 크기에 따른 비용을 설명하세요.
다음 추천 · 알고리즘 개념: 비트마스킹 입문: 선택한 항목을 숫자에 기록하기
권장 순서는 학습 가이드와 같습니다. 다른 순서는 접힌 참고 목록에서 확인할 수 있습니다. 읽음 표시는 숙달이나 채점 결과가 아닙니다.
원래 과정 목록 순서 (선택)
읽음 기록
읽음 — / 전체 30개 · 필수 0개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.