网络流重学

开始学习图论啦,网络流先行

最大流/最小割

\(Dinic\)最大流算法

最大流就是最小割,因为最大流相当于把整张图流满了,于是我就割那些满流边

要是割其他边的话,割的代价一定大于满流边

要学会转化题意,不止流量,包括代价、边权......都是网络流的词汇

code
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=105;
const int M=5005;
int n,m,s,t,ans;
int to[M*2],nxt[M*2],val[M*2],head[N],hea[N],rp=1;
void add_edg(int x,int y,int z){
    to[++rp]=y;val[rp]=z;
    nxt[rp]=head[x];
    head[x]=rp;
}
int dep[N];
bool bfs(){
    memcpy(head,hea,sizeof(hea));
    memset(dep,0x3f,sizeof(dep));
    queue<int> q;
    while(!q.empty())q.pop();
    q.push(s);dep[s]=0;
    while(!q.empty()){
        int x=q.front();q.pop();
        for(int i=head[x];i;i=nxt[i]){
            int y=to[i];
            if(!val[i]||dep[y]<=dep[x]+1)continue;
            q.push(y);dep[y]=dep[x]+1;
            if(y==t)return true;
        }
    }
    return false;
}
int dfs(int x,int in){
    if(x==t)return in;
    int rest=in,go=0;
    for(int i=head[x];i;head[x]=i=nxt[i]){
        int y=to[i];
        if(!val[i]||dep[y]!=dep[x]+1)continue;
        go=dfs(y,min(val[i],rest));
        if(go)val[i]-=go,val[i^1]+=go,rest-=go;
        else dep[y]=0;
        if(!rest)break;
    }
    return in-rest;
}
signed main(){
    n=read();m=read();
    s=read();t=read();
    fo(i,1,m){
        int x=read(),y=read(),z=read();
        add_edg(x,y,z);
        add_edg(y,x,0);
    }
    memcpy(hea,head,sizeof(head));
    while(bfs())ans+=dfs(s,inf);
    printf("%lld",ans);
}

于是乎,我认为建图才是最重要的,分清是最小割还是最大流

合理利用虚点的思想啊

一般求最大值的时候,看一下是减去最小值好做还是直接找最大值好做

要是找最小值好做的话,那就想办法构建最小割的网络流,要不然就直接上最大流就行了

一种新方法:在建图的时候,可以先建立一个不是那么完整的图,然后列式子,看看我那些边加起来应该是啥值,可以把边权直接解出来

考虑边权之间的限制关系,可以利用这个关系建边

在做这类问题的时候,不要考虑啥实际意义,搞不好的就直接给它新建一个节点表示它的信息

最小割树

对于一个\(n\)个点的图,我要求出任意两点的最小割

这个肯定是不可以做\(\mathcal{O(n^2)}\)次最小割的

所以我们引入一个叫做最小割树的东西

首先这个东西的性质是,任意两点之间的最小割即是最小割树上两点之间的路径上的最小值

构建方法就是,我们先任意找两个点,对这两个点跑最小割

那么我们可以的到两个残量网络

于是我们对这两个连通块递归以上操作,一共会做\(\mathcal{O(n)}\)

到时候之接在生成树上找就行了

不同的最小割

code
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define fo(i,x,y) for(int i=(x);i<=(y);i++)
#define fu(i,x,y) for(int i=(x);i>=(y);i--)
int read(){
    int s=0,t=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')t=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
    return s*t;
}
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=855;
const int M=8505;
int n,m,s,t,ans;
int to[M*2],nxt[M*2],val[M*2],va[M*2],head[N],hea[N],rp=1;
void add_edg(int x,int y,int z){
    to[++rp]=y;val[rp]=z;
    nxt[rp]=head[x];
    head[x]=rp;
}
int dep[N];
set<int> st;
bool bfs(){
    memcpy(head,hea,sizeof(hea));
    memset(dep,0x3f,sizeof(dep));
    queue<int> q;
    while(!q.empty())q.pop();
    q.push(s);dep[s]=0;
    while(!q.empty()){
        int x=q.front();q.pop();
        for(int i=head[x];i;i=nxt[i]){
            int y=to[i];
            if(!val[i]||dep[y]<=dep[x]+1)continue;
            q.push(y);dep[y]=dep[x]+1;
            if(y==t)return true;
        }
    }
    return false;
}
int dfs(int x,int in){
    if(x==t)return in;
    int rest=in,go=0;
    for(int i=head[x];i;i=nxt[i]){
        int y=to[i];
        if(!val[i]||dep[y]!=dep[x]+1)continue;
        go=dfs(y,min(rest,val[i]));
        if(go)val[i]-=go,val[i^1]+=go,rest-=go;
        else dep[y]=0;
        if(!rest)break;
    }
    return in-rest;
}
int p[N];
bool vis[N];
void find(int x){
    vis[x]=true;
    for(int i=head[x];i;i=nxt[i]){
        int y=to[i];
        if(!val[i]||vis[y])continue;
        find(y);
    }
}
signed main(){
    n=read();m=read();
    fo(i,1,m){
        int x=read(),y=read(),v=read();
        add_edg(x,y,v);add_edg(y,x,0);
        add_edg(y,x,v);add_edg(x,y,0);
    }
    memcpy(va,val,sizeof(val));
    memcpy(hea,head,sizeof(head));
    fo(i,2,n)p[i]=1;
    fo(i,2,n){
        s=i;t=p[i];
        memcpy(val,va,sizeof(va));
        int res=0;
        while(bfs())res+=dfs(s,inf);
        st.insert(res);
        memset(vis,false,sizeof(vis));
        find(s);
        for(int j=i;j<=n;j++)if(p[j]==t&&vis[j])p[j]=s;
    }
    printf("%d",st.size());
}

刚刚发现\(Luogu\)上有这个板子题Luogu4897,然而我懒得写了

对偶图与最小割的关系

对偶图是相对于平面图来说的,平面图就是我们常说的那种图,所有边只相交与顶点

每一个平面图都有一个与之对应的对偶图

对偶图以每个平面图中的最小环圈出来的地方为顶点

将平面图中的边旋转\(90^。\)作为对偶图的边

这个顺时针还是逆时针就看个人习惯了,但是所有边的旋转方向要是一样的

在平面图上找最小割或者最大流就相当于是在对偶图上求最短路

只不过做的时候需要自己新建节点,按照最小割的方向来确定源点和汇点

比如说平面图的源点在左上,汇点在右下,那么对偶图的源点在左下汇点在右上

当然如果不是矩形的话就要灵活判断了

Luogu2046 NOI2010 海拔

