您的位置:首页 > 理论基础 > 计算机网络

[线性规划与网络流24题] 餐巾计划问题

2016-05-28 23:32 507 查看
CodeVS 1237。

粗浅地学习了带上下界的网络流的人的代码的时间开销:1472ms。

领悟二分图精髓的人的代码的时间开销:18ms。

两种建图的顶点数相同,我的边数是后者的2/3。但是增广的次数可能很多。

优雅的模型

约束是每天的餐巾够用,目标是使费用最小。每天的餐巾有三种来源:新买的,m天前送到快洗部的,n天前送到慢洗部的。每天的餐巾有三种去路:延期处理,送到快洗部,送到慢洗部。网络流模型擅长处理的是小于等于号,然而这里是“够用”即大于等于。(1) 如果总是存在一种刚好够用的方案,它显然优于其他有冗余的方案。今天多用一些餐巾的好处是能多洗一些餐巾以备后用……这是不必要的。今天多用的餐巾如果是买的,要用的那天再买,能省去清洗费用;如果是洗的,从它被使用的那天起延期处理,今天清洗即可,今天用不着使用它。(2) 刚好够用的方案总是存在。所以,根据(1)(2),在费用最小的目标下,“够用”可以改成“恰好够用”。

上面的分析中,我们区分了“今天使用的”和“今天清洗的”。把“今天清洗的”作为X集合,“今天使用的”作为Y集合,我们能够建立二分图模型。今天使用的=今天清洗的=今天的需求,S->Xi,Yi->T,容量为ri,费用为0,求最大流把它们流满即满足约束。它们必能满流,因为其他边的容量都是正无穷的,见下文。

新买的:S->Yi,容量inf,费用p。

快洗:Xi->Y(i+m),i+m<=N,容量inf,费用f。

慢洗:Xi->Y(i+n),i+n<=N,容量inf,费用s。

延期处理:Xi->X(i+1),i< N,容量inf,费用0。

就是这样。

这个模型使我欣赏的地方:

1. 通过分析,把>=转为=,由于流的容量限制(<=),流量最大时满足了约束(取得等号)。“最小费用”、“最大流”两个约束很好地统一了。

2. “从源点流出的=流入汇点的”——广义的流量平衡。平时我们讲流量平衡,都是以一个顶点为对象,流入=流出。别忽视了整体。

我的模型

写完上面的,自惭形秽。

每个点i拆成两个,中间连边,容量为ri,费用为0。即in(i)->out(i)。

S->in(i),容量inf,费用为p。

out(i)->in(i+m),i+m<=N,容量inf,费用为f。

out(i)->in(i+n),i+n<=N,容量inf,费用s。

out(i)->out(i+1),i< N,容量inf,费用0。

out(N)->T,容量inf,费用为0。

我拆点不是看到了二分图,而是为了给节点赋容量。

对比两种模型,我的out很像二分图里的X,in很像二分图里的Y。实际上,用上下界网络流的方法,分离必要弧,建立附加源S’、汇T’,连接原T->S,两个模型是等价的。S’->S会经过两条容量为inf,费用为0的边,费用p相当于由S’付。但是,由于没有理清对第1点里“容量”的真正要求,一直觉得得求最小/大流,我使用了以下非常规方法。

第1点的“容量”,现在想来,我实际想表述的是上界=下界=ri。之前没意识到,直接跑S-T最小费用最大流,企图让这些边流满。然而流满它们的方式很多,最大流等于r1+r2+…+rN,不是我想要的。这里,“最小(可行)费用”和“最大流”未能统一。

做了些修改。用带上下界的网络流的方法,先跑S-T可行流,再跑T-S最大流。我直接建了跑完S-T可行流后的残存网络(去掉了一些不必要的弧)。接下来跑T-S最小费用最大流吗?好像不对头。这样求出了S-T最小流,但流量最小并不意味着费用最小。试了试p=1的情形,果然给出了错误的答案。

这里的费用有正有负。一开始,流费用为负;随着增广的进行,费用逐渐变成正的。也许我该在费用变为正数的时候停止增广。忽然想到了什么——

