【题解】洛谷 P3980 [NOI2008] 志愿者招募 [最大流最小费用]
P3980 [NOI2008] 志愿者招募 - 洛谷 (luogu.com.cn)
0.思考
时间复杂度首先排除模拟,dp 也需要枚举,贪心没想到而且时空复杂度都不对(太少了)。
确定是要上算法的题,先总结下题意:
- 有 n 天,第 i 天需要 a[i] 个志愿者
- 志愿者可以覆盖连续区间 [s , t]
- 目标是最小费用满足所有天的需求
- 是一个覆盖问题,每个志愿者覆盖一个连续时间段
关键词:最小费用、多个需求、覆盖连续。
单看前两个想到了网络流-最大流最小费用,我们把时间轴上的每一天离散当作节点,就有:
- 节点 0:源点
- 节点 1, 2, ..., n + 1:对应第 1, 2, ..., n + 1 天开始时
- 节点 n + 2:汇点
这样每天志愿者数量的流动与需求就像是 “流” 一样。
1.进一步分析
同时我们有每个节点(除源汇节点)的的入流 = 出流,对应到题目中就有:
第 i - 1 天延续来的 + 第 i 天新招募的 = 第 i 天延续到 i + 1 天的 + 第 i 天结束工作的
那么第 i - 1 和第 i 天就应该有条边,第 i 天新招募的也应该引流到第 i 天。
第 i 天应该出流到第 i + 1 天,但第 i 天结束工作的不应该直接出流到第 i 天。
因为第 i 天结束工作的第 i 天开始时还在工作,所以应该出流到第 i + 1 天。
至于覆盖问题,如果直接 s 为起点 t 为结束点的某种志愿者,从第 s 天连到第 t + 1 天。
这样就会跳过 s 到 t + 1 之间的节点,如何保证这些节点一定会被满足需求呢?
考虑把第 i 天到第 i + 1 天的边流量设为 -a[i],这代表任何流量经过第 i 天都要减 a[i],费用为 0。
负的边权不好处理,考虑天的边权都加上 INF,这样相互之间的大小关系不变,要求不变。
(因为流量是无穷大的,所以真正在意的只有之间的大小关系)
这样在源点的冲流下,INF - a[i] 小的点(需求大的)会先被填平,流量变为 0。
这代表着这些点不能再被经过,需要绕路,也就是走 “志愿者边”。
而以 s 为起点 t 为结束点的某种志愿者,从第 s 天连到第 t + 1 天,
流量为 INF,这代表可以选无数个志愿者,费用为 c。
这样保证了走 “志愿者边” 的情况都是需求大的天先走,走了就代表跳过流量为 0 的点。
再具体些,源点只连节点 1,汇点只连 n + 2。
第一遍走网络流时,由于 spfa 每一步都求最小费用,
会先将全部的天依次走一遍(所有费用为 0 的边)。
所有的边的流量都会减掉 ,相当于需求最大的那天变为 0。
剩下的天变为与 的差值,差值越大代表着还有更多流量可以霍霍,更少用志愿者。
由于网络流-最大流最小费用的底层逻辑,需求较大的天会先变为 0,
天然的需要更多志愿者去填补它与别的天权值的差值,只有填补成一样的权值,
才能和其他边共用志愿者(需求一样)。
因为优先保证最大流,也就是所有点都被 “填平”,最大可流量是 INF(本来是 0 但之前加了)。
算法又会在其中选择最小费用的方法,求出最优答案。
2.代码
我用的是多次 spfa 判断残余网络里是否有增广路 + dinic 算法求最大流。
SPFA 时间复杂度 ,最多调用
次。
其中 是节点数量,本题中为天数
。
是边数,等于 (天数 + 志愿者种数 + 两条源汇边) * 2,
。
也就是 。
dinic 时间复杂度 ,
因为加了弧优化,单个回合每条边只会遍历一次。
总共时间复杂度 ,可以通过。
注释代码:
#include<bits/stdc++.h>
using namespace std;typedef long long LL;
const int N = 1010; // 点:0 ~ n+2 → 最多 1003
const int M = 3e4; // 边:每条志愿者边 + 相邻天数 + 源汇边,*2 后约 22000
const LL INF = 2e9;int n, m;
LL a[N];struct edge {int x, y;LL c, f; // 费用与可流量 int pre;
} e[M];
int elen, last[N], cur[N]; void ins(int x, int y, LL c, LL f) { // 我流链式前向星 elen ++; e[elen] = {x, y, c, f, last[x]}; last[x] = elen;elen ++; e[elen] = {y, x, -c, 0, last[y]}; last[y] = elen;
}int st, ed;
LL mx_flow, mn_cost;
LL d[N]; bool v[N];bool spfa() {queue<int> Q; Q.push(st);memset(d, 0x7f, sizeof(d)); LL inf = d[1]; d[st] = 0;memset(v, 0, sizeof(v)); v[st] = 1;while (!Q.empty()) {int x = Q.front(); Q.pop(); v[x] = 0;for (int k = last[x]; k; k = e[k].pre) if (e[k].f > 0) {int y = e[k].y;if (d[y] > d[x] + e[k].c) {d[y] = d[x] + e[k].c;if (!v[y]) {Q.push(y);v[y] = 1;}}}}return d[ed] != inf; // 可以到达 ed,即还有增广流量路径
}LL dinic(int x, LL f) {LL sx = 0; v[x] = 1; // 当前点没有流量了,下次不要走 if (x == ed) {mn_cost += f * d[ed];return f;}for (int k = cur[x]; k; k = e[k].pre) if (e[k].f > 0) {int y = e[k].y; cur[x] = k; // 弧优化,这条边已经被走过了,同一回合不可能再有流量了if (d[y] == d[x] + e[k].c && !v[y]) { // 如果 y 是 x 的下一层,且还有流量 LL sy = dinic(y, min(f - sx, e[k].f));sx += sy;e[k].f -= sy; e[k ^ 1].f += sy;if (sx == f) { // 可用流量全部已走完 return f;}} }if (sx) {v[x] = 0; // 还有可用流量,下次再来 }return sx;
}int main () {ios::sync_with_stdio(false);cin.tie(0);cin >> n >> m;elen = 1; // 2 ^ 1 = 3, 3 ^ 1 = 2memset(last, 0, sizeof(last));st = 0; ed = n + 2;ins(st, 1, 0, INF);for (int i = 1; i <= n; i ++) {cin >> a[i];ins(i, i + 1, 0, INF - a[i]);}ins(n + 1, ed, 0, INF);for (int i = 1; i <= m; i ++) {int x, y; LL c;cin >> x >> y >> c;ins(x, y + 1, c, INF);}mx_flow = mn_cost = 0;memset (v, 0, sizeof(v));while (spfa()) {memcpy(cur, last, sizeof(last));mx_flow += dinic(st, 1ll << 60); }cout << mn_cost << "\n";return 0;
}