code
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define fo(i,x,y) for(int i=(x);i<=(y);i++)
#define fu(i,x,y) for(int i=(x);i>=(y);i--)
int read(){
    int s=0,t=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')t=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
    return s*t;
}
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=260005;
const int M=N*4;
int n,s=260001,t=260002,ans;
int id(int x,int y){
    if(x==n+1||y==0)return s;
    if(x==0||y==n+1)return t;
    return (x-1)*(n+1)+y;
}
struct E{int to,nxt,val;}e[M*2];
int head[N],hea[N],rp=1;
void add_edg(int x,int y,int z){
    e[++rp].to=y;e[rp].val=z;
    e[rp].nxt=head[x];head[x]=rp;
}
int dis[N];bool vis[N];
struct node{
    int x,d;
    bool operator < (node a)const{
        return d>a.d;
    }
};
priority_queue<node> q;
void dij(){
    memset(dis,0x3f,sizeof(dis));
    q.push(node{s,0});dis[s]=0;
    while(!q.empty()){
        int x=q.top().x;q.pop();
        // if(x==t)cout<<x<<endl;
        if(vis[x])continue;
        vis[x]=true;
        for(int i=head[x];i;i=e[i].nxt){
            int y=e[i].to;
            if(dis[y]<=dis[x]+e[i].val)continue;
            dis[y]=dis[x]+e[i].val;
            q.push(node{y,dis[y]});
        }
    }
}
signed main(){
    n=read();
    fo(i,1,n+1)fo(j,1,n){
        int x=read();
        add_edg(id(i,j),id(i-1,j),x);
    }
    fo(i,1,n)fo(j,1,n+1){
        int x=read();
        add_edg(id(i,j-1),id(i,j),x);
    }
    fo(i,1,n+1)fo(j,1,n){
        int x=read();
        add_edg(id(i-1,j),id(i,j),x);
    }
    fo(i,1,n)fo(j,1,n+1){
        int x=read();
        add_edg(id(i,j),id(i,j-1),x);
    }
    dij();ans=dis[t];
    printf("%lld",ans);
}
最小割写法(90pts)
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define fo(i,x,y) for(int i=(x);i<=(y);i++)
#define fu(i,x,y) for(int i=(x);i>=(y);i--)
int read(){
    int s=0,t=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')t=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
    return s*t;
}
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=260005;
const int M=N*4;
int n,s,t,ans;
int id(int x,int y){return (x-1)*(n+1)+y;}
struct E{int to,nxt,val;}e[M*2];
int head[N],hea[N],rp=1;
void add_edg(int x,int y,int z){
    e[++rp].to=y;e[rp].val=z;
    e[rp].nxt=head[x];head[x]=rp;
}
int dis[N];
bool bfs(){
    memset(dis,0x3f,sizeof(dis));
    memcpy(head,hea,sizeof(hea));
    queue<int> q;while(!q.empty())q.pop();
    q.push(s);dis[s]=0;
    while(!q.empty()){
        int x=q.front();q.pop();
        for(int i=head[x];i;i=e[i].nxt){
            int y=e[i].to;
            if(!e[i].val||dis[y]<=dis[x]+1)continue;
            dis[y]=dis[x]+1;q.push(y);
            if(y==t)return true;
        }
    }
    return false;
}
int dfs(int x,int in){
    if(x==t)return in;
    int rest=in,go=0;
    for(int i=head[x];i;head[x]=i=e[i].nxt){
        int y=e[i].to;
        if(!e[i].val||dis[y]!=dis[x]+1)continue;
        go=dfs(y,min(rest,e[i].val));
        if(go)e[i].val-=go,e[i^1].val+=go,rest-=go;
        else dis[y]=0;
        if(!rest)break;
    }
    return in-rest;
}
signed main(){
    n=read();s=id(1,1);t=id(n+1,n+1);
    fo(i,1,n+1)fo(j,1,n){
        int x=read();
        add_edg(id(i,j),id(i,j+1),x);
        add_edg(id(i,j+1),id(i,j),0);
    }
    fo(i,1,n)fo(j,1,n+1){
        int x=read();
        add_edg(id(i,j),id(i+1,j),x);
        add_edg(id(i+1,j),id(i,j),0);
    }
    fo(i,1,n+1)fo(j,1,n){
        int x=read();
        add_edg(id(i,j+1),id(i,j),x);
        add_edg(id(i,j),id(i,j+1),0);
    }
    fo(i,1,n)fo(j,1,n+1){
        int x=read();
        add_edg(id(i+1,j),id(i,j),x);
        add_edg(id(i,j),id(i+1,j),0);
    }
    memcpy(hea,head,sizeof(head));
    while(bfs())ans+=dfs(s,inf);
    printf("%lld",ans);
}

最大权闭合子图

在做植物大战僵尸,对面的二货告诉我这个可以自己切,他错误的估计了我的实力......

于是我就学会了最大权闭合子图,于是对最小割的理解再一次深入

闭合子图就是说,一个点在选出来的子图中,它的连向的点也必须在这个图中

这是对于有向图来说的,可以有环,但是有的时候环的贡献不能算进去,这个时候就要减掉

因为闭合环的贡献是一定可以加到总权值里的

咋找最大权值和的这个东西??

最小割!!

我们把点分成正权值和负权值

超级源点向正权值连边,权值是点权

负权值向超级汇点连边,权值是点权的相反数

原图的边也连上,权值是\(inf\),这个保证了我割的时候只能割掉和源汇点的连边

这样的话,最终答案就是正权值加和减去最小割

如果割掉和超级源点的连边的话,那我这个正权值的点就不要了

如果不割,那就要割掉对面负权值的边,这就说明我要选上这个负权值的点

经常用这个东西来解决一些有依赖的问题,我认为有依赖的背包可以这么写

Luogu2805 NOI2009 植物大战僵尸

\(HZOJ\)上漂亮的提交记录

image

code
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define fo(i,x,y) for(int i=(x);i<=(y);i++)
#define fu(i,x,y) for(int i=(x);i>=(y);i--)
int read(){
    int s=0,t=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')t=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
    return s*t;
}
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=605;
const int M=N*N;
int n,m,s=601,t=602,w[N],ans;
int id(int x,int y){return (x-1)*m+y;}
struct E{int to,nxt,val;}e[M*4],d[M];
int head[N],hea[N],ht[N],rp;
void add_e(int x,int y,int z){
    e[++rp].to=y;e[rp].val=z;
    e[rp].nxt=head[x];head[x]=rp;
}
int dis[N];
bool bfs(){
    memset(dis,0x3f,sizeof(dis));
    memcpy(head,hea,sizeof(hea));
    queue<int> q;while(!q.empty())q.pop();
    q.push(s);dis[s]=0;
    while(!q.empty()){
        int x=q.front();q.pop();
        for(int i=head[x];i;i=e[i].nxt){
            int y=e[i].to;
            if(!e[i].val||dis[y]<=dis[x]+1)continue;
            dis[y]=dis[x]+1;q.push(y);
            if(y==t)return true;
        }
    }
    return false;
}
int dfs(int x,int in){
    if(x==t)return in;
    int rest=in,go=0;
    for(int i=head[x];i;head[x]=i=e[i].nxt){
        int y=e[i].to;
        if(!e[i].val||dis[y]!=dis[x]+1)continue;
        go=dfs(y,min(rest,e[i].val));
        if(go)e[i].val-=go,e[i^1].val+=go,rest-=go;
        else dis[y]=0;
        if(!rest)break;
    }
    return in-rest;
}
int du[N];
void add_d(int x,int y){
    d[++rp].to=y;d[rp].nxt=ht[x];
    ht[x]=rp;du[y]++;
}
void topu(){
    queue<int> q;while(!q.empty())q.pop();
    fo(i,1,n*m)if(!du[i])q.push(i);
    while(!q.empty()){
        int x=q.front();q.pop();
        for(int i=ht[x];i;i=d[i].nxt){
            int y=d[i].to;
            if(!--du[y])q.push(y);
        }
    }
}
signed main(){
    n=read();m=read();
    fo(i,1,n)fo(j,1,m){
        w[id(i,j)]=read();
        int sm=read();
        fo(k,1,sm){
            int x=read()+1,y=read()+1;
            add_d(id(i,j),id(x,y));
        }
        if(j!=1)add_d(id(i,j),id(i,j-1));
    }
    topu();rp=1;
    fo(i,1,n)fo(j,1,m){
        if(du[id(i,j)])continue;
        if(w[id(i,j)]>=0){
            ans+=w[id(i,j)];
            add_e(s,id(i,j),w[id(i,j)]);
            add_e(id(i,j),s,0);
        }
        else {
            add_e(id(i,j),t,-w[id(i,j)]);
            add_e(t,id(i,j),0);
        }
        for(int k=ht[id(i,j)];k;k=d[k].nxt){
            int y=d[k].to;
            if(du[y])continue;
            add_e(y,id(i,j),inf);
            add_e(id(i,j),y,0);
        }
    }
    memcpy(hea,head,sizeof(head));
    while(bfs())ans-=dfs(s,inf);
    printf("%lld",ans);
}

最小点权覆盖和最大点权独立集(二分图)

