포드 풀커슨 최대 유량: 잔여 용량과 역방향 간선으로 선택 수정하기

·설명 약 6분·작성: 해비·블로그 소개

포드 풀커슨은 잔여 용량과 역방향 간선으로 최대 유량을 어떻게 구하나요?

최대 유량은 출발점에서 도착점으로 보낼 수 있는 양을 간선 용량 안에서 최대로 만듭니다. 포드 풀커슨은 잔여 그래프에서 보낼 수 있는 경로를 찾고, 역방향 간선으로 이전 선택을 수정할 여지를 남깁니다.

적용 조건·범위: 이 구현의 용량은 0 이상의 안전한 정수이고 전체 용량 합도 안전한 정수 범위로 제한합니다. 정수이면 한 번 성공할 때 총 유량이 최소 1 증가하므로 최대 유량 F에 대해 성공 횟수는 F 이하입니다. 0 용량 간선이나 자기 루프는 증가 경로에 도움을 주지 않습니다. 경로가 전혀 없으면 총 유량은 0이고, 출발점과 도착점이 같은 입력은 이 문제의 계약에서 거절합니다. 소수·무리수 용량까지 포함하는 일반적인 종료 논의와 섞지 마세요.

본문 근거: Competitive Programmer’s Handbook — 20.1 Ford–Fulkerson algorithm

읽기 전에 알아두면 좋아요

아래 개념이 익숙하면 본문을 바로 읽으세요. 낯선 개념만 링크에서 복습하면 됩니다.

개념과 예제는 실행하지 않아도 읽을 수 있습니다. 내 컴퓨터·터미널에서 직접 실행하는 과정은 선택 실습입니다.

과정 위치와 선택 복습 안내

과정 위치: 유량·상태 압축과 계산 복잡도 (선택) · 알고리즘 개념

과정 위치는 이 글이 속한 묶음입니다. 이 단계의 모든 개념을 먼저 읽을 필요는 없습니다. 위에 연결한 개념 중 낯선 내용만 복습하세요. 문제의 조건·상태·갱신 순서는 익숙한 언어로 설명하면 됩니다.

JavaScript로 문법·실행 복습하기 (선택)

개념 글의 코드는 JavaScript 예시입니다. 문제 화면에서는 JavaScript·Python·Java·C++·C# 중 익숙한 언어의 함수를 작성하세요. 본문의 Node.js 입출력 예시는 로컬 실행을 위한 별도 안내입니다.

포드 풀커슨 최대 유량: 잔여 용량과 역방향 간선으로 선택 수정하기 주제를 표현한 밝은 일러스트와 작은 곰 표지

최대 유량은 출발점에서 도착점으로 보낼 수 있는 양을 간선 용량 안에서 최대로 만듭니다. 포드 풀커슨은 잔여 그래프에서 보낼 수 있는 경로를 찾고, 역방향 간선으로 이전 선택을 수정할 여지를 남깁니다.

선수 지식: 방향 그래프·DFS 방문 표시·경로 추적·최솟값

  • 용량 제한·중간 정점의 유량 보존 이해하기
  • 증가 경로의 병목과 역방향 갱신 추적하기
  • 평행 간선 보존과 최대 유량·최소 컷 확인하기

앞으로 더 보낼 양과 되돌릴 양을 함께 저장합니다

방향 간선 u→v의 용량이 c이고 현재 유량이 f이면, 앞으로 더 보낼 수 있는 잔여 용량은 c−f입니다. 반대 방향에는 이미 보낸 양을 취소할 수 있는 잔여 용량 f가 생깁니다. 역방향 잔여 간선은 물리적인 관이 새로 생겼다는 뜻이 아니라, 현재의 유량 배정을 수정할 수 있다는 뜻입니다. 중간 정점에서는 들어온 유량과 나간 유량이 같아야 하므로 경로 전체를 같은 양만큼 갱신합니다.

용량 5에서 2를 보냈을 때 정방향 잔여 3과 역방향 취소 가능 2를 나타내는 두 노드
용량 c=5에 유량 f=2를 배정한 예입니다. 앞으로는 3을 더 보낼 수 있고, 반대로는 기존 유량 2를 취소할 수 있습니다. 역방향 화살표는 새 물리 간선이 아닙니다. 원본 보기 (새 탭)

예제는 입력 간선 하나마다 전방·역방향 잔여 간선을 한 쌍씩 새로 만듭니다. 두 잔여 간선은 서로의 배열 위치를 저장합니다. u→v가 여러 개 있거나 원래의 v→u도 있는 경우에도 각각 독립된 쌍을 가지므로 덮어쓰지 않습니다. 단순히 목적지 번호를 객체의 키로 삼아 하나의 용량만 저장하면 평행 간선이 사라지거나, 원래의 반대 방향 간선과 취소용 간선이 섞일 수 있습니다.

경로에서 가장 작은 잔여 용량만큼 보냅니다

현재 잔여 용량이 양수인 간선만 따라 출발점에서 도착점까지 DFS를 합니다. 발견할 때 방문 표시와 부모 간선 번호를 저장하므로 순환을 반복하지 않습니다. 도착점에 닿으면 부모 간선을 거꾸로 따라가며 최소 잔여 용량을 구합니다. 이 값이 경로의 병목이며, 그보다 많이 보내면 적어도 한 간선의 용량을 넘습니다. 병목만큼 보내고 전방 잔여 용량은 줄이며 짝인 역방향 잔여 용량은 같은 양만큼 늘립니다.

