学习笔记:费用流

上讲习题

AcWing 2279

这个题看着很懵,这根本不是最小割啊!然而我们可以思考一下,这种 a b \cfrac{a}{b} ba的模型,且求的最小或最大值,那么不就是分数规划吗?于是,我们可以设平均值为 g g g,则: h ( g ) = ∑ e ∈ C w e − ∣ C ∣ × g h(g)=\displaystyle\sum_{e\in C}w_e-|C|\times g h(g)=eCweC×g = ∑ e ∈ C w e − ∑ e ∈ C g =\displaystyle\sum_{e\in C}w_e-\displaystyle\sum_{e\in C}g =eCweeCg = ∑ e ∈ C ( w e − g ) =\displaystyle\sum_{e\in C}(w_e-g) =eC(weg)我们要求最小,则这样的式子要取一个 min ⁡ \min min,如果 h ( g ) < 0 h(g)<0 h(g)<0,则说明平均值还可以更小, r = m i d r=mid r=mid。如果 h ( g ) > 0 h(g)>0 h(g)>0则说明平均值太小了, l = m i d l=mid l=mid。如果 h ( g ) = 0 h(g)=0 h(g)=0,则说明平均值刚好,得到答案。于是我们就来思考如何计算这个式子的值。我们可以发现,要想得到尽量小的答案,且要让 s → t s\to t st不连通,则我们可以先把所有 w e − g < 0 w_e-g<0 weg<0的边用上。剩下的就全都是正权边了。我们又可以根据正权来推算,发现假设得到了一个割集 c u t ( S , T ) cut(S,T) cut(S,T),则割掉了所有割边后因为 s ∈ S s\in S sS t ∈ T t\in T tT,然而 S S S T T T的所有边都被割掉了,则 s , t s,t s,t一定不连通。那么我们要求最小,所以已经不连通了就不会再在集合内部选割掉的边了。于是对于正权边,求一个最小割即可。两个相加即是我们这个式子的值。

#include<bits/stdc++.h>
using namespace std;
const int NN=10004,MM=200004;
int n,m,s,t,h[NN],ne[MM],e[MM],idx=-1,head[NN],d[NN],l[NN];
double c[MM];
void add(int u,int v,int w)
{
    
    
    e[++idx]=v;
    l[idx]=w;
    ne[idx]=h[u];
    h[u]=idx;
    e[++idx]=u;
    l[idx]=w;
    ne[idx]=h[v];
    h[v]=idx;
}
bool bfs()
{
    
    
    queue<int>q;
    memset(d,-1,sizeof(d));
    q.push(s);
    d[s]=0;
    head[s]=h[s];
    while(q.size())
    {
    
    
        int f=q.front();
        q.pop();
        for(int i=h[f];~i;i=ne[i])
        {
    
    
            int v=e[i];
            if(d[v]==-1&&c[i])
            {
    
    
                d[v]=d[f]+1;
                head[v]=h[v];
                if(v==t)
                    return true;
                q.push(v);
            }
        }
    }
    return false;
}
double find(int u,double sum)
{
    
    
    if(u==t)
        return sum;
    double res=0;
    for(int i=head[u];~i&&res<sum;i=ne[i])
    {
    
    
        head[u]=i;
        int v=e[i];
        if(d[v]==d[u]+1&&c[i])
        {
    
    
            double temp=find(v,min(c[i],sum-res));
            if(!temp)
                d[v]=-1;
            c[i]-=temp;
            c[i^1]+=temp;
            res+=temp;
        }
    }
    return res;
}
double dinic()
{
    
    
    double res=0,sum;
    while(bfs())
        while(sum=find(s,1e9))
            res+=sum;
    return res;
}
double check(double mid)
{
    
    
    double res=0;
    for(int i=0;i<=idx;i+=2)
        if(l[i]<=mid)
            res+=l[i]-mid;
        else
            c[i]=c[i^1]=l[i]-mid;
    return res+dinic();
}
int main()
{
    
    
    scanf("%d%d%d%d",&n,&m,&s,&t);
    memset(h,-1,sizeof(h));
    for(int i=1;i<=m;i++)
    {
    
    
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        add(u,v,w);
    }
    double l=0,r=1e9;
    while(r-l>1e-8)
    {
    
    
        double mid=l+(r-l)/2;
        if(check(mid)<0)
            r=mid;
        else
            l=mid;
    }
    printf("%.2lf",r);
    return 0;
}

AcWing 2280

既然这个题牵扯到了位运算异或,数据范围也不是很大,那么很容易想到一种思路,枚举每一位分别计算。可以设计一个割集 c u t ( S , T ) cut(S,T) cut(S,T) S S S内的点表示这个点用 0 0 0 T T T内的点就表示用 1 1 1。如果两个点之间有边,那么这两个点用同样的数字(放在一个集合里),是不会让答案变大的,如果用不同的数字(放在不同集合里),则会让答案变大。这也正好满足最小割的性质。如果一个点已经赋值了,那么就向 s s s t t t连一条容量为正无穷的边,让最小割不选这条边。如果两个点之间有边,则连一条容量为 1 1 1的边,表示如果你把我们分开了这一位异或值的和就会增加 1 1 1。注意因为我们枚举的是每一位,则最后答案要乘一个 2 k 2^k 2k

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int NN=504,MM=(3004+NN)*2,INF=1e9;
pair<int,int>edge[3004];
int h[NN],e[MM],ne[MM],c[MM],head[NN],d[NN],idx,p[NN],n,m,s,t;
void add(int u,int v,int w1,int w2)
{
    
    
    e[++idx]=v;
    ne[idx]=h[u];
    c[idx]=w1;
    h[u]=idx;
    e[++idx]=u;
    ne[idx]=h[v];
    c[idx]=w2;
    h[v]=idx;
}
void build(int k)
{
    
    
    memset(h,-1,sizeof(h));
    idx=-1;
    for(int i=1;i<=n;i++)
        if(p[i]>0)
        {
    
    
            if(p[i]>>k&1)
                add(i,t,INF,0);
            else
                add(s,i,INF,0);
        }
    for(int i=1;i<=m;i++)
        add(edge[i].first,edge[i].second,1,1);
}
bool bfs()
{
    
    
    queue<int>q;
    q.push(s);
    memset(d,-1,sizeof(d));
    d[s]=0;
    head[s]=h[s];
    while(q.size())
    {
    
    
        int f=q.front();
        q.pop();
        for(int i=h[f];~i;i=ne[i])
        {
    
    
            int v=e[i];
            if(d[v]==-1&&c[i])
            {
    
    
                d[v]=d[f]+1;
                head[v]=h[v];
                if(v==t)
                    return true;
                q.push(v);
            }
        }
    }
    return false;
}
int find(int u,int sum)
{
    
    
    if(u==t)
        return sum;
    int res=0;
    for(int i=head[u];~i&&res<sum;i=ne[i])
    {
    
    
        head[u]=i;
        int v=e[i];
        if(d[v]==d[u]+1&&c[i])
        {
    
    
            int temp=find(v,min(c[i],sum-res));
            if(!temp)
                d[v]=-1;
            c[i]-=temp;
            c[i^1]+=temp;
            res+=temp;
        }
    }
    return res;
}
ll dinic()
{
    
    
    int res=0,sum;
    while(bfs())
        while(sum=find(s,INF))
            res+=sum;
    return res;
}
int main()
{
    
    
    scanf("%d%d",&n,&m);
    t=n+1;
    for(int i=1;i<=m;i++)
        scanf("%d%d",&edge[i].first,&edge[i].second);
    int k;
    scanf("%d",&k);
    memset(p,-1,sizeof(p));
    for(int i=1;i<=k;i++)
    {
    
    
        int a,b;
        scanf("%d%d",&a,&b);
        p[a]=b;
    }
    ll res=0;
    for(int i=0;i<=30;i++)
    {
    
    
        build(i);
        res+=dinic()<<i;
    }
    printf("%lld",res);
    return 0;
}

AcWing 381

这个题难度不是很大,但是输入太毒瘤。这也是我很少见用cin输入的题目之一。题目不让我们用边割开,那么我们就可以拆点。两个边之间为了让他们不被割开,容量设为正无穷。拆点后把一个点割开代价是 1 1 1,入点向出点连一条容量为 1 1 1的边。注意读入时要用浪费掉的char来占掉括号和逗号的位置。

#include<bits/stdc++.h>
using namespace std;
const int NN=104,MM=(NN+NN*NN)*2,INF=1e9;
int h[NN],e[MM],ne[MM],c[MM],head[NN],d[NN],idx,s,t;
void add(int u,int v,int w)
{
    
    
    e[++idx]=v;
    ne[idx]=h[u];
    c[idx]=w;
    h[u]=idx;
    e[++idx]=u;
    ne[idx]=h[v];
    c[idx]=0;
    h[v]=idx;
}
bool bfs()
{
    
    
    queue<int>q;
    q.push(s);
    memset(d,-1,sizeof(d));
    d[s]=0;
    head[s]=h[s];
    while(q.size())
    {
    
    
        int f=q.front();
        q.pop();
        for(int i=h[f];~i;i=ne[i])
        {
    
    
            int v=e[i];
            if(d[v]==-1&&c[i])
            {
    
    
                d[v]=d[f]+1;
                head[v]=h[v];
                if(v==t)
                    return true;
                q.push(v);
            }
        }
    }
    return false;
}
int find(int u,int sum)
{
    
    
    if(u==t)
        return sum;
    int res=0;
    for(int i=head[u];~i&&res<sum;i=ne[i])
    {
    
    
        head[u]=i;
        int v=e[i];
        if(d[v]==d[u]+1&&c[i])
        {
    
    
            int temp=find(v,min(c[i],sum-res));
            if(!temp)
                d[v]=-1;
            c[i]-=temp;
            c[i^1]+=temp;
            res+=temp;
        }
    }
    return res;
}
int dinic()
{
    
    
    int res=0,sum;
    while(bfs())
        while(sum=find(s,INF))
            res+=sum;
    return res;
}
int main()
{
    
    
    int n,m;
    while(cin>>n>>m)
    {
    
    
        memset(h,-1,sizeof(h));
        idx=-1;
        for(int i=0;i<n;i++)
            add(i,n+i,1);
        for(int i=1;i<=m;i++)
        {
    
    
            int u,v;
            char c1,c2,c3;
            cin>>c1>>u>>c2>>v>>c3;
            add(u+n,v,INF);
            add(v+n,u,INF);
        }
        int ans=n;
        for(int i=0;i<n;i++)
            for(int j=0;j<i;j++)
            {
    
    
                s=i+n,t=j;
                for(int k=0;k<=idx;k+=2)
                {
    
    
                    c[k]+=c[k^1];
                    c[k^1]=0;
                }
                ans=min(ans,dinic());
            }
        cout<<ans<<endl;
    }
    return 0;
}

AcWing 2176

很明显,最大权闭合图板子题。同样,输入太毒瘤!先整一个 s t r i n g s t r e a m stringstream stringstream,读入一行,把那一行的数一个一个提取出来放在 i n t int int里面。如果不懂 s t r i n g s t r e a m stringstream stringstream的可以上博客找找。然后直接套我上一篇文章的最大权闭合图的建图方法就可以过。

#include<bits/stdc++.h>
using namespace std;
const int NN=104,MM=(NN+2500)*2,INF=1e9;
int h[NN],e[MM],ne[MM],c[MM],head[NN],d[NN],idx=-1,s,t;
bool st[NN];
void add(int u,int v,int w)
{
    
    
    e[++idx]=v;
    ne[idx]=h[u];
    c[idx]=w;
    h[u]=idx;
    e[++idx]=u;
    ne[idx]=h[v];
    c[idx]=0;
    h[v]=idx;
}
bool bfs()
{
    
    
    queue<int>q;
    q.push(s);
    memset(d,-1,sizeof(d));
    d[s]=0;
    head[s]=h[s];
    while(q.size())
    {
    
    
        int f=q.front();
        q.pop();
        for(int i=h[f];~i;i=ne[i])
        {
    
    
            int v=e[i];
            if(d[v]==-1&&c[i])
            {
    
    
                d[v]=d[f]+1;
                head[v]=h[v];
                if(v==t)
                    return true;
                q.push(v);
            }
        }
    }
    return false;
}
int find(int u,int sum)
{
    
    
    if(u==t)
        return sum;
    int res=0;
    for(int i=head[u];~i&&res<sum;i=ne[i])
    {
    
    
        head[u]=i;
        int v=e[i];
        if(d[v]==d[u]+1&&c[i])
        {
    
    
            int temp=find(v,min(c[i],sum-res));
            if(!temp)
                d[v]=-1;
            c[i]-=temp;
            c[i^1]+=temp;
            res+=temp;
        }
    }
    return res;
}
int dinic()
{
    
    
    int res=0,sum;
    while(bfs())
        while(sum=find(s,INF))
            res+=sum;
    return res;
}
void dfs(int u)
{
    
    
    st[u]=true;
    for(int i=h[u];~i;i=ne[i])
        if(c[i]&&!st[e[i]])
            dfs(e[i]);
}
int main()
{
    
    
    int n,m;
    scanf("%d%d",&n,&m);
    getchar();
    memset(h,-1,sizeof(h));
    t=n+m+1;
    int sum=0;
    for(int i=1;i<=n;i++)
    {
    
    
        string str;
        getline(cin,str);
        stringstream line(str);
        int get,id;
        line>>get;
        sum+=get;
        add(s,i,get);
        while(line>>id)
            add(i,id+n,INF);
    }
    for(int i=1;i<=m;i++)
    {
    
    
        int use;
        scanf("%d",&use);
        add(i+n,t,use);
    }
    int res=dinic();
    dfs(s);
    for(int i=1;i<=n;i++)
        if(st[i])
            printf("%d ",i);
    puts("");
    for(int i=1;i<=m;i++)
        if(st[i+n])
            printf("%d ",i);
    printf("\n%d",sum-res);
    return 0;
}