定义一张图的覆盖为选出一些点可以使图上所有边都被覆盖

我们要求这样的覆盖中点权最小的那个

左部点连源点,边权为点权,右部点连汇点,边权为点权

把原图的边连上,边权是\(inf\)

求最小割,这样所有边两端都必须选一个,所以最小割就是最小点权

定义一张图的独立集是选出一些点来是的这些点之间两两没有连边

仍然是按照上面那样建图,我们仍然不能同时选一条边的两个端点

好像和上面那个是一样的,其实就是上面那个最小割割掉的点的补集

于是最大点权独立集就是总点权减去最小点权覆盖

最小链覆盖/最长反链

最小链覆盖的意思就是我用最少的链来覆盖所有的点

还有一个结论就是最小链覆盖等于最长反链,这些东西也可以用网络流求的

拆点,因为链上的点的度数一定是2或1,所以我们拆成入点和出点

分开放,一边连s,一边连t,边权都是1,再把原图的边都连上,跑最大流,用总点数减去就是了

什么意思?就是我认为开始的时候每个点单独成为一个链,每个流量相当于是将两条链合并成为一个链

上面是链无交的情况,有交的话就把可达的点都连边就完事了

例题

Luogu4126 AHOI2009 最小割

残量网络上跑\(tarjan\),妙极了这题

题目里让我们找可以在最小割上的边和一定在最小割上的边

首先我们在求最小割的时候就是用最大流求的

于是如果想让某一条边成为最小割,一定要想方设法让它满流(如果没有满的话,那么割它一定不是最优的)

那么我们就有了第一个条件:满流.

既然我们需要满流,那就不能存在一种情况使得它是不满的

所以我们要求不能在这条边上退流,也就是我这条边的流量不能从别的边上流走

于是乎,我们不可以这条边连的两个点之间找到另外一条路径,也就是说,流量不能从别的地方走

以上就是一条边可以在最小割上的条件,保证在任意情况下都是满流的

那么要是想让一条边一定在最小割上,我们需要满足更加严苛的要求

我们要让这条边成为这条边上的流流过的路径上唯一一个满流边

那么前面的点可以和源点联通,后面的点可以和汇点联通

于是我们的条件就说完了

那么我们直接在残量网络上进行缩点,判断就行了

可行边:满流+不在同一强连通分量

必须边:满流+分别在源点的分量和汇点的分量中

code
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define fo(i,x,y) for(int i=(x);i<=(y);i++)
#define fu(i,x,y) for(int i=(x);i>=(y);i--)
int read(){
    int s=0,t=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')t=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
    return s*t;
}
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=4005;
const int M=60005;
int n,m,s,t;
struct D{int x,y,z;}d[M];
struct E{int to,nxt,val;}e[M*2];
int head[N],hea[N],rp=1;
void add_edg(int x,int y,int z){
    e[++rp].to=y;e[rp].val=z;
    e[rp].nxt=head[x];head[x]=rp;
}
int dis[N];
bool bfs(){
    memset(dis,0x3f,sizeof(dis));
    memcpy(head,hea,sizeof(hea));
    queue<int> q;while(!q.empty())q.pop();
    q.push(s);dis[s]=0;
    while(!q.empty()){
        int x=q.front();q.pop();
        for(int i=head[x];i;i=e[i].nxt){
            int y=e[i].to;
            if(!e[i].val||dis[y]<=dis[x]+1)continue;
            dis[y]=dis[x]+1;q.push(y);
            if(y==t)return true;
        }
    }
    return false;
}
int dfs(int x,int in){
    if(x==t)return in;
    int rest=in,go=0;
    for(int i=head[x];i;head[x]=i=e[i].nxt){
        int y=e[i].to;
        if(!e[i].val||dis[y]!=dis[x]+1)continue;
        go=dfs(y,min(rest,e[i].val));
        if(go)e[i].val-=go,e[i^1].val+=go,rest-=go;
        else dis[y]=0;
        if(!rest)break;
    }
    return in-rest;
}
int sta[N],top,bl[N],col;
int dfn[N],low[N],cnt;
bool vis[N];
void tarjan(int x){
    sta[++top]=x;low[x]=dfn[x]=++cnt;vis[x]=true;
    for(int i=head[x];i;i=e[i].nxt){
        int y=e[i].to;
        if(!e[i].val)continue;
        if(!dfn[y])tarjan(y),low[x]=min(low[x],low[y]);
        else if(vis[y]) low[x]=min(low[x],dfn[y]);
    }
    if(dfn[x]==low[x]){
        bl[x]=++col;vis[x]=false;
        while(sta[top]!=x){
            vis[sta[top]]=false;
            bl[sta[top]]=col,top--;
        }
        top--;
    }
}
signed main(){
    n=read();m=read();s=read();t=read();
    fo(i,1,m){
        int x=read(),y=read(),z=read();
        d[i]=D{x,y,z};
        add_edg(x,y,z);add_edg(y,x,0);
    }
    memcpy(hea,head,sizeof(head));
    int sum=0;
    while(bfs())sum+=dfs(s,inf);
    fo(i,1,n)if(!dfn[i])tarjan(i);
    fo(i,1,m){
        int x=d[i].x,y=d[i].y,ans1,ans2;
        if(bl[x]==bl[y]||e[i*2].val!=0)ans1=ans2=0;
        else {
            ans1=1;ans2=0;
            if(bl[x]==bl[s]&&bl[y]==bl[t])ans2=1;
        }
        printf("%lld %lld\n",ans1,ans2);
    }
}

Luogu3756 CQOI2017 老C的方块

好像写了一上午这个题吧,对染色法有了初步的了解

一开始,我就直接对着九种情况分类讨论去了,然后出锅了,只有10分

不知道为啥只有这么点分,原因是我分类讨论把一些本来没有关系的格子接在一起了

直接去看题解,因为是四个格子,所以染成四种颜色

这样每一个不合法的形状里面都恰好有四种颜色,我只要干掉一种就行了

染完色之后连边有一个好处,每一条边只会被链接一次,保证了不会重复

按照题意给的那些不好的形状,确定连边的顺序,很好的练手题!