流量不固定的s-t最小费用流。如果网络中的费用有正有负,如何求s-t最小费用流?注意,这里的流量并不固定。

解:如果费用都是正的,最小费用流显然是零流;但由于负费用的存在,最短增广路的权值可能是负的,这样增广之后会得到更小的费用;但随着增广的进行,增广路权值逐渐增大,最后变成正数,此时应该停止增广。换句话说,最小费用随着流量增大先减小,后增大,成下凸函数。前面说过,下凸函数求最小值一般使用三分法,但这里可不用这么麻烦,只需在最短增广路费用为正时停止增广即可,三分反而比较慢(想一想,为什么)。需要注意的是,如果一开始不仅有负费用弧,还有负费用圈,必须先用消圈法消去负圈,否则最短增广路算法的前提不成立。当然,如果网络是无环的,则无此问题。——刘汝佳《算法竞赛入门经典——训练指南》

正所谓“纸上得来终觉浅,绝知此事要躬行”。

这个模型虽然在边数上有优势,但在实践中运行得慢。大概是增广次数多。这种求最小费用流的最短增广路算法,缺点在于每次只能增广一条路。也许该学学zkw费用流了,很想知道它怎样做出改进。

点评:尽管结果不爽,但过程很爽。又实践了一次自创的“直接建可行流的残存网络法”,效果不如上次解决“最小路径覆盖问题”。要让一些边满流,把它们跟源、汇相连。

代码

#include <cstdio>
#include <queue>
using namespace std;
const int MAXN = 1000, MAXV = MAXN*2+2, MAXE = MAXN*8, INF = 1<<30;
int e_ptr = 2, N, p, m, f, n, s;
int fst[MAXV+5];

struct Edge {
int v, next, c, w;
} E[MAXE+5];

inline void add_edge(int u, int v, int c, int w)
{
E[e_ptr] = (Edge){v, fst[u], c, w}; fst[u] = e_ptr++;
E[e_ptr] = (Edge){u, fst[v], 0, -w}; fst[v] = e_ptr++;
}

namespace MCMF {
int d[MAXV+5], p[MAXV+5];
bool inq[MAXV+5];

bool SPFA(int s, int t, int& cost)
{
queue<int> Q;
for (int i = s+1; i <= t; ++i) {
inq[i] = false;
d[i] = INF;
}
inq[s] = true;
d[s] = 0;
Q.push(s);
while (!Q.empty()) {
int u = Q.front();
Q.pop();
inq[u] = false;
for (int i = fst[u]; i; i = E[i].next) {
int v = E[i].v;
if (E[i].c > 0 && d[v] > d[u] + E[i].w) {
d[v] = d[u] + E[i].w;
p[v] = i^1;
if (!inq[v]) {
inq[v] = true;
Q.push(v);
}
}
}
}
if (d[t] == INF)
return false;
int f = INF;
for (int u = t; u != s; u = E[p[u]].v)
f = min(f, E[p[u]^1].c);
for (int u = t; u != s; u = E[p[u]].v) {
E[p[u]].c += f;
E[p[u]^1].c -= f;
cost += f*E[p[u]^1].w;
}
return true;
}

int MCMF(int s, int t)
{
int cost = 0, d_cost = 0;
while (SPFA(s, t, d_cost))
if (d_cost < 0) {
cost += d_cost;
d_cost = 0;
} else
break;
return cost;
}
}

int main()
{
scanf("%d %d %d %d %d %d", &N, &p, &m, &f, &n, &s);
int sum = 0;
for (int i = 1; i <= N; ++i) {
int r;
scanf("%d", &r);
sum += r;
add_edge((i<N ? 2*i+1 : 0), 2*i-1, sum, 0);
if (i+m <= N)
add_edge(2*i-1, 2*(i+m), INF, f);
if (i+n <= N)
add_edge(2*i-1, 2*(i+n), INF, s);
add_edge(2*i, 2*N+1, r, -p);
}

printf("%d", p*sum + MCMF::MCMF(0, 2*N+1));
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: