把城市-日期拆成时间扩展网络,用最小费用最大流同时决定运输路线和跨日库存。
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;
}建立时间扩展网络
令
在这张时间扩展网络中发送全周总需求量,恰好对应一周循环调度。正式程序采用最小费用最大流:每轮以势能修正残量边费用后用 Dijkstra 求最短增广路,并按路径瓶颈容量一次增广。
网络结构示意
这张图展示一个城市两条道路连接下的网络局部(S 源点、T 汇点):
text
S --产量--> x(1,1) --需求--> T
|
| 仓库边(容量 = 仓库容量,费用 = 库存费)
v
x(2,1) --需求--> T
|
| 仓库边
v
x(3,1) ...x(1,1) 还与道路另一端城市
代码
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;
}复杂度
设
总结
周期性物流的关键是把“日期”也放进状态。跨日库存边负责时间推进,第 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 天后,任何跨周库存都会被模型自然计入。