code
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define fo(i,x,y) for(int i=(x);i<=(y);i++)
#define fu(i,x,y) for(int i=(x);i>=(y);i--)
#define pa pair<int,int>
#define mk(x,y) make_pair(x,y)
int read(){
    int s=0,t=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')t=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
    return s*t;
}
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=2e5+5;
const int M=7e5+5;
int c,r,n,m,s=200001,t=200002,ans;
struct E{int to,nxt,val;}e[M*2];
int head[N],hea[N],rp=1;
void add_edg(int x,int y,int z){
    e[++rp].to=y;e[rp].val=z;
    e[rp].nxt=head[x];head[x]=rp;
}
int dis[N];
bool bfs(){
    memset(dis,0x3f,sizeof(dis));
    memcpy(head,hea,sizeof(hea));
    queue<int> q;while(!q.empty())q.pop();
    q.push(s);dis[s]=0;
    while(!q.empty()){
        int x=q.front();q.pop();
        for(int i=head[x];i;i=e[i].nxt){
            int y=e[i].to;
            if(!e[i].val||dis[y]<=dis[x]+1)continue;
            dis[y]=dis[x]+1;q.push(y);
            if(y==t)return true;
        }
    }
    return false;
}
int dfs(int x,int in){
    if(x==t)return in;
    int rest=in,go=0;
    for(int i=head[x];i;head[x]=i=e[i].nxt){
        int y=e[i].to;
        if(!e[i].val||dis[y]!=dis[x]+1)continue;
        go=dfs(y,min(rest,e[i].val));
        if(go)e[i].val-=go,e[i^1].val+=go,rest-=go;
        else dis[y]=0;
        if(!rest)break;
    }
    return in-rest;
}
bool vip[N],vib[N];
map<pa,int> mp;
bool pd(int x,int y){return x>0&&x<=c&&y>0&&y<=r&&mp.find(mk(x,y))!=mp.end();}
struct P{int x,y,z;}p[N];
int col(int x,int y){
    if((x%4==1&&y%4==1)||(x%4==1&&y%4==3)||(x%4==0&&y%4==2)||(x%4==0&&y%4==0))return 1;
    if((x%4==1&&y%4==2)||(x%4==1&&y%4==0)||(x%4==0&&y%4==1)||(x%4==0&&y%4==3))return 2;
    if((x%4==2&&y%4==1)||(x%4==2&&y%4==3)||(x%4==3&&y%4==2)||(x%4==3&&y%4==0))return 3;
    if((x%4==2&&y%4==2)||(x%4==2&&y%4==0)||(x%4==3&&y%4==1)||(x%4==3&&y%4==3))return 4;
}
int px1[4]={1,0,-1,0},py1[4]={0,1,0,-1};
int px2[4]={1,1,-1,-1},py2[4]={1,-1,1,-1};
signed main(){
    c=read();r=read();n=read();
    fo(i,1,n){
        p[i].x=read();p[i].y=read();p[i].z=read();
        mp.insert(make_pair(mk(p[i].x,p[i].y),i));
        add_edg(i,i+n,p[i].z),add_edg(i+n,i,0);
        if(col(p[i].x,p[i].y)==2){
            add_edg(s,i,inf),add_edg(i,s,0);
        }
        if(col(p[i].x,p[i].y)==4)
            add_edg(i+n,t,inf),add_edg(t,i+n,0);
    }
    fo(i,1,n){
        int x=p[i].x,y=p[i].y;
        if(col(x,y)==2){
            fo(j,0,3)if(pd(x+px1[j],y+py1[j])&&col(x+px1[j],y+py1[j])==1){
                int id=mp[mk(x+px1[j],y+py1[j])];
                add_edg(i+n,id,inf),add_edg(id,i+n,0);
            }
        }
        if(col(x,y)==1){
            fo(j,0,3)if(pd(x+px1[j],y+py1[j])&&col(x+px1[j],y+py1[j])==3){
                int id=mp[mk(x+px1[j],y+py1[j])];
                add_edg(i+n,id,inf),add_edg(id,i+n,0);
            }
        }
        if(col(x,y)==3){
            fo(j,0,3)if(pd(x+px1[j],y+py1[j])&&col(x+px1[j],y+py1[j])==4){
                int id=mp[mk(x+px1[j],y+py1[j])];
                add_edg(i+n,id,inf),add_edg(id,i+n,0);
            }
        }
    }
    memcpy(hea,head,sizeof(head));
    while(bfs())ans+=dfs(s,inf);
    printf("%lld",ans);
}

费用流

最小费用最大流,直接上的\(EK\)算法,毕竟\(ZKW\)也不咋快,就直接学\(EK\)

每次都\(SPFA\)一遍,找到当前的最短路,然后每次增广一条

计算的时候就不用\(dfs\)了,直接回溯一遍就好了

改了改码风,因为边上要记的变量太多了,于是用结构体封了一下

Luogu3381 最小费用最大流

code
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define fo(i,x,y) for(int i=(x);i<=(y);i++)
#define fu(i,x,y) for(int i=(x);i>=(y);i--)
int read(){
    int s=0,t=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')t=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
    return s*t;
}
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=5e3+5;
const int M=5e4+5;
int n,m,s,t,ans1,ans2;
struct E{int to,nxt,val,cot;}e[M*2];
int head[N],rp=1;
void add_edg(int x,int y,int z,int w){
    e[++rp].to=y;e[rp].val=z;e[rp].cot=w;
    e[rp].nxt=head[x];head[x]=rp;
}
int dis[N];bool vis[N];
int pre[N],epr[N],flo[N];
bool spfa(){
    memset(dis,0x3f,sizeof(dis));
    queue<int> q;
    while(!q.empty())q.pop();
    q.push(s);dis[s]=0;vis[s]=true;flo[s]=inf;
    while(!q.empty()){
        int x=q.front();q.pop();vis[x]=false;
        for(int i=head[x];i;i=e[i].nxt){
            int y=e[i].to;
            if(!e[i].val||dis[y]<=dis[x]+e[i].cot)continue;
            dis[y]=dis[x]+e[i].cot;
            flo[y]=min(flo[x],e[i].val);
            pre[y]=x;epr[y]=i;
            if(!vis[y])q.push(y),vis[y]=true;
        }
    }
    return dis[t]!=inf;
}
signed main(){
    n=read();m=read();s=read();t=read();
    fo(i,1,m){
        int x=read(),y=read(),z=read(),w=read();
        add_edg(x,y,z,w);
        add_edg(y,x,0,-w);
    }
    while(spfa()){
        int now=t;
        while(now!=s){
            e[epr[now]].val-=flo[t];
            e[epr[now]^1].val+=flo[t];
            now=pre[now];
        }
        ans1+=flo[t];ans2+=flo[t]*dis[t];
    }
    printf("%lld %lld",ans1,ans2);
}

例题

Luogu4553 80人环游世界

想破脑袋也想不到是这样保证每个点都跑满的

拆成两个点这个是可以想的到的,一个入点,一个出点

我们不能将一条环游路径看成是一个流

因为我们不知道要从哪里走,并且不可以保证每个国家的都走满

所以我们只能限制当前国家的出入情况

我们从\(s\)建立连向入点的边,边权为\(v\),花费为\(0\),这个表示我这个国家一共走出来过\(v\)个人

从出点建立向\(t\)的边,边权为\(v\),花费为\(0\),表示这个国家进去过\(v\)个人

这个地方可以这样理解,我进去的人就直接从超级汇点离开了,下次进来的时候就从超级源点进来,继续下一个航线

也就是说,我这张网络流的图,只表示单次航线,多走几次就构成了一个人的旅程

然后我们在两个国家之间航线

忘了,有\(m\)个人,可以从任意节点出发,所以我们建立一个新点,\(s\)连向它,边权为\(m\),花费\(0\),再建立向所有国家的边,边权\(inf\),花费\(0\)

这样的话,因为这个新点花费为\(0\),所以一定先把这个点走完

因为保证这个问题是有解的,所以最后最大流的时候,一定把所有出点的流量都走完了

而所有入点是不一定跑满的,因为有可能在某一个国家停下来了

并且所有入点的边权加和和总入点经过的流量之间的差恰好是\(m,\)因为入点和出点的加和一致,而源点可以先走新点

正好对应了\(m\)个人都会在某一城市停下来!!!

简直是妙哉!!!

code
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define fo(i,x,y) for(int i=(x);i<=(y);i++)
#define fu(i,x,y) for(int i=(x);i>=(y);i--)
int read(){
    int s=0,t=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')t=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
    return s*t;
}
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=405;
const int M=4e4+605;
int n,m,s=402,t=403,o=401,v[N],ans;
struct E{int to,nxt,val,cot;}e[M*2];
int head[N],rp=1;
void add_edg(int x,int y,int z,int w){
    e[++rp].to=y;e[rp].val=z;e[rp].cot=w;
    e[rp].nxt=head[x];head[x]=rp;
}
int dis[N];bool vis[N];
int pre[N],epr[N],flo[N];
bool spfa(){
    memset(dis,0x3f,sizeof(dis));
    queue<int> q;while(!q.empty())q.pop();
    q.push(s);dis[s]=0;vis[s]=true;flo[s]=inf;
    while(!q.empty()){
        int x=q.front();q.pop();vis[x]=false;
        //cout<<x<<endl;
        for(int i=head[x];i;i=e[i].nxt){
            int y=e[i].to;
            if(!e[i].val||dis[y]<=dis[x]+e[i].cot)continue;
            dis[y]=dis[x]+e[i].cot;
            flo[y]=min(flo[x],e[i].val);
            pre[y]=x;epr[y]=i;
            if(!vis[y])q.push(y),vis[y]=true;
        }
    }
    return dis[t]!=inf;
}
signed main(){
    n=read();m=read();
    add_edg(s,o,m,0);add_edg(o,s,0,0);
    fo(i,1,n){
        v[i]=read();
        add_edg(s,i,v[i],0);add_edg(i,s,0,0);
        add_edg(i+n,t,v[i],0);add_edg(t,i+n,0,0);
        add_edg(o,i+n,inf,0);add_edg(i+n,o,0,0);
    }
    fo(i,1,n)fo(j,i+1,n){
        int x=read();if(x==-1)continue;
        add_edg(i,j+n,inf,x);add_edg(j+n,i,0,-x);
    }
    while(spfa()){
        int now=t;//cout<<"SB"<<endl;
        while(now!=s){
            e[epr[now]].val-=flo[t];
            e[epr[now]^1].val+=flo[t];
            now=pre[now];
        }
        ans+=flo[t]*dis[t];
    }
    printf("%lld",ans);
}

Luogu2050 NOI2012 美食节

这个题和修车有点像啊,就是个数据加强版

发现,我们给一个厨师安排\(n\)道菜的时候,第一个人的时间被计算了\(n\)次,第二个\(n-1\)次,依次类推

所以我们就把每个厨师都拆成\(p*m\)个点,这里的\(p\)表示总人数

建图就直接把权值乘上几就好了

假设我们现在把所有的点和所有的边都搞出来了,那么我们直接跑最小费用流就是答案

那么我们咋优化它??

我们发现第一条路一定是当前厨师乘的倍数最小的那个点

所以我们只建当前厨师最小倍数的点,当它被流走的时候再建立下一条边

code
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define fo(i,x,y) for(int i=(x);i<=(y);i++)
#define fu(i,x,y) for(int i=(x);i>=(y);i--)
int read(){
    int s=0,t=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')t=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
    return s*t;
}
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=80045;
const int M=2e5+5;
int n,m,p[45],w[45][105],s=80041,t=80042,ans;
int id(int x,int y){return (x-1)*m+y;}
int ck(int id){return (id-1)%m+1;}
int ti(int id){return (id-1)/m+1;}
struct E{int to,nxt,val,cot;}e[M*2];
int head[N],rp=1;
void add_edg(int x,int y,int z,int v){
    e[++rp].to=y;e[rp].val=z;e[rp].cot=v;
    e[rp].nxt=head[x];head[x]=rp;
}
int dis[N];bool vis[N];
int pre[N],epr[N],flo[N];
bool spfa(){
    memset(dis,0x3f,sizeof(dis));
    queue<int> q;while(!q.empty())q.pop();
    q.push(s);dis[s]=0;vis[s]=true;flo[s]=inf;
    while(!q.empty()){
        int x=q.front();q.pop();vis[x]=false;
        for(int i=head[x];i;i=e[i].nxt){
            int y=e[i].to;
            if(!e[i].val||dis[y]<=dis[x]+e[i].cot)continue;
            dis[y]=dis[x]+e[i].cot;
            flo[y]=min(flo[x],e[i].val);
            pre[y]=x;epr[y]=i;
            if(!vis[y])q.push(y),vis[y]=true;
        }
    }
    return dis[t]!=inf;
}
signed main(){
    n=read();m=read();int sum=0;
    fo(i,1,n)p[i]=read(),sum+=p[i];
    fo(i,1,n)fo(j,1,m)w[i][j]=read();
    fo(i,1,n)add_edg(s,i+sum*m,p[i],0),add_edg(i+sum*m,s,0,0);
    fo(j,1,m){
        add_edg(id(1,j),t,1,0);
        add_edg(t,id(1,j),0,0);
    }
    fo(i,1,n)fo(j,1,m){
        add_edg(i+sum*m,id(1,j),1,w[i][j]);
        add_edg(id(1,j),i+sum*m,0,-w[i][j]);
    }
    while(spfa()){
        int now=t;
        while(now!=s){
            e[epr[now]].val-=flo[t];
            e[epr[now]^1].val+=flo[t];
            now=pre[now];
        }
        ans+=flo[t]*dis[t];
        now=e[epr[t]^1].to+m;
        fo(i,1,n){
            add_edg(i+sum*m,now,1,w[i][ck(now)]*ti(now));
            add_edg(now,i+sum*m,0,-w[i][ck(now)]*ti(now));
        }
        add_edg(now,t,1,0);
        add_edg(t,now,0,0);
    }
    printf("%lld",ans);
}

有上下界的网络流

上下界是啥?就是每一条边不仅有最大流量,还有一个最小流量

就是说我这条边的流量必须在上下界之间,这样就有可行和不可行之分了

所以我们要先判可行,再找最大最小

无源汇上下界可行流

这个是所有上下界网络流的基础,学会这个,剩下的就轻而易举了

这个要求的就是使所有点流量平衡(流入量=流出量)并且满足边的上下界的流

我们知道\(dinic\)只能跑有源汇的最大流,而这个无源汇,我们思考如何给它加上一个超级源汇点?

我们发现直接加是不可以的,因为有一些边可能会不满足下界,而\(dinic\)没有调整功能,退流也不能自行控制方向

考虑先让所有的边满足下界,我们叫它初始流,但是这样的话有一些点就不能满足流量平衡了

没关系,我们待会再调整......

我们将所有的边的流量先流成下界,这样我们建立一个残量网络,边权是上界减下界

这样的话我们每一个点会有一个流入量和一个流出量,如果不相等的话,我们需要调整它

用啥调整,用另外一个附加流

这个附加流加上初始流,每个点的流量守恒

那么也就意味着,附加流的流量也是不守恒的......

如果初始流的流入大于流出,那么附加流的流入就要小于流出,这个很好理解吧

那我们要怎么保证这个东西嘞?

如果想要让流出多的话,我们就给这个点新加一条边,给它增加点流入量,这样的话如果在流量守恒的情况下跑,流出量就多了

很妙??!!

我们用\(dinic\)处理附加流的问题

定义\(cha[]\)数组,表示在初始流中每个点的流入量减去流出量,这个可以直接找到的

那么如果这个东西小于零,证明初始流的流入小于流出,我要让附加流的流入大于流出

所以我给这个点加一条边连向超级汇点,边权为差值的相反数

这样就让在守恒情况下,流入大于流出了

如果这个东西大于零,证明初始流的流入大于流出,我要让附加流的流入小于流出

那就给这个点加一条边从超级源点连向它的边,边权为差值

这样流入多了,附加流的流出也就多了

然后我们从超级源点向超级汇点跑\(dinic\),如果最大流等于我连的边的权值和的话(就是源点出发的边的流量都跑满了)

那就证明可以找到一个附加流使得满足差值条件,那么附加流+初始流就是一个合法的流了,流量守恒并且符合上下界

源点的流出量一定等于汇点的流入量,就是负差值相加的绝对值等于正差值相加的绝对值

因为原图的流量守恒,某个点流出多了,这个点的流入必然少了,那么必然有一个点的流出流向了这个点,那么差值就守恒了

因为每一条边对两个点的贡献一正一负大小相等,所以差值守恒

LOJ115 无源汇有上下界可行流