첫 경로 0→1→2→3에서 역방향 2→1로 선택을 수정하고, 최종 두 경로에 1씩 흐르는 과정
모든 원래 간선의 용량은 1입니다. 두 번째 잔여 경로의 2→1은 먼저 보낸 1→2를 취소합니다. 최종 두 경로에 1씩 흘러 총 유량은 2입니다. 원본 보기 (새 탭)

간선 순서가 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에서 바깥으로 향하는 간선들의 용량 합이 컷의 용량입니다. 어떤 유량도 이 경계를 넘는 용량보다 클 수 없으므로 모든 컷은 최대 유량의 상한입니다.

정점 0만 포함한 집합 S와 밖으로 향하는 용량 1의 간선 두 개, 컷 용량 2
종료 후 잔여 그래프에서 도달 가능한 집합은 S={0}입니다. 그림에는 원래 간선의 유량/용량을 표시했습니다. S 밖으로 나가는 두 간선의 용량 합은 2로, 최대 유량과 같습니다. 원본 보기 (새 탭)

종료 시 S에서 바깥으로 나가는 전방 잔여 용량은 0입니다. 또한 반대편에서 S로 들어오는 원래 간선에 양의 유량이 남아 있다면 그 취소용 잔여 간선으로 바깥에 도달할 수 있으므로 그런 유량도 남지 않습니다. 따라서 이 경계를 넘는 순유량이 컷 용량과 같아집니다. 예제의 반환값에는 총 유량과 입력 간선별 유량, 집합 S를 뜻하는 sourceSide를 함께 담습니다. 작은 입력에서는 모든 S 후보의 컷 용량을 직접 비교해도 결과를 확인할 수 있습니다.

정수 용량과 경로 선택에 따른 시간 한계를 지킵니다

이 구현의 용량은 0 이상의 안전한 정수이고 전체 용량 합도 안전한 정수 범위로 제한합니다. 정수이면 한 번 성공할 때 총 유량이 최소 1 증가하므로 최대 유량 F에 대해 성공 횟수는 F 이하입니다. 0 용량 간선이나 자기 루프는 증가 경로에 도움을 주지 않습니다. 경로가 전혀 없으면 총 유량은 0이고, 출발점과 도착점이 같은 입력은 이 문제의 계약에서 거절합니다. 소수·무리수 용량까지 포함하는 일반적인 종료 논의와 섞지 마세요.

성공 횟수 F 이하와 마지막 실패 1회에 각 DFS의 V+E 비용을 곱한 시간 상한
정수 용량에서 성공하는 탐색은 최대 F회이고 종료를 확인하는 실패 탐색이 1회 더 있습니다. 한 번의 DFS에 O(V+E)가 들어 전체 상한은 O((V+E)(F+1))입니다. 성공 횟수가 항상 F회라는 뜻은 아닙니다. 원본 보기 (새 탭)

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 이상의 안전한 정수이며 출발점과 도착점은 달라야 합니다.

원래 간선 5개에 정방향과 역방향 레코드 10개를 두고 방문·부모 배열에 각 4칸을 저장하는 구조
본문의 V=4·E=5 예제를 저장 공간으로 나타냈습니다. 잔여 레코드는 2E=10개, 방문·부모 배열은 각각 V=4칸입니다. 배열의 숫자는 저장값이 아닌 칸의 인덱스이며, 전체 추가 공간은 O(V+E)입니다. 원본 보기 (새 탭)

직접 확인해 보기

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

용량 1인 간선 5개에서 첫 경로 0→1→2→3만 강조하고 다음 경로를 묻는 문제
확인 문제의 시작 상태입니다. 모든 간선의 용량은 1이고 짙은 선은 첫 번째로 선택한 경로입니다. 다음 경로와 1→2의 최종 유량은 답을 펼치기 전에 생각해 보세요. 원본 보기 (새 탭)
답과 이유 확인

0→2→1→3을 잔여 경로로 사용합니다. 2→1은 기존 1→2의 유량을 취소하므로 그 최종 유량은 0입니다. 두 단위가 각각 0→1→3과 0→2→3으로 흐릅니다.

참고 자료

이 글이 도움이 되었나요?

조회 중

이어서 공부하기

체크포인트: 예제의 상태 변화를 손으로 추적하고 아래 문제에 적용하세요. 값이 없는 경우와 원소 하나인 경우도 설명해 보세요. 작은 기준 풀이와 비교하고 경로 복원·역방향 취소·입력 크기에 따른 비용을 설명하세요.

다음 추천 · 알고리즘 개념: 비트마스킹 입문: 선택한 항목을 숫자에 기록하기

같은 주제에서 다음 개념과 예제로 이해를 넓힙니다.

권장 순서는 학습 가이드와 같습니다. 다른 순서는 접힌 참고 목록에서 확인할 수 있습니다. 읽음 표시는 숙달이나 채점 결과가 아닙니다.

원래 과정 목록 순서 (선택)

읽음 기록

읽음 — / 전체 30개 · 필수 0개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기