费用流

费用:给每一条边定义一个费用,那么对于一个可行流,其费用为 \(\sum \limits_{e \in E} flow(e) \times cost(e)\)
最小费用最大流:对于所有最大可行流中费用最小的那一个。


求法:考虑朴素的 ek 算法。是每次 BFS 得到一条增广路,然后进行增广操作。

对于最小费用最大流,只需要把 BFS 改成 SPFA 找最短路即可。

正确性证明:

考虑某一个流 \(f\),如果有同样流量但费用更小的流 \(f'\) 那么考虑流 \(f' - f\)。其费用是负数,所以一定存在负环。如果有负环,那么一定不是最小费用流。因此有:

最小费用流 等价于 残量网络没负环。

考虑归纳。初始的时候残量网络是原图,原图没负环那么就不会含有负环。考虑目前找到的 \(f_0\) 是最小费用的一个,那么在找到一个最短路径 \(f'\),加起来得到新的 \(f_1\)。考虑存在不存在和 \(f_1\) 相同流量但是更小的流。考虑存在这样的流 \(f_1'\)\(f_1' - f_0\) 是由若干个环与一个 \(s \rightarrow t\) 的路形成的(流量守恒)。如果要比最短路(一条 \(s \rightarrow t\) 的路线)的费用还小,那么一定有负环。矛盾。因此假设成立。

只有一个最起码的上界:\(O(nmf)\),其中 \(nm\) 是 bellman-ford 的时间复杂度。\(f\) 是增广次数。

实现

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
//#define cerr if(false)cerr
//#define freopen if(false)freopen
#define watch(x) cerr  << (#x) << ' '<<'i'<<'s'<<' ' << x << endl
void pofe(int number, int bitnum) {
    string s; f(i, 0, bitnum) {s += char(number & 1) + '0'; number >>= 1; } 
    reverse(s.begin(), s.end()); cerr << s << endl; 
    return;
}
void cmax(int &x, int y) {if(x < y) x = y;}
void cmin(int &x, int y) {if(x > y) x = y;}
//调不出来给我对拍!
const int sq=77777;
int nd[1010];
int head[sq],to[sq],nxt[sq],dis[sq],cur[sq],cap[sq],cost[sq],bcnt,S,T,ret,vis[sq];
void add(int x,int y,int w,int c){
    nxt[bcnt]=head[x];to[bcnt]=y;cap[bcnt]=w;cost[bcnt]=c;head[x]=bcnt;bcnt++;
    nxt[bcnt]=head[y];to[bcnt]=x;cap[bcnt]=0;cost[bcnt]=-c;head[y]=bcnt;bcnt++;
}
bool bf(){
    memset(dis,0x3f,sizeof(dis)); dis[S]=0; memcpy(cur,head,sizeof(cur));
    bool change=1;
    while(change) {
        change=0;
        f(now,S,T){
            for(int i=head[now];~i;i=nxt[i]){
                if(cap[i]==0)continue;
                if(dis[to[i]]>dis[now]+cost[i]){
                    dis[to[i]]=dis[now]+cost[i];
                    change=1;
                }
            }
        }
    }
    return dis[T] < inf;
}
int find(int now,int limit){
    if(now==T)return limit;
    int flow=0;
    vis[now]=1;
    for(int i=cur[now];~i&&flow<limit;i=nxt[i]){
        cur[now]=i;
        if(cap[i]>0&&vis[to[i]]==0&&dis[to[i]]==dis[now]+cost[i]){
            int tmp=find(to[i],min(cap[i],limit-flow));
            if(tmp==0)dis[to[i]]=-1;
            ret+=cost[i]*tmp; flow+=tmp;cap[i]-=tmp;cap[i^1]+=tmp;
        }
    }
    vis[now]=0;
    return flow;
}
int mcmf(){
    ret=0;
    int flow,maxflow=0;
    while(bf()){flow=find(S,inf);maxflow+=flow;}
    return maxflow;
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(NULL);
    cout.tie(NULL);
    //freopen();
    //freopen();
    //time_t start = clock();
    //think twice,code once.
    //think once,debug forever.
    int n,a,b,c,d,e;cin>>n>>a>>b>>c>>d>>e; f(i,1,n)cin>>nd[i]; S=0;T=n+n+1;
    memset(head,-1,sizeof(head));
    f(i,1,n)add(S, i, inf, a);
    f(i,1,n){add(S, n+i, nd[i], 0); add(i, T, nd[i], 0);}
    f(i,1,n){if(i+1<=n)add(n+i, n+1+i, inf, 0);}
    f(i,1,n){if(i+b<=n)add(n+i, i+b, inf, c); if(i+d<=n)add(n+i, i+d, inf, e);}
    mcmf(); 
    cout<< ret<<endl;
    //time_t finish = clock();
    //cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
    return 0;
}
/*
2023/x/xx
start thinking at h:mm


start coding at h:mm
finish debugging at h:mm
*/

这里最重要的就是 find 函数里面的 vis 了。不允许走出环。不然可能会死循环的。

二分图最大权完美匹配

完美匹配:一个满的匹配。

考虑使用流量限制每一个点匹配只有 \(1\) 个点,权值留给费用。

原始对偶优化

对于二分图最大权完美匹配,流量为 \(n\),KM 算法做到 \(O(n^3)\),而 mcmf 是 \(O(n^4)\) 的。我们使用类似 johnson 全源最短路的方式,对 bellman-ford 这一步进行优化,得到 \(O(n^3 \log n)\) 的做法。

规定符号:我们记 \(cost_{i \rightarrow j}\) 表示原图的该边边权,\(dis_{i \rightarrow j}\) 表示原图做带势变换后的边权,\(h_i\) 表示某一个点的势能函数值,\(d_i\) 表示势能源点到某一个点的距离。

首先对 johnson 的势能函数思想做一个总结:

考虑给每个点定义一个 \(h_i\)不论 \(h\) 是什么,对图做变换(边权全部加上 \(h_s - h_t\))之后做全源最短路,某一条图上从 \(S\)\(T\) 的路径的带势距离一定是 \(\sum cost + h_S - h_T\)。而我们为了使得整张图带势之后边权都是非负数,选取适当的 \(h\) 函数。例如,对于非负权值图,\(h_i = 0\);对于带负权图,\(h_i = d_i\),就是 johnson 选取的函数。

说这个,主要是为了澄清,\(h\) 函数不一定要是 \(d\) 函数,我们可以自行选用。

好的,现在考虑原始对偶算法的思想。原始对偶算法基于 EK 算法(EK 算法流程:每一次从起点找到到终点的一条最短路径,然后找到路径上的瓶颈,然后增广(也就是,统计将流流向这条路产生的结果)),在增广过程中动态维护(或者说,每一次增广的时候因为图可能形态变化,修改势能函数)一个我们定义的势能函数,时刻保持图上的每一条正边(容量不是 \(0\))都是正向的

注意,由于要进行多次重新标号的过程,以下 \(d_i\) 指的是势能源点到上张图的带势距离,准确来说,是 \(h\) 为势能函数的时候求出的最短路。(其实应该就是这个定义,johnson 只是默认上一次带势距离的势能为 \(0\) 即可)容易发现,三角不等式 \(d_i + dis_{i \rightarrow j} \ge d_j\) 依然成立。

考虑实际上要达成什么目标。一次增广之后,图上一些边的状态会:

  • 从正边变成零边
  • 从零边变成正边
  • 保持是零边
  • 保持是正边

其中前两种只会在最短路上发生。我们考虑主要注意保持第二种的非负性。

为了方便区分,我们把修改之后的所有变量加上 \('\) 记号。

考虑因为是最短路,我们有 \(d_i + dis_{i \rightarrow j} = d_j\)。我们需要 \(cost_{j \rightarrow i} + h'_j - h'_i \ge 0\)

考虑对第一个式子变形:\(d_i + cost_{i \rightarrow j} + h_i - h_j - d_j = 0\)。由于 \(cost_{i \rightarrow j }=-cost_{j \rightarrow i}\),所以 \(cost_{j \rightarrow i} + (h_j + d_j) - (h_i - d_i) = 0\)。只需 \(h'_i = h_i + d_i\) 即可满足需要的式子。

我们得到了一个满足第二种非负性的方法:每次增广之后/找到最短路之后(这两个时间是一致的)将 \(h_i\) 变成 \(h_i + d_i\)\(d_i\) 是刚刚 dijkstra 求出来的,就是求最短路这次求的)

注意是对所有 \(i\),而不是对最短路上的 \(i\)

考虑其对其他的边满不满足。

首先是 \((i, j)\) 边。其是 \((j, i)\) 的反边,所以新的 \(d\) 加上之后其边权应该是 \(0\)

然后考虑一般情况。我们有 \(d_i + dis_{i \rightarrow k} \ge d_k, dis_{i \rightarrow k} \ge 0\),因此 \(dis'_{i \rightarrow k} = dis_{i \rightarrow k} + d_i - d_k \ge dis_{i \rightarrow k} - dis_{i \rightarrow k} = 0\)

更详细地说,等于 \(d_i + dis_{i \rightarrow j}- d_k\),也就是走到 \(k\) 的 delta。

这样就很明白了:我们其实维护的是 delta(delta 的定义见逛公园)。那么就说明了这个势能函数的真实含义。于是一切都说得通了(包括为什么加了一个数之后都是 \(0\),因为上一个的 delta 对于最短路多半都是 \(0\)

时间复杂度方面,每一次增广耗费 \(O(n) + O(m \log n)\),总时间复杂度 \(O(nm + m \log n f)\)

再次梳理一下 Primal-Dual 优化的步骤:

bellman-ford 计算初始 h 并先对图进行更新
while(1) {
dijkstra 计算 d,同时记录每一个节点的前驱
更新所有 h
增广
}

考虑能不能放在 dinic 上用!

维护的是 delta 的话,其实按道理应该是可以的,多路增广增广的都是最短路,使用这个并没有什么副作用。

(但是测了一下很慢,比 KM 逊色很多)
但是我太蠢了。

为什么呢?

dijkstra 可以 \(O(n^2)\),这样时间复杂度就是 \(O(n^3)\) 的。

posted @ 2023-03-04 23:26  OIer某罗  阅读(95)  评论(0编辑  收藏  举报