code
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define fo(i,x,y) for(int i=(x);i<=(y);i++)
#define fu(i,x,y) for(int i=(x);i>=(y);i--)
int read(){
    int s=0,t=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')t=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
    return s*t;
}
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=205;
const int M=10405;
int n,m,s=201,t=202,ans[M];
struct E{int to,nxt,val,id;}e[M*2];
int head[N],hea[N],rp=1;
void add_edg(int x,int y,int z,int id){
    e[++rp].to=y;e[rp].val=z;e[rp].id=id;
    e[rp].nxt=head[x];head[x]=rp;
}
int dis[N];
bool bfs(){
    memset(dis,0x3f,sizeof(dis));
    memcpy(head,hea,sizeof(hea));
    queue<int> q;while(!q.empty())q.pop();
    q.push(s);dis[s]=0;
    while(!q.empty()){
        int x=q.front();q.pop();
        for(int i=head[x];i;i=e[i].nxt){
            int y=e[i].to;
            if(!e[i].val||dis[y]<=dis[x]+1)continue;
            dis[y]=dis[x]+1;q.push(y);
            if(y==t)return true;
        }
    }
    return false;
}
int dfs(int x,int in){
    if(x==t)return in;
    int rest=in,go=0;
    for(int i=head[x];i;head[x]=i=e[i].nxt){
        int y=e[i].to;
        if(!e[i].val||dis[y]!=dis[x]+1)continue;
        go=dfs(y,min(rest,e[i].val));
        if(go)e[i].val-=go,e[i^1].val+=go,rest-=go;
        else dis[y]=0;
        if(!rest)break;
    }
    return in-rest;
}
int dinic(){
    int ret=0;memcpy(hea,head,sizeof(head));
    while(bfs())ret+=dfs(s,inf);
    return ret;
}
int low[M],hei[M],cha[N],sch;
signed main(){
    n=read();m=read();
    fo(i,1,m){
        int x=read(),y=read();
        low[i]=read();hei[i]=read();
        cha[x]-=low[i];cha[y]+=low[i];
        add_edg(x,y,hei[i]-low[i],i);
        add_edg(y,x,0,i);
    }
    fo(i,1,n){
        if(cha[i]<0){
            add_edg(i,t,-cha[i],0);
            add_edg(t,i,0,0);
        }
        else {
            sch+=cha[i];
            add_edg(s,i,cha[i],0);
            add_edg(i,s,0,0);
        }
    }
    if(dinic()==sch){
        printf("YES\n");
        fo(i,2,rp)if(i&1)ans[e[i].id]=e[i].val+low[e[i].id];
        fo(i,1,m)printf("%lld\n",ans[i]);
    }
    else printf("NO\n");
}

有源汇上下界可行流(最大流/最小流)

有源汇我们可以直接转化为无源汇,就是给源汇点之间连一条上界是\(inf\),下界是\(0\)的边

这样再跑一遍无源汇的可行流就是有源汇的可行流

注意这个地方,原来的源汇点,不要和虚拟的源汇点弄混了,小心调死......

那最大流的话,就是在有源汇的可行流基础上加一个从源点到汇点的最大流,可行流+这个最大流就是答案

最小流的话,就是从汇点往源点跑最大流,这样相当于退流,可行流-这个最大流就是答案

这里的可行流就是源汇点之间的那个新建的边的流量,即反边的剩余流量

不会不符合条件,因为从源点往汇点跑,流量最大就是上界,反着跑的话,流量最小不会小于下界......

千万别忘记把原图的源汇点连起来的那条边删掉,会死人的

Luogu5192 东方文花帖

这个就是直接源点向每一天,每一天向每一个少女,每一个少女向汇点,跑上下界最大流即可

code
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define fo(i,x,y) for(int i=(x);i<=(y);i++)
#define fu(i,x,y) for(int i=(x);i>=(y);i--)
int read(){
    int s=0,t=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')t=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
    return s*t;
}
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=1405;
const int M=120505;
int n,m,s,t,ss=1403,tt=1404,ans;
struct E{int to,nxt,val,id;}e[M*2];
int head[N],hea[N],rp=1;
void add_edg(int x,int y,int z,int id){
    e[++rp].to=y;e[rp].val=z;e[rp].id=id;
    e[rp].nxt=head[x];head[x]=rp;
}
int dis[N];
bool bfs(){
    memset(dis,0x3f,sizeof(dis));
    memcpy(head,hea,sizeof(hea));
    queue<int> q;while(!q.empty())q.pop();
    q.push(s);dis[s]=0;
    while(!q.empty()){
        int x=q.front();q.pop();
        for(int i=head[x];i;i=e[i].nxt){
            int y=e[i].to;
            if(!e[i].val||dis[y]<=dis[x]+1)continue;
            dis[y]=dis[x]+1;q.push(y);
            if(y==t)return true;
        }
    }
    return false;
}
int dfs(int x,int in){
    if(x==t)return in;
    int rest=in,go=0;
    for(int i=head[x];i;head[x]=i=e[i].nxt){
        int y=e[i].to;
        if(!e[i].val||dis[y]!=dis[x]+1)continue;
        go=dfs(y,min(rest,e[i].val));
        if(go)e[i].val-=go,e[i^1].val+=go,rest-=go;
        else dis[y]=0;
        if(!rest)break;
    }
    return in-rest;
}
int dinic(){
    int ret=0;memcpy(hea,head,sizeof(head));
    while(bfs())ret+=dfs(s,inf);
    return ret;
}
int cnt,low[M],hei[M],cha[M],sch;
signed main(){
    while(scanf("%lld%lld",&n,&m)!=EOF){
        memset(head,0,sizeof(head));
        memset(cha,0,sizeof(cha));
        ans=sch=cnt=0;rp=1;s=0;t=n+m+1;
        fo(i,1,m){
            int x=read();cnt++;
            low[cnt]=x;hei[cnt]=inf;
            cha[i+n]-=low[cnt];cha[t]+=low[cnt];
            add_edg(i+n,t,inf-x,cnt);
            add_edg(t,i+n,0,cnt);
        }
        fo(i,1,n){
            int c=read(),d=read();
            cnt++;low[cnt]=0;hei[cnt]=d;
            cha[s]-=low[cnt];cha[i]+=low[cnt];
            add_edg(s,i,d,cnt);
            add_edg(i,s,0,cnt);
            fo(j,1,c){
                int T=read()+1,L=read(),R=read();
                cnt++;low[cnt]=L;hei[cnt]=R;
                cha[i]-=low[cnt];cha[T+n]+=low[cnt];
                add_edg(i,T+n,R-L,cnt);
                add_edg(T+n,i,0,cnt);
            }
        }
        fo(i,0,n+m+1){
            if(cha[i]<0){
                add_edg(i,tt,-cha[i],0);
                add_edg(tt,i,0,0);
            }
            else {
                sch+=cha[i];
                add_edg(ss,i,cha[i],0);
                add_edg(i,ss,0,0);
            }
        }
        cnt++;low[cnt]=0;hei[cnt]=inf;
        add_edg(t,s,inf,cnt);
        add_edg(s,t,0,cnt);
        int tms=s,tmt=t;s=ss;t=tt;
        if(dinic()==sch){
            s=tms;t=tmt;
            ans=e[rp].val;
            // cout<<ans<<endl;
            e[rp].val=e[rp-1].val=0;
            for(int i=head[ss];i;i=e[i].nxt)e[i].val=e[i^1].val=0;
            for(int i=head[tt];i;i=e[i].nxt)e[i].val=e[i^1].val=0;
            ans+=dinic();
            printf("%lld\n",ans);
        }
        else printf("-1\n");    
        printf("\n");
    }
}

Luogu4843 清理雪道

直接每条边的下界是\(1\),上界无穷,这样一定有解,直接跑上下界最小流就好了

