货物调度

把城市-日期拆成时间扩展网络,用最小费用最大流同时决定运输路线和跨日库存。

OJ: shumeng

题目 ID: CSP201412E

难度:提高+/省选-

标签:网络流最小费用最大流时间扩展网络

日期: 2026-07-31 16:21

形式化题目

每个城市每天会生产和消耗货物,货物当天可沿道路无限量运输;未消耗的货物必须在某个城市仓库存一晚,且受容量和费用限制。调度方案按每周循环,求一周运输费和库存费之和的最小值。

思路

先看小数据基准。它把每吨货物单独增广,在残量网络中用 SPFA 找当前最短路:

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-31 16:21
 * update_at: 2026-08-17 22:43
 */
// brute.cpp:小数据基准,逐吨使用 SPFA 求最短增广路。
// 与 main.cpp 的最小费用最大流算法结论一致,只适合小数据验证。
#include <bits/stdc++.h>
using namespace std;

const int INF = 1000000000;
const int INF_CAP = 1000000000;

// 一条网络流边:to 为终点,rev 为反向边在终点邻接表中的下标,cap 为剩余容量
struct Edge {
    int to;
    int rev;
    int cap;
    int cost;
};

vector<vector<Edge> > graph;

// 加一条容量 cap、单位费用 cost 的边,并同时加入反向边。
void add_edge(int u, int v, int cap, int cost) {
    Edge forward = {v, (int)graph[v].size(), cap, cost};
    Edge backward = {u, (int)graph[u].size(), 0, -cost};
    graph[u].push_back(forward);
    graph[v].push_back(backward);
}

// 节点编号:城市 city 在第 day 天对应的节点下标。
int node_id(int day, int city, int n) {
    return day * n + city;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m;
    cin >> n >> m;

    int production[10][7] = {};
    int demand[10][7] = {};
    int capacity[10], hold_cost[10];
    for (int city = 0; city < n; city++) {
        for (int day = 0; day < 7; day++) {
            cin >> production[city][day];
        }
        for (int day = 0; day < 7; day++) {
            cin >> demand[city][day];
        }
        cin >> capacity[city] >> hold_cost[city];
    }

    struct Road {
        int u;
        int v;
        int cost;
    };
    vector<Road> roads(m);
    for (int i = 0; i < m; i++) {
        cin >> roads[i].u >> roads[i].v >> roads[i].cost;
        roads[i].u--;
        roads[i].v--;
    }

    // 建时间扩展网络:源点 S 提供产量,汇点 T 消耗需求,
    // 仓库边负责从当天流入下一天,道路边在同一天内双向运输。
    int source = 7 * n;
    int sink = source + 1;
    graph.assign(sink + 1, vector<Edge>());
    int total = 0;
    for (int city = 0; city < n; city++) {
        for (int day = 0; day < 7; day++) {
            int current = node_id(day, city, n);
            add_edge(source, current, production[city][day], 0);
            add_edge(current, sink, demand[city][day], 0);
            add_edge(current, node_id((day + 1) % 7, city, n), capacity[city], hold_cost[city]);
            total += demand[city][day];
        }
    }
    for (int day = 0; day < 7; day++) {
        for (int i = 0; i < m; i++) {
            add_edge(node_id(day, roads[i].u, n), node_id(day, roads[i].v, n), INF_CAP, roads[i].cost);
            add_edge(node_id(day, roads[i].v, n), node_id(day, roads[i].u, n), INF_CAP, roads[i].cost);
        }
    }

    // 每轮用 SPFA 在残量网络中找最短增广路,只增广 1 吨货物。
    long long answer = 0;
    int sent = 0;
    int nodes = sink + 1;
    while (sent < total) {
        vector<int> distance(nodes, INF);
        vector<int> in_queue(nodes, 0);
        vector<int> previous_node(nodes);
        vector<int> previous_edge(nodes);
        queue<int> q;
        distance[source] = 0;
        q.push(source);
        in_queue[source] = 1;

        while (!q.empty()) {
            int u = q.front();
            q.pop();
            in_queue[u] = 0;
            for (int i = 0; i < (int)graph[u].size(); i++) {
                Edge &e = graph[u][i];
                if (e.cap == 0 || distance[e.to] <= distance[u] + e.cost) {
                    continue;
                }
                distance[e.to] = distance[u] + e.cost;
                previous_node[e.to] = u;
                previous_edge[e.to] = i;
                if (!in_queue[e.to]) {
                    in_queue[e.to] = 1;
                    q.push(e.to);
                }
            }
        }

        // 沿记录的前驱边回退,更新残量并累加本次增广的费用。
        int add = 1;
        for (int v = sink; v != source; v = previous_node[v]) {
            Edge &e = graph[previous_node[v]][previous_edge[v]];
            e.cap -= add;
            graph[v][e.rev].cap += add;
            answer += e.cost;
        }
        sent += add;
    }

    cout << answer << '\n';
    return 0;
}