AcWing 2199

这个题还是很简单。两个骑士互相攻击,那么就是个最大点权独立集,每个位置的点权是 1 1 1,如果不能放就不用连边,也没有点权。这种格子图建二分图的板子就不用我多说了吧。

#include<bits/stdc++.h>
using namespace std;
const int NN=40004,MM=NN*16,INF=1e9,dx[8]={
    
    -1,-1,-2,-2,1,1,2,2},dy[8]={
    
    -2,2,1,-1,-2,2,-1,1};
int n,h[NN],e[MM],ne[MM],c[MM],head[NN],d[NN],idx=-1,s,t;
bool g[204][204];
int get(int x,int y)
{
    
    
    return (x-1)*n+y;
}
void add(int u,int v,int w)
{
    
    
    e[++idx]=v;
    ne[idx]=h[u];
    c[idx]=w;
    h[u]=idx;
    e[++idx]=u;
    ne[idx]=h[v];
    h[v]=idx;
}
bool bfs()
{
    
    
    queue<int>q;
    q.push(s);
    memset(d,-1,sizeof(d));
    d[s]=0;
    head[s]=h[s];
    while(q.size())
    {
    
    
        int f=q.front();
        q.pop();
        for(int i=h[f];~i;i=ne[i])
        {
    
    
            int v=e[i];
            if(d[v]==-1&&c[i])
            {
    
    
                d[v]=d[f]+1;
                head[v]=h[v];
                if(v==t)
                    return true;
                q.push(v);
            }
        }
    }
    return false;
}
int find(int u,int sum)
{
    
    
    if(u==t)
        return sum;
    int res=0;
    for(int i=head[u];~i&&res<sum;i=ne[i])
    {
    
    
        head[u]=i;
        int v=e[i];
        if(d[v]==d[u]+1&&c[i])
        {
    
    
            int temp=find(v,min(c[i],sum-res));
            if(!temp)
                d[v]=-1;
            c[i]-=temp;
            c[i^1]+=temp;
            res+=temp;
        }
    }
    return res;
}
int dinic()
{
    
    
    int res=0,sum;
    while(bfs())
        while(sum=find(s,INF))
            res+=sum;
    return res;
}
int main()
{
    
    
    int m;
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof(h));
    t=n*n+1;
    for(int i=1;i<=m;i++)
    {
    
    
        int x,y;
        scanf("%d%d",&x,&y);
        g[x][y]=true;
    }
    int res=0;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
        {
    
    
            if(g[i][j])
                continue;
            res++;
            if(i+j&1)
            {
    
    
                add(s,get(i,j),1);
                for(int k=0;k<8;k++)
                {
    
    
                    int nx=i+dx[k],ny=j+dy[k];
                    if(nx<=n&&ny<=n&&nx>=1&&ny>=1&&!g[nx][ny])
                        add(get(i,j),get(nx,ny),INF);
                }
            }
            else
                add(get(i,j),t,1);
        }
    printf("%d",res-dinic());
    return 0;
}

概念

费用流,全称最小费用最大流。是网络流中的一类题型。和最大流的区别就是每条边加了一个单位费用,每流 1 1 1的流量就要花费 m o n e y e money_e moneye。要求的东西也变成了在最大流中费用最小的一个。
残留网络:在费用流中,残留网络还需要建立反向边的费用为 − m o n e y e -money_e moneye,因为退货肯定要退钱。

算法

EK算法

方法

把求最大流 E K EK EK算法中的 b f s bfs bfs换成最短路即可。因为残留网络中有负权边,所以 d i j dij dij会出问题,最好用 s p f a spfa spfa。加上一个极大值可以让 d i j dij dij都变成正权,这个技巧有点像最大密度子图,详见我的博客最小割。但是一般 s p f a spfa spfa足够了,所以这里只介绍 s p f a spfa spfa的方法。不知道 E K EK EK怎么做的可以看我的博客最大流。然后我们继续来看,我们可以求出一条增广路的费用和以及流量。因为这一条路径每一条边流量一定相等(少了流不过去,多了又不流量守恒),则设增广路中边集为 E E E,增广路路径之和为 M o n e y Money Money,所以路径的费用 = ∑ e ∈ E ( m o n e y e × f l o w ) =\displaystyle\sum_{e\in E}(money_e\times flow) =eE(moneye×flow) = ∑ e ∈ E ( m o n e y e ) × f l o w =\displaystyle\sum_{e\in E}(money_e)\times flow =eE(moneye)×flow = M o n e y × f l o w =Money\times flow =Money×flow那么,每次增广路的费用加上一个 M o n e y × f l o w Money\times flow Money×flow即可。
时间复杂度 O ( n 2 m 2 ) O(n^2m^2) O(n2m2)