code
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define fo(i,x,y) for(int i=(x);i<=(y);i++)
#define fu(i,x,y) for(int i=(x);i>=(y);i--)
int read(){
    int s=0,t=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')t=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
    return s*t;
}
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=105;
const int M=N*N;
int n,m[N],s,t,ss=102,tt=103,ans;
struct E{int to,nxt,val;}e[M*2];
int head[N],hea[N],rp=1;
void add_edg(int x,int y,int z){
    e[++rp].to=y;e[rp].val=z;
    e[rp].nxt=head[x];head[x]=rp;
}
int dis[N];
bool bfs(){
    memset(dis,0x3f,sizeof(dis));
    memcpy(head,hea,sizeof(hea));
    queue<int> q;while(!q.empty())q.pop();
    q.push(s);dis[s]=0;
    while(!q.empty()){
        int x=q.front();q.pop();
        for(int i=head[x];i;i=e[i].nxt){
            int y=e[i].to;
            if(!e[i].val||dis[y]<=dis[x]+1)continue;
            dis[y]=dis[x]+1;q.push(y);
            if(y==t)return true;
        }
    }
    return false;
}
int dfs(int x,int in){
    if(x==t)return in;
    int rest=in,go=0;
    for(int i=head[x];i;head[x]=i=e[i].nxt){
        int y=e[i].to;
        if(!e[i].val||dis[y]!=dis[x]+1)continue;
        go=dfs(y,min(rest,e[i].val));
        if(go)e[i].val-=go,e[i^1].val+=go,rest-=go;
        else dis[y]=0;
        if(!rest)break;
    }
    return in-rest;
}
int dinic(){
    int ret=0;memcpy(hea,head,sizeof(head));
    while(bfs())ret+=dfs(s,inf);
    return ret;
}
int cnt,low[M],hei[M],cha[N],sch;
signed main(){
    n=read();s=0;t=n+1;
    fo(i,1,n){
        m[i]=read();
        cnt++;low[cnt]=0;hei[cnt]=inf;
        add_edg(s,i,inf);add_edg(i,s,0);
        fo(j,1,m[i]){
            int x=read();
            cnt++;low[cnt]=1;hei[cnt]=inf;
            cha[i]-=1;cha[x]+=1;
            add_edg(i,x,inf-1);add_edg(x,i,0);
        }
        cnt++;low[cnt]=0;hei[cnt]=inf;
        add_edg(i,t,inf);add_edg(t,i,0);
    }
    fo(i,0,n+1){
        if(cha[i]<0){
            add_edg(i,tt,-cha[i]);
            add_edg(tt,i,0);
        }
        else {
            sch+=cha[i];
            add_edg(ss,i,cha[i]);
            add_edg(i,ss,0);
        }
    }
    cnt++;low[cnt]=0;hei[cnt]=inf;
    add_edg(t,s,inf);add_edg(s,t,0);
    int tms=s,tmt=t;s=ss;t=tt;
    dinic();s=tmt;t=tms;
    ans=e[rp].val;
    // cout<<ans<<endl;
    e[rp].val=e[rp-1].val=0;
    ans-=dinic();
    printf("%lld",ans);
}

有源汇上下界最小费用可行流

这个和上面的可行流大同小异,直接把从虚拟源汇上跑的最大流换成最小费用最大流就行了

原图上的边,该是啥费用就是啥费用,这个不用说

新加的从虚拟源点出发或者进入虚拟汇点的边费用都是\(0\),原图的源汇点之间的连边费用也是\(0\)

最终的最小费用就是这个最小费用流跑出来的费用加上满足下界的流量的费用

但是不需要像上下界最大最小流那样去再跑一遍最大流了

也就是说,整个最小费用可行流,只需要跑一遍最小费用最大流就完事了

为什么?为什么最小流还可以退流但是最小费用不可以继续退费呢??

首先我们知道,流量是相对于源汇点来说的,那么我们无法通过经过了多少条边来计算流量

但是费用可以,我经过了那条边多少次就是多少费用,这个是不可以变的

所以我在跑最小费用最大流的时候,已经保证了是最小费用,而跑虚拟源汇的最大流的时候却不能保证原图源汇的最小流

然后我闲的没事想了一中午,发现,有一种情况是需要再跑一遍最大费用流的

如果从虚拟源点向虚拟汇点跑的那个最小费用流换成普通的最大流,那么我们需要从汇点向源点再跑一次最小费用流

但是如果这样做的话,我们要在费用变成正值的时候及时停止,并且还有不合法的可能

所以只跑一遍最小费用流是最好的选择

说了这么多,证明只需要跑一遍最小费用流是对的

Luogu4043 支线剧情

好像是个板子,确实是个板子,建立一个汇点即可,把所有剧情向汇点链接一个费用为\(0\)的边

code
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define fo(i,x,y) for(int i=(x);i<=(y);i++)
#define fu(i,x,y) for(int i=(x);i>=(y);i--)
int read(){
    int s=0,t=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')t=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
    return s*t;
}
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=305;
const int M=6e3+5;
int n,k[N],s,t,ss=302,tt=303,ans;
struct E{int to,nxt,val,cot;}e[M*2];
int head[N],rp=1;
void add_edg(int x,int y,int z,int w){
    e[++rp].to=y;e[rp].val=z;e[rp].cot=w;
    e[rp].nxt=head[x];head[x]=rp;
}
int dis[N];bool vis[N];
int pre[N],epr[N],flo[N];
bool spfa(){
    memset(dis,0x3f,sizeof(dis));
    queue<int> q;while(!q.empty())q.pop();
    q.push(s);dis[s]=0;vis[s]=true;flo[s]=inf;
    while(!q.empty()){
        int x=q.front();q.pop();vis[x]=false;
        for(int i=head[x];i;i=e[i].nxt){
            int y=e[i].to;
            if(!e[i].val||dis[y]<=dis[x]+e[i].cot)continue;
            dis[y]=dis[x]+e[i].cot;
            flo[y]=min(flo[x],e[i].val);
            pre[y]=x;epr[y]=i;
            if(!vis[y])q.push(y),vis[y]=true;
        }
    }
    return dis[t]!=inf;
}
int EK(){
    int flw=0,cst=0;
    while(spfa()){
        int now=t;
        while(now!=s){
            e[epr[now]].val-=flo[t];
            e[epr[now]^1].val+=flo[t];
            now=pre[now];
        }
        flw+=flo[t];cst+=dis[t]*flo[t];
    }
    return cst;
}
int cnt,low[M],hei[M],cha[N],sch;
signed main(){
    n=read();s=1;t=n+1;
    fo(i,1,n){
        ++cnt;low[cnt]=0;hei[cnt]=inf;
        add_edg(i,t,inf,0);
        add_edg(t,i,0,0);
        int k=read();
        fo(j,1,k){
            int b=read(),ti=read();
            ++cnt;low[cnt]=1;hei[cnt]=inf;
            cha[i]--;cha[b]++;ans+=ti;
            add_edg(i,b,inf,ti);
            add_edg(b,i,0,-ti);
        }
    }
    fo(i,1,n){
        if(cha[i]<0){
            add_edg(i,tt,-cha[i],0);
            add_edg(tt,i,0,0);
        }
        else {
            add_edg(ss,i,cha[i],0);
            add_edg(i,ss,0,0);
        }
    }
    add_edg(t,s,inf,0);
    add_edg(s,t,0,0);
    s=ss;t=tt;
    ans+=EK();
    printf("%lld",ans);
}

例题

Luogu3980 NOI2008 志愿者招募

这......我何德何能可以做出啊??所以我去看题解了

发现无法处理一个志愿者可以干好多天这个问题......

所以题解就按照时间建图了

我们发现无法维护当前有多少个志愿者在工作,那就维护有多少个志愿者没有工作

第零天有\(inf\)个志愿者没有工作,走到第一天的时候,仍然有\(inf\)个没有工作

然而第一天向第二天走的时候,只能剩下\(inf-a_1\)个志愿者没有工作,因为第一天需要\(a_1\)个人

于是这个时候就要雇人了,每个志愿者\(s\)连向\(t+1\),连一个费用为\(c\),流量为\(inf\)的边

每一天向下一天连费用为\(0\),流量为\(inf-a_i\)的边

我们只需要保证在\(n+1\)天之后,仍然有\(inf\)个人没有工作就好了

也就是把所有的边都按照时间分成一段一段的,那么我们保证当前时间所有边的流量加起来是\(inf\)就行了

