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

CodeVS 1237。

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

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

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

优雅的模型

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

  1. 上面的分析中,我们区分了“今天使用的”和“今天清洗的”。把“今天清洗的”作为X集合,“今天使用的”作为Y集合,我们能够建立二分图模型。今天使用的=今天清洗的=今天的需求,S->Xi,Yi->T,容量为ri,费用为0,求最大流把它们流满即满足约束。它们必能满流,因为其他边的容量都是正无穷的,见下文。
  2. 新买的:S->Yi,容量inf,费用p。
  3. 快洗:Xi->Y(i+m),i+m<=N,容量inf,费用f。
  4. 慢洗:Xi->Y(i+n),i+n<=N,容量inf,费用s。
  5. 延期处理:Xi->X(i+1),i< N,容量inf,费用0。

就是这样。

这个模型使我欣赏的地方:
1. 通过分析,把>=转为=,由于流的容量限制(<=),流量最大时满足了约束(取得等号)。“最小费用”、“最大流”两个约束很好地统一了。
2. “从源点流出的=流入汇点的”——广义的流量平衡。平时我们讲流量平衡,都是以一个顶点为对象,流入=流出。别忽视了整体。

我的模型

写完上面的,自惭形秽。

  1. 每个点i拆成两个,中间连边,容量为ri,费用为0。即in(i)->out(i)。
  2. S->in(i),容量inf,费用为p。
  3. out(i)->in(i+m),i+m<=N,容量inf,费用为f。
  4. out(i)->in(i+n),i+n<=N,容量inf,费用s。
  5. out(i)->out(i+1),i< N,容量inf,费用0。
  6. 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;
}
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值