例题

AcWing 2174

这个题套板子即可。

#include<bits/stdc++.h>
using namespace std;
const int NN=5004,MM=100004;
int head[NN],ne[MM],e[MM],w[MM],c[MM],f[NN],d[NN],pre[NN],idx=-1,flow,cost,s,t;
bool st[NN];
void add(int u,int v,int l,int money)
{
    
    
    e[++idx]=v;
    ne[idx]=head[u];
    c[idx]=l;
    w[idx]=money;
    head[u]=idx;
    e[++idx]=u;
    ne[idx]=head[v];
    w[idx]=-money;
    head[v]=idx;
}
bool spfa()
{
    
    
    memset(f,0,sizeof(f));
    memset(d,0x3f,sizeof(d));
    queue<int>q;
    q.push(s);
    f[s]=1e9;
    d[s]=0;
    st[s]=true;
    while(q.size())
    {
    
    
        int u=q.front();
        q.pop();
        st[u]=false;
        for(int i=head[u];~i;i=ne[i])
        {
    
    
            int v=e[i];
            if(c[i]&&d[v]>d[u]+w[i])
            {
    
    
                f[v]=min(f[u],c[i]);
                d[v]=d[u]+w[i];
                pre[v]=i;
                if(st[v])
                    continue;
                q.push(v);
                st[v]=true;
            }
        }
    }
    return f[t]>0;
}
void EK()
{
    
    
    while(spfa())
    {
    
    
        flow+=f[t];
        cost+=d[t]*f[t];
        for(int i=t;i!=s;i=e[pre[i]^1])
        {
    
    
            c[pre[i]]-=f[t];
            c[pre[i]^1]+=f[t];
        }
    }
}
int main()
{
    
    
    memset(head,-1,sizeof(head));
    int n,m;
    scanf("%d%d%d%d",&n,&m,&s,&t);
    for(int i=1;i<=m;i++)
    {
    
    
        int u,v,l,money;
        scanf("%d%d%d%d",&u,&v,&l,&money);
        add(u,v,l,money);
    }
    EK();
    printf("%d %d",flow,cost);
    return 0;
}

费用流解二分图最大权匹配

概念

二分图最大权匹配,就是二分图匹配加上了边权,要求一个边权最大的匹配。

方法

还是最大流解二分图的建图方式,两点之间的边加上费用即可。因为是求最大权,所以边权变成负的求最小费用最大流,最后再把 E K EK EK的结果变回正的即可。不知道怎么最大流解二分图的,见我的博客最大流

例题

AcWing 2193

这个题目就是一个二分图最大权匹配问题。但是又要求一个最小权匹配,直接就正权求最小费用最大流即可。

#include<bits/stdc++.h>
using namespace std;
const int NN=104,MM=5204;
int head[NN],ne[MM],e[MM],w[MM],c[MM],f[NN],d[NN],pre[NN],idx=-1,s,t;
bool st[NN];
void add(int u,int v,int l,int money)
{
    
    
    e[++idx]=v;
    ne[idx]=head[u];
    c[idx]=l;
    w[idx]=money;
    head[u]=idx;
    e[++idx]=u;
    ne[idx]=head[v];
    w[idx]=-money;
    head[v]=idx;
}
bool spfa()
{
    
    
    memset(f,0,sizeof(f));
    memset(d,0x3f,sizeof(d));
    queue<int>q;
    q.push(s);
    f[s]=1e9;
    d[s]=0;
    st[s]=true;
    while(q.size())
    {
    
    
        int u=q.front();
        q.pop();
        st[u]=false;
        for(int i=head[u];~i;i=ne[i])
        {
    
    
            int v=e[i];
            if(c[i]&&d[v]>d[u]+w[i])
            {
    
    
                f[v]=min(f[u],c[i]);
                d[v]=d[u]+w[i];
                pre[v]=i;
                if(st[v])
                    continue;
                q.push(v);
                st[v]=true;
            }
        }
    }
    return f[t]>0;
}
int EK()
{
    
    
    int res=0;
    while(spfa())
    {
    
    
        res+=d[t]*f[t];
        for(int i=t;i!=s;i=e[pre[i]^1])
        {
    
    
            c[pre[i]]-=f[t];
            c[pre[i]^1]+=f[t];
        }
    }
    return res;
}
int main()
{
    
    
    memset(head,-1,sizeof(head));
    int n;
    scanf("%d",&n);
    t=2*n+1;
    for(int i=1;i<=n;i++)
    {
    
    
        add(s,i,1,0);
        add(i+n,t,1,0);
    }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
        {
    
    
            int x;
            scanf("%d",&x);
            add(i,j+n,1,x);
        }
    printf("%d\n",EK());
    for(int i=0;i<=idx;i+=2)
    {
    
    
        c[i]+=c[i^1];
        c[i^1]=0;
        w[i]=-w[i];
        w[i^1]=-w[i^1];
    }
    printf("%d",-EK());
    return 0;
}