code
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define fo(i,x,y) for(int i=(x);i<=(y);i++)
#define fu(i,x,y) for(int i=(x);i>=(y);i--)
int read(){
    int s=0,t=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')t=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
    return s*t;
}
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=1005;
const int M=11005;
int n,m,s=1003,t=1002;
struct E{int to,nxt,val,cot;}e[M*2];
int head[N],rp=1;
void add_edg(int x,int y,int z,int w){
    e[++rp].to=y;e[rp].val=z;e[rp].cot=w;
    e[rp].nxt=head[x];head[x]=rp;
}
int dis[N];bool vis[N];
int pre[N],epr[N],flo[N];
bool spfa(){
    memset(dis,0x3f,sizeof(dis));
    queue<int> q;while(!q.empty())q.pop();
    q.push(s);dis[s]=0;vis[s]=true;flo[s]=inf;
    while(!q.empty()){
        int x=q.front();q.pop();vis[x]=false;
        for(int i=head[x];i;i=e[i].nxt){
            int y=e[i].to;
            if(!e[i].val||dis[y]<=dis[x]+e[i].cot)continue;
            dis[y]=dis[x]+e[i].cot;
            flo[y]=min(flo[x],e[i].val);
            pre[y]=x;epr[y]=i;
            if(!vis[y])q.push(y),vis[y]=true;
        }
    }
    return dis[t]!=inf;
}
int EK(){
    int flw=0,cst=0;
    while(spfa()){
        int now=t;
        while(now!=s){
            e[epr[now]].val-=flo[t];
            e[epr[now]^1].val+=flo[t];
            now=pre[now];
        }
        flw+=flo[t];cst+=flo[t]*dis[t];
    }
    return cst;
}
signed main(){
    n=read();m=read();
    add_edg(s,1,inf,0);add_edg(1,s,0,0);
    add_edg(n+1,t,inf,0);add_edg(t,n+1,0,0);
    fo(i,1,n){
        int x=read();
        add_edg(i,i+1,inf-x,0);
        add_edg(i+1,i,0,0);
    }
    fo(i,1,m){
        int a=read(),b=read(),c=read();
        add_edg(a,b+1,inf,c);
        add_edg(b+1,a,0,-c);
    }
    printf("%lld",EK());
}

Luogu4194 矩阵

第一道利用上下界可行流判断是否可行的题!!!

既然\(A\)矩阵是已知的,那么我们就把重点放在\(B\)矩阵上

题意就是说让两个矩阵的差的行加和与列加和的\(max\)最小

我们就二分这个最小值,于是我们思考如何\(check\)

现在已知\(B\)矩阵的行列加和的范围,和每一个数的范围,于是我们就直接可行流

源点连所有行,上下界是行的范围

汇点连所有列,上下界是列的范围

中间行列交点连点权,范围就是\([L,R]\)

再把源汇连起来,因为矩阵的总和是要相等的!!

code
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define fo(i,x,y) for(int i=(x);i<=(y);i++)
#define fu(i,x,y) for(int i=(x);i>=(y);i--)
int read(){
    int s=0,t=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')t=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
    return s*t;
}
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=405;
const int M=40805;
int n,m,s,t,ss=402,tt=403;
struct E{int to,nxt,val;}e[M*2];
int head[N],hea[N],rp=1;
void add_edg(int x,int y,int z){
    e[++rp].to=y;e[rp].val=z;
    e[rp].nxt=head[x];head[x]=rp;
}
int dis[N];
bool bfs(){
    memset(dis,0x3f,sizeof(dis));
    memcpy(head,hea,sizeof(hea));
    queue<int> q;while(!q.empty())q.pop();
    q.push(s);dis[s]=0;
    while(!q.empty()){
        int x=q.front();q.pop();
        for(int i=head[x];i;i=e[i].nxt){
            int y=e[i].to;
            if(!e[i].val||dis[y]<=dis[x]+1)continue;
            dis[y]=dis[x]+1;q.push(y);
            if(y==t)return true;
        }
    }
    return false;
}
int dfs(int x,int in){
    if(x==t)return in;
    int rest=in,go=0;
    for(int i=head[x];i;head[x]=i=e[i].nxt){
        int y=e[i].to;
        if(!e[i].val||dis[y]!=dis[x]+1)continue;
        go=dfs(y,min(rest,e[i].val));
        if(go)e[i].val-=go,e[i^1].val+=go,rest-=go;
        else dis[y]=0;
        if(!rest)break;
    }
    return in-rest;
}
int dinic(){
    int ret=0;memcpy(hea,head,sizeof(head));
    while(bfs())ret+=dfs(s,inf);
    return ret;
}
int a[N][N],sh[N],sz[N],L,R;
int cha[N],sch;
bool ck(int mid){
    rp=1;memset(head,0,sizeof(head));
    sch=0;memset(cha,0,sizeof(cha));
    s=0;t=n+m+1;
    fo(i,1,n){
        cha[s]-=sh[i]-mid;
        cha[i]+=sh[i]-mid;
        add_edg(s,i,2*mid);
        add_edg(i,s,0);
    }
    fo(i,1,m){
        cha[i+n]-=sz[i]-mid;
        cha[t]+=sz[i]-mid;
        add_edg(i+n,t,2*mid);
        add_edg(t,i+n,0);
    }
    fo(i,1,n)fo(j,1,m){
        cha[i]-=L;cha[j+n]+=L;
        add_edg(i,j+n,R-L);
        add_edg(j+n,i,0);
    }
    fo(i,0,n+m+1){
        if(cha[i]<0){
            add_edg(i,tt,-cha[i]);
            add_edg(tt,i,0);
        }
        else {
            sch+=cha[i];
            add_edg(ss,i,cha[i]);
            add_edg(i,ss,0);
        }
    }
    add_edg(t,s,inf);add_edg(s,t,0);
    int tms=s,tmt=t;s=ss;t=tt;
    return dinic()==sch;
}
signed main(){
    n=read();m=read();
    fo(i,1,n)fo(j,1,m)a[i][j]=read(),sh[i]+=a[i][j],sz[j]+=a[i][j];
    L=read();R=read();
    int l=0,r=200000,mid;
    while(l<r){
        mid=l+r>>1;
        if(ck(mid))r=mid;
        else l=mid+1;
    }
    printf("%lld",l);
}

TIPS

一些小小的\(tips\):

1、\(rp=1\)

2、最大流中\(bfs\)\(dis[y]<=dis[x]+1\)而不是\(!=\)

3、最大流千万不要忘记在\(dinic\)之前把\(head\)赋值给\(hea\),小心出来全是\(0\)

4、上下界网络流,注意原图中的源汇点和虚拟的源汇点别弄混了,注意两次\(dinic\)别忘了把源汇点换回来

5、上下界网络流,注意找最大流最小流之前别忘记删掉链接原图源汇点的那条边!!!

在这里说一个非常重要的东西,网络流中是可以出现环的!!

对于普通的最大流最小割而言,环的出现不会导致答案的影响,因为总是走最短的那个路,所以环是永远不会被经过的

建图模型

最大流

这个东西的模型比较单一,一般是几个东西有竞争关系,然后分成几列,跑就完事了

最小割

这个玩意博大精深,各种代价的题用它都能搞定

但是这样的代价题都有一个共性,都有冲突的利益,比如说要这个就不能要那个

我们就根据这个冲突建图,有冲突的就连边

那么权值也是很重要的一部分,可以列等式或者想实际含义之类的

还有一些特殊的题目比如最大权闭合子图

费用流

当然加上了费用也和流量离不开关系,我们仍然考虑两个东西流量的关系

只不过这个时候,我们更加关心的不是流量的多少而是费用的大小

这玩意变化多端,我还没学明白

但是要及时的找到那个是流量,哪个是费用,根据这个建图,简单许多

目前做过的题中大部分是对二元关系定义费用,然后求最小费用

上下界

????一样的,之不过多了个下界而已,换个跑法就好了

可行流可能应用更加广泛一些,可行流可以判断一种情况是否成立,于是可以作为判断的根据

posted @ 2021-12-20 09:44  fengwu2005  阅读(69)  评论(0)    收藏  举报