建立时间扩展网络

x(d,i)x(d,i) 表示城市 ii 在第 dd 天。源点连向 x(d,i)x(d,i) 的容量是生产量;x(d,i)x(d,i) 连向汇点的容量是需求量。每条道路在每一天各建一条双向边,费用为道路费;x(d,i)x(d,i) 连向下一天同城节点的边容量为仓库容量、费用为库存费。第 7 天的“下一天”是第 1 天。

在这张时间扩展网络中发送全周总需求量,恰好对应一周循环调度。正式程序采用最小费用最大流:每轮以势能修正残量边费用后用 Dijkstra 求最短增广路,并按路径瓶颈容量一次增广。

网络结构示意

这张图展示一个城市两条道路连接下的网络局部(S 源点、T 汇点):

text
S --产量--> x(1,1) --需求--> T
              |
              | 仓库边(容量 = 仓库容量,费用 = 库存费)
              v
           x(2,1) --需求--> T
              |
              | 仓库边
              v
           x(3,1) ...

x(1,1) 还与道路另一端城市 jj 的 x(1,j) 有双向容量无限、费用为道路费的边。仓库边只改变日期,道路边只改变城市,两类边组合出完整的调度方案。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-31 16:21
 * update_at: 2026-08-17 23:01
 */
#include <bits/stdc++.h>
using namespace std;

const long long INF = (1LL << 60);
const int INF_CAP = 1000000000;

struct Edge {
    int to;
    int rev;
    int cap;
    int cost;
};

class MinCostFlow {
public:
    vector<vector<Edge> > graph;

    explicit MinCostFlow(int n) : graph(n) {}

    void add_edge(int from, int to, int cap, int cost) {
        Edge forward = {to, (int)graph[to].size(), cap, cost};
        Edge backward = {from, (int)graph[from].size(), 0, -cost};
        graph[from].push_back(forward);
        graph[to].push_back(backward);
    }

    long long solve(int source, int sink, int need) {
        int node_count = (int)graph.size();
        vector<long long> potential(node_count, 0);
        vector<long long> distance(node_count);
        vector<int> previous_node(node_count);
        vector<int> previous_edge(node_count);
        long long answer = 0;
        int flow = 0;

        while (flow < need) {
            fill(distance.begin(), distance.end(), INF);
            distance[source] = 0;
            priority_queue<pair<long long, int>, vector<pair<long long, int> >,
                           greater<pair<long long, int> > > queue;
            queue.push(make_pair(0, source));

            while (!queue.empty()) {
                pair<long long, int> current = queue.top();
                queue.pop();
                long long current_distance = current.first;
                int u = current.second;
                if (current_distance != distance[u]) continue;

                for (int i = 0; i < (int)graph[u].size(); i++) {
                    Edge &edge = graph[u][i];
                    if (edge.cap == 0) continue;
                    long long next_distance = current_distance + edge.cost
                                              + potential[u] - potential[edge.to];
                    if (next_distance >= distance[edge.to]) continue;
                    distance[edge.to] = next_distance;
                    previous_node[edge.to] = u;
                    previous_edge[edge.to] = i;
                    queue.push(make_pair(next_distance, edge.to));
                }
            }

            if (distance[sink] == INF) break;
            for (int i = 0; i < node_count; i++) {
                if (distance[i] != INF) potential[i] += distance[i];
            }

            int add = need - flow;
            for (int v = sink; v != source; v = previous_node[v]) {
                add = min(add, graph[previous_node[v]][previous_edge[v]].cap);
            }
            for (int v = sink; v != source; v = previous_node[v]) {
                Edge &edge = graph[previous_node[v]][previous_edge[v]];
                answer += (long long)add * edge.cost;
                edge.cap -= add;
                graph[v][edge.rev].cap += add;
            }
            flow += add;
        }
        return answer;
    }
};