技巧:拆点

概念

不知道怎么拆点的,见我的博客最大流。这里和最大流的拆点一样,只不过边加上了费用而已。在费用流中还有一种情况要拆点,就是有点权,拆点之间边的费用就是点权。

方法

和最大流的一样。

例题

AcWing 2191

这个题有点权,要拆点。第一问就是点只能用一次的情况,拆点限制为 1 1 1即可。第二问就是每个点用多次,把入点到出点的限制放开即可。第三问就是边的限制放开。更详细的见我在AcWing上写的题解

#include<bits/stdc++.h>
using namespace std;
const int NN=1004,MM=3124;
int g[20][40],id[20][40],head[NN],ne[MM],w[MM],c[MM],e[MM],d[NN],f[NN],pre[NN],idx,s,t;
bool st[NN];
void add(int u,int v,int l,int money)
{
    
    
    e[++idx]=v;
    ne[idx]=head[u];
    c[idx]=l;
    w[idx]=money;
    head[u]=idx;
    e[++idx]=u;
    ne[idx]=head[v];
    c[idx]=0;
    w[idx]=-money;
    head[v]=idx;
}
bool spfa()
{
    
    
    memset(f,0,sizeof(f));
    memset(d,0x3f,sizeof(d));
    queue<int>q;
    q.push(s);
    f[s]=1e9;
    d[s]=0;
    st[s]=true;
    while(q.size())
    {
    
    
        int u=q.front();
        q.pop();
        st[u]=false;
        for(int i=head[u];~i;i=ne[i])
        {
    
    
            int v=e[i];
            if(c[i]&&d[v]>d[u]+w[i])
            {
    
    
                f[v]=min(f[u],c[i]);
                d[v]=d[u]+w[i];
                pre[v]=i;
                if(st[v])
                    continue;
                q.push(v);
                st[v]=true;
            }
        }
    }
    return f[t]>0;
}
int EK()
{
    
    
    int res=0;
    while(spfa())
    {
    
    
        res+=d[t]*f[t];
        for(int i=t;i!=s;i=e[pre[i]^1])
        {
    
    
            c[pre[i]]-=f[t];
            c[pre[i]^1]+=f[t];
        }
    }
    return res;
}
int main()
{
    
    
    int n,m,cnt=0;
    scanf("%d%d",&m,&n);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i+m-1;j++)
        {
    
    
            scanf("%d",&g[i][j]);
            id[i][j]=++cnt;
        }
    t=2*cnt+2;
    memset(head,-1,sizeof(head));
    idx=-1;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i+m-1;j++)
        {
    
    
            add(id[i][j]*2,id[i][j]*2+1,1,-g[i][j]);
            if(i==1)
                add(s,id[i][j]*2,1,0);
            if(i==n)
                add(id[i][j]*2+1,t,1,0);
            if(i<n)
            {
    
    
                add(id[i][j]*2+1,id[i+1][j]*2,1,0);
                add(id[i][j]*2+1,id[i+1][j+1]*2,1,0);
            }
        }
    printf("%d\n",-EK());
    memset(head,-1,sizeof(head));
    idx=-1;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i+m-1;j++)
        {
    
    
            add(id[i][j]*2,id[i][j]*2+1,1e9,-g[i][j]);
            if(i==1)
                add(s,id[i][j]*2,1,0);
            if(i==n)
                add(id[i][j]*2+1,t,1e9,0);
            if(i<n)
            {
    
    
                add(id[i][j]*2+1,id[i+1][j]*2,1,0);
                add(id[i][j]*2+1,id[i+1][j+1]*2,1,0);
            }
        }
    printf("%d\n",-EK());
    memset(head,-1,sizeof(head));
    idx=-1;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i+m-1;j++)
        {
    
    
            add(id[i][j]*2,id[i][j]*2+1,1e9,-g[i][j]);
            if(i==1)
                add(s,id[i][j]*2,1,0);
            if(i==n)
                add(id[i][j]*2+1,t,1e9,0);
            if(i<n)
            {
    
    
                add(id[i][j]*2+1,id[i+1][j]*2,1e9,0);
                add(id[i][j]*2+1,id[i+1][j+1]*2,1e9,0);
            }
        }
    printf("%d\n",-EK());
    return 0;
}

