차분 제약 시스템: 그래프 이론으로 푸는 부등식 문제

여러 개의 선형 부등식으로 표현된 제약 조건 하에서 각 변수의 최댓값이나 최솟값을 구해야 할 때, 그래프 이론의 최단경로/최장경로 알고리즘을 활용할 수 있다. 이를 차분 제약 시스템(Difference Constraints System)이라 한다.

핵심 원리: 부등식의 방향성

부등식 xi ≤ xj + ck정점 j에서 정점 i로 가는 가중치 ck의 유향 간선으로 해석한다.

목표그래프 해석알고리즘
x[i]의 최댓값모든 상한 중 최소값최단경로 (SPFA/Dijkstra)
x[i]의 최솟값모든 하한 중 최댓값최장경로 (SPFA)

문제 유형별 변환 규칙

// 등식
A = B        →  A ≥ B  &&  B ≥ A

// 엄격한 부등식 (정수 범위에서)
A < B        →  B ≥ A + 1
A > B        →  A ≥ B + 1

// 비엄격한 부등식
A ≤ B        →  B ≥ A
A ≥ B        →  A ≥ B + 0

필수 조건: 슈퍼 소스점

모든 정점에 대한 절대적 하한/상한이 필요하다. 예를 들어 xi ≥ C가 주어지면, 가상의 소스점 S=0에서 각 정점 i로 가중치 C의 간선을 연결한다. 이를 슈퍼 소스점이라 한다.

또한 슈퍼 소스점에서 모든 간선에 도달 가능해야 하므로, 그래프의 연결성을 반드시 확인해야 한다.

예제 1: 사탕 분배 (최솟값 → 최장경로)

각 아이에게 최소 몇 개의 사탕을 줘야 하는지 구하는 문제. 모든 조건을 만족하는 최솟값을 구해야 하므로 최장경로로 해결한다.

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const int MAX_V = 100010;
const int MAX_E = 300010;

int vertexCnt, edgeInput;
int head[MAX_V], to[MAX_E], weight[MAX_E], nxt[MAX_E], edgeIdx;
ll maxDist[MAX_V];
int inQueue[MAX_V], cycleCnt[MAX_V];
bool visited[MAX_V];

void initGraph() {
    memset(head, -1, sizeof(head));
    edgeIdx = 0;
}

void makeEdge(int u, int v, int w) {
    to[edgeIdx] = v;
    weight[edgeIdx] = w;
    nxt[edgeIdx] = head[u];
    head[u] = edgeIdx++;
}

bool longestPath() {
    int front = 0, rear = 1;
    memset(maxDist, 0xc0, sizeof(maxDist));  // -∞
    memset(cycleCnt, 0, sizeof(cycleCnt));
    
    maxDist[0] = 0;
    inQueue[0] = 0;
    visited[0] = true;
    
    while (front != rear) {
        int cur = inQueue[front++];
        if (front == MAX_V) front = 0;
        visited[cur] = false;
        
        for (int i = head[cur]; ~i; i = nxt[i]) {
            int nxtVertex = to[i];
            if (maxDist[nxtVertex] < maxDist[cur] + weight[i]) {
                maxDist[nxtVertex] = maxDist[cur] + weight[i];
                cycleCnt[nxtVertex] = cycleCnt[cur] + 1;
                
                // n+1개 정점(슈퍼 소스 포함)이로 n+1 이상이면 음수/양수 사이클
                if (cycleCnt[nxtVertex] >= vertexCnt + 1) 
                    return false;
                
                if (!visited[nxtVertex]) {
                    inQueue[rear++] = nxtVertex;
                    if (rear == MAX_V) rear = 0;
                    visited[nxtVertex] = true;
                }
            }
        }
    }
    return true;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    cin >> vertexCnt >> edgeInput;
    initGraph();
    
    for (int i = 0; i < edgeInput; i++) {
        int type, a, b;
        cin >> type >> a >> b;
        if (type == 1) {           // A = B
            makeEdge(b, a, 0);
            makeEdge(a, b, 0);
        } else if (type == 2) {    // A < B → B ≥ A+1
            makeEdge(a, b, 1);
        } else if (type == 3) {    // A ≥ B
            makeEdge(b, a, 0);
        } else if (type == 4) {    // A > B → A ≥ B+1
            makeEdge(b, a, 1);
        } else {                    // A ≤ B
            makeEdge(a, b, 0);
        }
    }
    
    // 슈퍼 소스점(0)에서 각 정점으로 최소 1개 조건
    for (int i = 1; i <= vertexCnt; i++) {
        makeEdge(0, i, 1);
    }
    
    if (!longestPath()) {
        cout << -1 << '\n';
    } else {
        ll total = 0;
        for (int i = 1; i <= vertexCnt; i++) total += maxDist[i];
        cout << total << '\n';
    }
    
    return 0;
}