int node_id(int day, int city, int n) {
    return day * n + city;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m;
    cin >> n >> m;
    int production[100][7] = {};
    int demand[100][7] = {};
    int storage_capacity[100];
    int storage_cost[100];
    for (int city = 0; city < n; city++) {
        for (int day = 0; day < 7; day++) cin >> production[city][day];
        for (int day = 0; day < 7; day++) cin >> demand[city][day];
        cin >> storage_capacity[city] >> storage_cost[city];
    }

    struct Road {
        int u;
        int v;
        int cost;
    };
    vector<Road> roads(m);
    for (int i = 0; i < m; i++) {
        cin >> roads[i].u >> roads[i].v >> roads[i].cost;
        roads[i].u--;
        roads[i].v--;
    }

    int source = 7 * n;
    int sink = source + 1;
    MinCostFlow flow(sink + 1);
    int total_need = 0;
    for (int city = 0; city < n; city++) {
        for (int day = 0; day < 7; day++) {
            int current = node_id(day, city, n);
            if (production[city][day] > 0) {
                flow.add_edge(source, current, production[city][day], 0);
            }
            if (demand[city][day] > 0) {
                flow.add_edge(current, sink, demand[city][day], 0);
                total_need += demand[city][day];
            }

            int next_day = (day + 1) % 7;
            flow.add_edge(current, node_id(next_day, city, n),
                          storage_capacity[city], storage_cost[city]);
        }
    }

    for (int day = 0; day < 7; day++) {
        for (int i = 0; i < m; i++) {
            int u = node_id(day, roads[i].u, n);
            int v = node_id(day, roads[i].v, n);
            flow.add_edge(u, v, INF_CAP, roads[i].cost);
            flow.add_edge(v, u, INF_CAP, roads[i].cost);
        }
    }

    cout << flow.solve(source, sink, total_need) << '\n';
    return 0;
}

复杂度

V=7n+2V=7n+2E=O(7n+7m)E=O(7n+7m)FF 是全周总需求量。时间复杂度为 O(FElogV)O(FE\log V),空间复杂度为 O(V+E)O(V+E)

总结

周期性物流的关键是把“日期”也放进状态。跨日库存边负责时间推进,第 7 天连回第 1 天保证调度可以无限重复;最小费用最大流则在所有运输和存储方案中选出总代价最小的一种。

图示解析

这张图串起时间扩展网络的四类边和求解目标:

text
节点 x(d, i):城市 i 的第 d 天
  |- S -> x(d, i)                   容量 = 生产量,费用 = 0
  |- x(d, i) -> T                   容量 = 需求量,费用 = 0
  |- x(d, i) -> x((d + 1) mod 7, i) 容量 = 仓库容量,费用 = 库存费
  `- x(d, i) <-> x(d, j)            容量 = 无限,费用 = 道路费
       `- 发送全周总需求量的最小费用流

同日道路边只改变城市,不改变日期;仓库边只改变日期,不改变城市。第 7 天连回第 1 天后,任何跨周库存都会被模型自然计入。