AcWing 382

这个题有点权,需要拆点解决。把一个点拆成入点和出点,费用是价值的容量为 1 1 1,再来一种以后走的边,费用为 0 0 0容量为正无穷,求最大费用最大流。

#include<bits/stdc++.h>
using namespace std;
const int NN=10004,MM=30004;
int n,head[NN],ne[MM],w[MM],c[MM],e[MM],d[NN],f[NN],pre[NN],idx=-1,s,t=1;
bool st[NN];
int get(int x,int y)
{
    
    
    return ((x-1)*n+y)*2;
}
void add(int u,int v,int l,int money)
{
    
    
    e[++idx]=v;
    ne[idx]=head[u];
    c[idx]=l;
    w[idx]=money;
    head[u]=idx;
    e[++idx]=u;
    ne[idx]=head[v];
    w[idx]=-money;
    head[v]=idx;
}
bool spfa()
{
    
    
    memset(f,0,sizeof(f));
    memset(d,0x3f,sizeof(d));
    queue<int>q;
    q.push(s);
    f[s]=1e9;
    d[s]=0;
    st[s]=true;
    while(q.size())
    {
    
    
        int u=q.front();
        q.pop();
        st[u]=false;
        for(int i=head[u];~i;i=ne[i])
        {
    
    
            int v=e[i];
            if(c[i]&&d[v]>d[u]+w[i])
            {
    
    
                f[v]=min(f[u],c[i]);
                d[v]=d[u]+w[i];
                pre[v]=i;
                if(st[v])
                    continue;
                q.push(v);
                st[v]=true;
            }
        }
    }
    return f[t]>0;
}
int EK()
{
    
    
    int res=0;
    while(spfa())
    {
    
    
        res+=d[t]*f[t];
        for(int i=t;i!=s;i=e[pre[i]^1])
        {
    
    
            c[pre[i]]-=f[t];
            c[pre[i]^1]+=f[t];
        }
    }
    return res;
}
int main()
{
    
    
    int k;
    scanf("%d%d",&n,&k);
    memset(head,-1,sizeof(head));
    add(s,get(1,1),k,0);
    add(get(n,n)+1,t,k,0);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
        {
    
    
            int x;
            scanf("%d",&x);
            add(get(i,j),get(i,j)+1,1,-x);
            add(get(i,j),get(i,j)+1,1e9,0);
            if(i<n)
                add(get(i,j)+1,get(i+1,j),1e9,0);
            if(j<n)
                add(get(i,j)+1,get(i,j+1),1e9,0);
        }
    printf("%d\n",-EK());
    return 0;
}

AcWing 2184

这个题就是典型的多种状态。把每天的毛巾拆成两个点,一个是洗过,一个是没有洗。每天没洗过的毛巾可以流到下一天,也可以经过清洗流到干净的毛巾,快洗流到新毛巾的 i + t 1 i+t1 i+t1天,费用 m 1 m1 m1,慢洗同理。也可以购买新毛巾,费用是 m 0 m0 m0,新毛巾用了可以流到汇点。每天都会用完刚好 x x x条毛巾,所以会有 x x x条脏毛巾,但是他们流向汇点计算答案了,所以这里就改成从源点向脏毛巾的点流 x x x