예제 2: 구간 선택 문제 (접두사 합 + 최장경로)

배열에서 각 구간 [a,b]에 대해 최소 c개를 선택해야 할 때, 전체 선택 개수의 최솟값을 구한다. 접두사 합(prefix sum) 기법을 활용한다.

정의: S[i] = 1부터 i까지 선택한 정수의 개수 (S[0] = 0)

#include <bits/stdc++.h>
using namespace std;

const int LIMIT = 50001;
const int MAX_N = LIMIT + 10;
const int MAX_E = 150010;

int rangeCount;
int head[MAX_N], to[MAX_E], weight[MAX_E], nxt[MAX_E], edgeIdx;
int longestDist[MAX_N], cycleQueue[MAX_N];
bool inQueue[MAX_N];

void init() {
    memset(head, -1, sizeof(head));
    edgeIdx = 0;
}

void addDirectedEdge(int u, int v, int w) {
    to[edgeIdx] = v;
    weight[edgeIdx] = w;
    nxt[edgeIdx] = head[u];
    head[u] = edgeIdx++;
}

void spfaLongest() {
    memset(longestDist, 0xc0, sizeof(longestDist));
    longestDist[0] = 0;
    
    int front = 0, rear = 0;
    cycleQueue[rear++] = 0;
    inQueue[0] = true;
    
    while (front != rear) {
        int cur = cycleQueue[front++];
        if (front == MAX_N) front = 0;
        inQueue[cur] = false;
        
        for (int i = head[cur]; ~i; i = nxt[i]) {
            int nxtVertex = to[i];
            if (longestDist[nxtVertex] < longestDist[cur] + weight[i]) {
                longestDist[nxtVertex] = longestDist[cur] + weight[i];
                if (!inQueue[nxtVertex]) {
                    inQueue[nxtVertex] = true;
                    cycleQueue[rear++] = nxtVertex;
                    if (rear == MAX_N) rear = 0;
                }
            }
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    cin >> rangeCount;
    init();
    
    // S[i] - S[i-1] ∈ [0, 1]  (각 정수는 0개 또는 1개 선택)
    for (int i = 1; i <= LIMIT; i++) {
        addDirectedEdge(i - 1, i, 0);    // S[i] ≥ S[i-1] + 0
        addDirectedEdge(i, i - 1, -1);   // S[i-1] ≥ S[i] - 1
    }
    
    // 구간 [a,b]에서 최소 c개 선택: S[b] - S[a-1] ≥ c
    for (int i = 0; i < rangeCount; i++) {
        int a, b, c;
        cin >> a >> b >> c;
        a++; b++;  // 1-based 보정
        addDirectedEdge(a - 1, b, c);    // S[b] ≥ S[a-1] + c
    }
    
    spfaLongest();
    cout << longestDist[LIMIT] << '\n';
    
    return 0;
}

사이클 판별의 의미

경우의미결과
최단경로에서 음수 사이클상한이 계속 감소 → 모순해 없음
최장경로에서 양수 사이클하한이 계속 증가 → 모순해 없음
사이클 없음유한한 해 존재정상 계산

구현 체크리스트

  1. 모든 부등식을 xj ≥ xi + w 형태로 통일
  2. 해당 형태에 맞춰 i → j 방향으로 가중치 w의 간선 생성
  3. 절대 조건(상수 부등식)은 슈퍼 소스점에서 연결
  4. 슈퍼 소스점에서 모든 정점 도달 가능하도록 그래프 보완
  5. 최댓값 = 최단경로 / 최솟값 = 최장경로 선택
  6. SPFA로 사이클(음수/양수) 탐지하며 결과 계산

태그: 차분제약 SPFA 최단경로 최장경로 그래프이론

7월 11일 02:13에 게시됨