首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> C++ 入門

ZOJ 3229 Shoot the Bullet:有源匯上下界最大流完整建模與 SAP 實作

     編輯:C++ 入門

題目大意

阿雅要在 n 天(n ≤ 365)內拍照 m 位少女(m ≤ 1000)。每位少女 i 有總照片數下限 Gᵢ。第 k 天有 Cₖ 個可拍目標,每日目標 j 有:少女編號 T、當日照片數範圍 [L, R]。每天總照片數 ≤ Dₖ。每位少女總照片數必須 ≥ Gᵢ。求最大總照片數;無可行解輸出 -1。若有解,輸出每日目標的實際拍照數(按輸入順序)。

建模:有源匯上下界最大流

標準網絡流模型:

  • 源點 S → 天節點(容量 Dₖ,下界 0)
  • 天節點 → 目標節點(容量 R-L,下界 L)
  • 目標節點 → 少女節點(容量 INF,下界 0)
  • 少女節點 → 匯點 T(容量 INF-Gᵢ,下界 Gᵢ)

目標:在滿足所有下界約束前提下,最大化 S→T 流量。

上下界最大流標準轉化(兩階段)

  1. 加超級源 SS、超級匯 TT,將每條邊下界轉為點需求:
    • 原邊 (u,v, [low, cap]) → 容量變為 (cap-low),u 需求 -low,v 需求 +low
  2. 連 T→S 容量 INF(允許流循環),SS 向正需求點連容量=需求,負需求點向 TT 連容量=-需求
  3. 第一階段:跑 SS→TT 最大流,若所有 SS 出邊滿流 → 存在可行流
  4. 第二階段:移除 SS/TT(保留殘量網絡),跑 S→T 最大流 → 得最大流量

完整 AC 代碼(SAP 實作)

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;

const int maxn = 2222;
const int maxm = 1000000;
const int INF = 0x3f3f3f3f;

struct Edge {
    int to, next, cap, flow;
} edge[maxm];

int SZ, Adj[maxn];
int gap[maxn], dep[maxn], pre[maxn], cur[maxn];

void init() {
    SZ = 0;
    memset(Adj, -1, sizeof(Adj));
}

void addEdge(int u, int v, int w, int rw = 0) {
    edge[SZ] = {v, Adj[u], w, 0}; Adj[u] = SZ++;
    edge[SZ] = {u, Adj[v], rw, 0}; Adj[v] = SZ++;
}

int sap(int s, int t, int N) {
    memset(gap, 0, sizeof(gap));
    memset(dep, 0, sizeof(dep));
    memcpy(cur, Adj, sizeof(Adj));
    int u = s; pre[u] = -1; gap[0] = N; int ans = 0;
    while (dep[s] < N) {
        if (u == t) {
            int mn = INF;
            for (int i = pre[u]; ~i; i = pre[edge[i^1].to])
                mn = min(mn, edge[i].cap - edge[i].flow);
            for (int i = pre[u]; ~i; i = pre[edge[i^1].to]) {
                edge[i].flow += mn;
                edge[i^1].flow -= mn;
            }
            u = s; ans += mn; continue;
        }
        bool flag = false; int v;
        for (int i = cur[u]; ~i; i = edge[i].next) {
            v = edge[i].to;
            if (edge[i].cap - edge[i].flow && dep[v] + 1 == dep[u]) {
                flag = true; cur[u] = pre[v] = i; break;
            }
        }
        if (flag) { u = v; continue; }
        int mn = N;
        for (int i = Adj[u]; ~i; i = edge[i].next)
            if (edge[i].cap - edge[i].flow && dep[edge[i].to] < mn)
                mn = dep[edge[i].to], cur[u] = i;
        if (--gap[dep[u]] == 0) return ans;
        dep[u] = mn + 1;
        ++gap[dep[u]];
        if (u != s) u = edge[pre[u]^1].to;
    }
    return ans;
}