#include<bits/stdc++.h>
using namespace std;
const int NN=2004,MM=20004;
int head[NN],e[MM],ne[MM],c[MM],w[MM],idx=-1,f[NN],d[NN],pre[NN],s,t;
bool st[NN];
void add(int u,int v,int l,int money)
{
    
    
    e[++idx]=v;
    ne[idx]=head[u];
    c[idx]=l;
    w[idx]=money;
    head[u]=idx;
    e[++idx]=u;
    ne[idx]=head[v];
    w[idx]=-money;
    head[v]=idx;
}
bool spfa()
{
    
    
    memset(f,0,sizeof(f));
    memset(d,0x3f,sizeof(d));
    queue<int>q;
    q.push(s);
    f[s]=1e9;
    d[s]=0;
    st[s]=true;
    while(q.size())
    {
    
    
        int u=q.front();
        q.pop();
        st[u]=false;
        for(int i=head[u];~i;i=ne[i])
        {
    
    
            int v=e[i];
            if(c[i]&&d[v]>d[u]+w[i])
            {
    
    
                d[v]=d[u]+w[i];
                f[v]=min(f[u],c[i]);
                pre[v]=i;
                if(!st[v])
                {
    
    
                    st[v]=true;
                    q.push(v);
                }
            }
        }
    }
    return f[t]>0;
}
int EK()
{
    
    
    int res=0;
    while(spfa())
    {
    
    
        res+=d[t]*f[t];
        for(int i=t;i!=s;i=e[pre[i]^1])
        {
    
    
            c[pre[i]]-=f[t];
            c[pre[i]^1]+=f[t];
        }
    }
    return res;
}
int main()
{
    
    
    int n,m0,t1,m1,t2,m2;
    scanf("%d%d%d%d%d%d",&n,&m0,&t1,&m1,&t2,&m2);
    t=2*n+1;
    memset(head,-1,sizeof(head));
    for(int i=1;i<=n;i++)
    {
    
    
        int x;
        scanf("%d",&x);
        add(s,i,x,0);
        add(i+n,t,x,0);
        add(s,i+n,1e9,m0);
        if(i<n)
            add(i,i+1,1e9,0);
        if(i+t1<=n)
            add(i,n+i+t1,1e9,m1);
        if(i+t2<=n)
            add(i,n+i+t2,1e9,m2);
    }
    printf("%d",EK());
    return 0;
}

上下界可行流

概念

不知道上下界可行流见我的博客最大流,这里就是多了个费用。但是在费用流中要求从源点和流向汇点的边是没有费用的,因为它们只是为了流量守恒。

方法

和最大流的上下界可行流一样。

例题

AcWing 969

这个题就是一个上下界可行流。但是这个是没有上界的,所以上界可以设为正无穷。于是,上界减下界还是正无穷。前一天的志愿者可以到后一天,到了最后一天就流出去。然后我们发现,原图 G G G从志愿者结束工作的时间是流到 t t t了一些人,开始从 s s s流进来了一些开始工作的志愿者,但是新图 G G G的源点和汇点是满足流量守恒的,不能提供志愿者。所以为了流量守恒,结束向开始连一条边即可。如果要公式化的解释我看到了网上一篇解释地特别漂亮的博客。围观点我

#include<bits/stdc++.h>
using namespace std;
const int NN=1004,MM=24004;
int head[NN],ne[MM],w[MM],c[MM],e[MM],d[NN],f[NN],pre[NN],idx=-1,s,t;
bool st[NN];
void add(int u,int v,int l,int money)
{
    
    
    e[++idx]=v;
    ne[idx]=head[u];
    c[idx]=l;
    w[idx]=money;
    head[u]=idx;
    e[++idx]=u;
    ne[idx]=head[v];
    w[idx]=-money;
    head[v]=idx;
}
bool spfa()
{
    
    
    memset(f,0,sizeof(f));
    memset(d,0x3f,sizeof(d));
    queue<int>q;
    q.push(s);
    f[s]=1e9;
    d[s]=0;
    st[s]=true;
    while(q.size())
    {
    
    
        int u=q.front();
        q.pop();
        st[u]=false;
        for(int i=head[u];~i;i=ne[i])
        {
    
    
            int v=e[i];
            if(c[i]&&d[v]>d[u]+w[i])
            {
    
    
                f[v]=min(f[u],c[i]);
                d[v]=d[u]+w[i];
                pre[v]=i;
                if(st[v])
                    continue;
                q.push(v);
                st[v]=true;
            }
        }
    }
    return f[t]>0;
}
int EK()
{
    
    
    int res=0;
    while(spfa())
    {
    
    
        res+=d[t]*f[t];
        for(int i=t;i!=s;i=e[pre[i]^1])
        {
    
    
            c[pre[i]]-=f[t];
            c[pre[i]^1]+=f[t];
        }
    }
    return res;
}
int main()
{
    
    
    int n,m;
    scanf("%d%d",&n,&m);
    memset(head,-1,sizeof(head));
    t=n+2;
    int last=0;
    for(int i=1;i<=n;i++)
    {
    
    
        int x;
        scanf("%d",&x);
        if(last>x)
            add(s,i,last-x,0);
        else
            add(i,t,x-last,0);
        add(i,i+1,1e9,0);
        last=x;
    }
    add(s,n+1,last,0);
    for(int i=1;i<=m;i++)
    {
    
    
        int b,e,money;
        scanf("%d%d%d",&b,&e,&money);
        add(e+1,b,1e9,money);
    }
    printf("%d\n",EK());
    return 0;
}

习题

AcWing 2192

AcWing 2194

AcWing 2195

解析和代码在下一篇博客——2-sat给出(下一次有可能会做算法提高课同步内容)

猜你喜欢

转载自blog.csdn.net/weixin_44043668/article/details/108738212