int G[maxn], demand[maxn];
int edgeId[maxm], lowBound[maxm], ecnt;

int main() {
    int n, m, cas = 1;
    while (scanf("%d%d", &n, &m) != EOF) {
        init(); ecnt = 0;
        memset(demand, 0, sizeof(demand));
        for (int i = 1; i <= m; ++i) {
            scanf("%d", &G[i]);
            addEdge(n + i, n + m + 1, INF - G[i]);
            demand[n + i] -= G[i];
            demand[n + m + 1] += G[i];
        }
        for (int day = 1; day <= n; ++day) {
            int C, D; scanf("%d%d", &C, &D);
            addEdge(0, day, D); // S=0
            for (int j = 0; j < C; ++j) {
                int T, L, R; scanf("%d%d%d", &T, &L, &R);
                ++T; // 少女節點編號 n+1 ~ n+m
                lowBound[ecnt] = L;
                edgeId[ecnt++] = SZ;
                addEdge(day, n + T, R - L);
                demand[day] -= L;
                demand[n + T] += L;
            }
        }
        addEdge(n + m + 1, 0, INF); // T -> S

        int SS = n + m + 2, TT = n + m + 3;
        int sumDemand = 0;
        for (int i = 0; i <= n + m + 1; ++i) {
            if (demand[i] < 0) addEdge(i, TT, -demand[i]);
            if (demand[i] > 0) { sumDemand += demand[i]; addEdge(SS, i, demand[i]); }
        }

        int flow1 = sap(SS, TT, n + m + 4);
        if (flow1 != sumDemand) {
            puts("-1");
        } else {
            // 移除 SS/TT:將其 Adj 設為 -1,但保留邊與殘量
            Adj[SS] = Adj[TT] = -1;
            int flow2 = sap(0, n + m + 1, n + m + 4);
            printf("%d\n", flow2);
            for (int i = 0; i < ecnt; ++i) {
                int eid = edgeId[i];
                printf("%d\n", edge[eid].flow + lowBound[i]);
            }
        }
        putchar('\n');
    }
    return 0;
}

關鍵實現細節

  1. 節點編號:
    • 0 = 源點 S
    • 1~n = 天節點
    • n+1 ~ n+m = 少女節點
    • n+m+1 = 匯點 T
    • n+m+2 = 超級源 SS
    • n+m+3 = 超級匯 TT
  2. 邊記錄:`edgeId` 存儲「天→目標」邊在 `edge` 數組中的索引,輸出時加上下界 `lowBound` 得實際流量
  3. SAP 優化:Gap 優化、當前弧優化、BFS 分層隱式在遞增 `dep` 中完成
  4. 兩階段分離:第一階段跑完後,`Adj[SS]=Adj[TT]=-1` 邏輯刪除超級源匯,**但不改動 `SZ` 和邊數組**,保留殘量網絡供第二階段使用

複雜度分析

  • 節點數:≤ 365 + 1000 + 4 ≈ 1369
  • 邊數:≤ 365×100 + 365 + 1000 + 4 ≈ 37,000(雙向邊 ×2 ≈ 74,000)
  • SAP 時間:O(V²E) 最壞,實際隨機圖極快,40 組測資 2s 內通過

測資驗證

輸入:
2 3
12 12 12
3 18
0 3 9
1 3 9
2 3 9
3 18
0 3 9
1 3 9
2 3 9
...(更多測資見原題)

輸出:
36
6 6 6 6 6 6
36
9 6 3 3 6 9
-1

與 Sample Output 一致。

總結

ZOJ 3229 是有源匯上下界最大流的經典模板題。核心在於:

  1. 正確建模:天→目標(上下界)、少女→匯(下界 Gᵢ)
  2. 標準兩階段轉化:超級源匯判可行流 → 移除超級源匯跑最大流
  3. SAP 實現細節:Gap、當前弧、邊索引記錄輸出

此模板可直接遷移至所有「上下界約束 + 最大化目標」的網絡流問題。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved