网络流

网络流

​ ————重在建模

网络,有向图,源点s,汇点t

最大流

Edmonds−karp(EK)增广路算法

\(O(nm^2)\) $ 10^3 — 10^4 $

不断用BFS寻找增广路并不断更新最大流量值,直到网络上不存在增广路为止

在BFS寻找一条增广路时,我们只需要考虑剩余流量不为0的边,然后找到一条从S到T的路径,同时计算出路径上各边剩余容量值的最小值dis,则网络的最大流量就可以增加dis(经过的正向边容量值全部减去dis,反向边全部加上dis)

注意tot=1;

tot是从2开始,因为1^1=0,而没有0这条边,2的反边是3,4的反边是5

建边的时候见单向边,然后再建一条 w=0 的反边

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
typedef long long LL;
const int N=510005;
int n,m,s,t;
int tot=1,hd[N];

struct edge{
    int to,nxt;
    LL w;
}e[N];
inline void add(int x,int y,LL z) {
    e[++tot].to=y;e[tot].w=z;e[tot].nxt=hd[x];hd[x]=tot;
}

bool vis[N];
int pre[N];
LL dis[N],ans;
inline bool bfs() {
    for(int i=1;i<=n;i++) vis[i]=0;
    queue<int>q;
    q.push(s);
    vis[s]=1;
    dis[s]=99999999999;
    while(!q.empty()) {
        int x=q.front();q.pop();
        for(int i=hd[x];i;i=e[i].nxt) {
            int y=e[i].to;
            if(vis[y]) continue;
            if(e[i].w<=0) continue;//!!!
            dis[y]=min(dis[x],e[i].w);
            pre[y]=i;
            q.push(y);
            vis[y]=1;
            if(y==t) return 1;
        }
    }
    return 0;
}

inline void update() {
    int x=t;
    ans+=dis[t];
    while(x!=s) {
        int v=pre[x];
        e[v].w-=dis[t];
        e[v^1].w+=dis[t];
        x=e[v^1].to;
    }
}
int flag[2505][2505];
int main() {
    scanf("%d%d%d%d",&n,&m,&s,&t);
    int x,y;LL z;
    for(int i=1;i<=m;i++) {
        scanf("%d%d%lld",&x,&y,&z);
        if(!flag[x][y]) {
            add(x,y,z);
            add(y,x,0);   
            flag[x][y]=tot;         
        }   
        else e[flag[x][y]-1].w+=z;
    }
    while(bfs()) update();
    printf("%lld\n",ans);
    return 0;
}


Dinic

\(O(n^2m)\) —— $ 10^4 — 10^5 $

食用这篇blog更易理解

基于对EK的优化,EK每次bfs只能找到一条增广路,Dinic就通过dfs优化它(见下面)

BFS 出图的层次,用 d[] 数组表示它的层次,即S到x最少需要经过的边数

在DFS中,从S开始,每次我们向下一层次随便找一个点,直到到达T,然后再一层一层回溯回去,继续找这一层的另外的点再往下搜索,这样就满足了我们同时求出多条增广路的需求

当前弧优化

其实就是走过的边dfs返回后不再走。。。

具体来说,就是 开一个now[] ,一开始now[]=head[],在dfs中,把now[x]赋值为 i ,也就是下一条边,这样回溯回来的时候会直接走下一条边,而不是重复搜之前搜的

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
typedef long long LL;
const int N=510005;
const int inf=0x3f3f3f3f;
int n,m,s,t;
int hd[N],nxt[N],to[N],tot=1;
LL ans,w[N];
inline void add(int x,int y,LL z) {
    to[++tot]=y;w[tot]=z;nxt[tot]=hd[x];hd[x]=tot;
}

LL c[N];
int now[N];
inline bool bfs() {
    for(int i=1;i<=n;i++) c[i]=inf;
    queue<int>q;
    q.push(s);
    c[s]=0;
    now[s]=hd[s];
    while(!q.empty()) {
        int x=q.front();q.pop();
        for(int i=hd[x];i;i=nxt[i]) {
            int y=to[i];
            if(c[y]<inf||w[i]<=0) continue;
            c[y]=c[x]+1;
            q.push(y);
            now[y]=hd[y];
            if(y==t) return 1;
        }        
    }
    return 0;
}

inline LL dfs(int x,LL rest) {
    if(x==t) return rest;
    LL sum=0,k;
    for(int i=now[x];i&&rest;i=nxt[i]) {//rest需>0
        now[x]=i;//当前弧优化
        int y=to[i];
        if(w[i]<=0||c[y]!=c[x]+1) continue;//
        k=dfs(y,min(rest,w[i]));
        if(!k) c[y]=inf;
        w[i]-=k;w[i^1]+=k;
        sum+=k;rest-=k;
    }
    return sum; 
}

int main() {
    scanf("%d%d%d%d",&n,&m,&s,&t);
    LL z;
    for(int i=1,x,y;i<=m;i++) {
        scanf("%d%d%lld",&x,&y,&z);
        add(x,y,z);
        add(y,x,0);
    }
    while(bfs()) ans+=dfs(s,inf);
    printf("%lld\n",ans);
    return 0;
}

更牛B的算法

题:

模板

模板题1

模板题2

模板+输出流的路径

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int N=1e5+5; 
const int inf=0x3f3f3f3f;
inline int read() {
    char ch=getchar();
    while(!isalpha(ch)) ch=getchar();
    return ch=='Y';
}
int n,m;
bool mp[55][55];
int hd[N],from[N],to[N],nxt[N],w[N],tot=1;
inline void add(int x,int y,int z) {
    to[++tot]=y;from[tot]=x;w[tot]=z;nxt[tot]=hd[x];hd[x]=tot;
}
void add_edge(int x,int y,int z) {
    add(x,y,z),add(y,x,0);
}

int s,t,ans;
int c[N],now[N];
bool bfs() {
    for(int i=1;i<N;i++) c[i]=inf;
    queue<int>q;
    q.push(s);
    c[s]=0;
    now[s]=hd[s];
    while(!q.empty()) {
        int x=q.front();q.pop();
        for(int i=hd[x];i;i=nxt[i]) {
            int y=to[i];
            if(w[i]==0||c[y]<inf) continue;
            c[y]=c[x]+1;
            q.push(y);
            now[y]=hd[y];
            if(y==t) return 1;
        }
    }
    return 0;
}

int dfs(int x,int rest) {
    if(x==t) return rest;
    int sum=0,k;
    for(int i=now[x];i&&rest;i=nxt[i]) {
        now[x]=i;
        int y=to[i];
        if(w[i]==0||c[y]!=c[x]+1) continue;
        k=dfs(y,min(rest,w[i]));
        if(!k) c[y]=inf;
        w[i]-=k;w[i^1]+=k;
        sum+=k;rest-=k;
    }
    return sum;
}
int main(){
    scanf("%d%d",&m,&n);
    int x,y;
    while(1) {
        scanf("%d%d",&x,&y);
        if(x==-1&&y==-1) break;
        add_edge(x,y,1);
    }
    s=0,t=n+1;    
    for(int i=1;i<=m;i++)
        add_edge(s,i,1);
    for(int i=m+1;i<=n;i++)
        add_edge(i,t,1);
    while(bfs()) ans+=dfs(s,inf);
    printf("%d\n",ans);
    for(int i=2;i<=tot;i+=2) {
        if(to[i]==s||to[i^1]==s) continue;
        if(to[i]==t||to[i^1]==t) continue;
        if(w[i^1]) printf("%d %d\n",from[i],to[i]);
    }
	return 0;
}

最小割

网络图上每条边有边权,移除边权和最小的一组边使得源点和汇点不连通

最小割=最大流

Description

•有m个男生和n个女生,每个人有一个智商值,其中有一些男女生存在“交往过密”的现象(一个人有可能和多个异性“交往过密”)

•现在要从中选出若干人组成一个班,现在你是教育处主任,你不希望看到这个班中有“交往过密”现象,同时你非常关心升学率,希望最大化这个班的智商值之和,求这个最大值

•n,m≤10000,智商值非负

Solution

男女间连正无穷,s到男连男智商,女到t连女智商,跑最小割

技巧

边点转化——拆点/拆边

Poj 1966

最小费用最大流

•费用流:边上还有一些费用,要求在流量最大的前提下,让每条边的流量*费用之和最大/最小

bfs改成spfa,+1改成+val[i]

注意dfs 入时vis[x]=1;回溯时vis[x]=0

#include <iostream>
#include <cstdio>
#include <cctype>
#include <cstring>
#include <cmath>
#include <queue>
using namespace std;
inline int read() {
    int x=0,f=1;char ch=getchar();
    while(!isdigit(ch)) {if(ch=='-')f=-1;ch=getchar();}
    while(isdigit(ch)) {x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
int n,m,s,t,ans=0,cost=0;
const int N = 510005;
const int inf = 0x3f3f3f3f;
int hd[N],to[N],nxt[N],w[N],val[N],tot=1;
inline void add(int x,int y,int z,int f) {
    to[++tot]=y;w[tot]=z;val[tot]=f;nxt[tot]=hd[x];hd[x]=tot;
}

bool vis[N];
int now[N],c[N];
inline bool spfa() {
    for(int i=1;i<=N;i++) c[i]=inf;
    queue<int>q;
    c[s]=0;
    q.push(s);
    now[s]=hd[s];
    while(!q.empty()) {
        int x=q.front();q.pop();
        vis[x]=0;
        for(int i=hd[x];i;i=nxt[i]) {
            int y=to[i];
            if(w[i]<=0) continue;
            if(c[y]>c[x]+val[i]) {
                c[y]=c[x]+val[i];
                now[y]=hd[y];
                if(!vis[y]) vis[y]=1,q.push(y);
            }
        }        
    }
    return (c[t]!=inf);
}

inline int dfs(int x,int rest) {
    if(x==t) return rest;
    int sum=0,k;
    vis[x]=1;
    for(int i=now[x];i&&rest;i=nxt[i]) {
        now[x]=i;
        int y=to[i];
        if(c[y]!=c[x]+val[i] || w[i]<=0 || vis[y]) continue;
        k=dfs(y,min(rest,w[i]));
        if(!k) c[y]=inf;
        w[i]-=k;w[i^1]+=k;
        sum+=k; rest-=k;
        cost+=k*val[i];
    }
    vis[x]=0;//回溯
    return sum;
}
int main() {
    n=read();m=read();
    s=read();t=read();
    for(int i=1,x,y,z,f;i<=m;i++) 
        x=read(),y=read(),z=read(),f=read(),add(x,y,z,f),add(y,x,0,-f);

    while(spfa()) 
        ans+=dfs(s,inf);

    printf("%d %d\n",ans,cost);
    return 0;
}

problems

晨跑

题意就是最小费用最大流,边和点的限制都是只能用一次,边可以用流量限制,点呢?应用上面提到的有向图拆点技巧,拆成入点和出点,中间连一条限制为 1(费用为0) 的边,以边代点

#include <iostream>
#include <cstdio>
#include <cctype>
#include <cstring>
#include <queue>
using namespace std;
inline int read() {
    int x=0,f=1;char ch=getchar();
    while(!isdigit(ch)) {if(ch=='-')f=-1;ch=getchar();}
    while(isdigit(ch)) {x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
int n,m,s,t,ans=0,cost=0;
const int N = 510005;
const int inf = 0x3f3f3f3f;
int hd[N],to[N],nxt[N],w[N],val[N],tot=1;
inline void add(int x,int y,int z,int f) {
    to[++tot]=y;w[tot]=z;val[tot]=f;nxt[tot]=hd[x];hd[x]=tot;
}

bool vis[N];
int now[N],c[N];
inline bool spfa() {
    for(int i=1;i<=N;i++) c[i]=inf;
    queue<int>q;
    c[s]=0;
    q.push(s);
    now[s]=hd[s];
    while(!q.empty()) {
        int x=q.front();q.pop();
        vis[x]=0;
        for(int i=hd[x];i;i=nxt[i]) {
            int y=to[i];
            if(w[i]==0) continue;
            if(c[y]>c[x]+val[i]) {
                c[y]=c[x]+val[i];
                now[y]=hd[y];
                if(!vis[y]) vis[y]=1,q.push(y);
            }
        }        
    }
    return (c[t]!=inf);
}

inline int dfs(int x,int rest) {
    if(x==t) return rest;
    int sum=0,k;
    vis[x]=1;
    for(int i=now[x];i&&rest;i=nxt[i]) {
        now[x]=i;
        int y=to[i];
        if(c[y]!=c[x]+val[i] || w[i]==0 || vis[y]) continue;
        k=dfs(y,min(rest,w[i]));
        if(!k) c[y]=inf;
        w[i]-=k;w[i^1]+=k;
        sum+=k; rest-=k;
        cost+=k*val[i];
    }
    vis[x]=0;
    return sum;
}
int main() {
    n=read();m=read();
    s=1+n;t=n;
    for(int i=1,x,y,z;i<=m;i++) {
        x=read(),y=read(),z=read();
        add(x+n,y,1,z);//
        add(y,x+n,0,-z);//
    }
    for(int i=1;i<=n;i++) 
        add(i,i+n,1,0),add(i+n,i,0,0);//

    while(spfa()) 
        ans+=dfs(s,inf);

    printf("%d %d\n",ans,cost);
    return 0;
}

方格取数

方法 1,dp,我担心的,就是一个点会被两条路径重复经过,注意到如果这样的话那么两条 路径走到这个点所用步数相同,那么可以 $dp[i][j][k][l] $对这两条路径同时 dp,同时一步一步 走。

方法 2,经过一个点,要么取一次,要么不取,这“仅取一次”是个限制 注意到一条路径可以等效为“一条流量” 一个点有两种入边,一种是不取数,只经过它,那么费用为 0,不需要流量限制(因为可以 是两条路径的交点,不仅经过一次) 另一种是取数,但是这种途径只能使用一次,因此流量限制为 1,费用为数 ,为了使其仅有两条路径,我把源点到左上角的点的流量连成 2 可以发现,这个建图思路是对“走路径”模拟得到的。 按照这个思路,很多带有“限制”的东西都能被费用流所“模拟”

跳舞

一个显然的想法是,男女可以看做左右部点,再让他们分别建立一个辅助点表示我要跟不喜欢 的人去跳,这个辅助点要连流量为 k 的边,表示 k 的限制,对于不喜欢的男女,男辅助点要连女 辅助点,喜欢的男女,让男主点连女主点,其实类似二分图匹配 但是这样的思路是混乱的,我们要求最多能让所有人集体跳多少轮,如果跑最大流,就会让 一些人多跳,但是另一些人少跳 你发现了没有,对于这种“整体限制”的不好做的问题,很好的思路就是二分答案 :我二分能跳多少轮,然后用刚才建的网络去判断是不是能跳 mid 轮。 对于每个人,我不让他多跳(源、汇点分别向男女连流量限制为 mid 的边)也不能让她少跳 (让总流量最大,判断是不是 mid*n),这种二分+判断满流的方法也是常用的

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int N=1e5+5; 
const int inf=0x3f3f3f3f;
inline int read() {
    char ch=getchar();
    while(!isalpha(ch)) ch=getchar();
    return ch=='Y';
}
int n,k;
bool mp[55][55];
int hd[N],to[N],nxt[N],w[N],tot=1;
inline void add(int x,int y,int z) {
    to[++tot]=y;w[tot]=z;nxt[tot]=hd[x];hd[x]=tot;
}
void add_edge(int x,int y,int z) {
    add(x,y,z),add(y,x,0);
}
void clear() {
    tot=1;
    memset(hd,0,sizeof(hd));
}

int s,t;
int c[N],now[N];
bool bfs() {
    for(int i=1;i<N;i++) c[i]=inf;
    queue<int>q;
    q.push(s);
    c[s]=0;
    now[s]=hd[s];
    while(!q.empty()) {
        int x=q.front();q.pop();
        for(int i=hd[x];i;i=nxt[i]) {
            int y=to[i];
            if(w[i]==0||c[y]<inf) continue;
            c[y]=c[x]+1;
            q.push(y);
            now[y]=hd[y];
            if(y==t) return 1;
        }
    }
    return 0;
}

int dfs(int x,int rest) {
    if(x==t) return rest;
    int sum=0,k;
    for(int i=now[x];i&&rest;i=nxt[i]) {
        now[x]=i;
        int y=to[i];
        if(w[i]==0||c[y]!=c[x]+1) continue;
        k=dfs(y,min(rest,w[i]));
        if(!k) c[y]=inf;
        w[i]-=k;w[i^1]+=k;
        sum+=k;rest-=k;
    }
    return sum;
}

bool check(int x) {
    clear();
    int res=0;
    for(int i=1;i<=n;i++)   
        add_edge(s,i,x),add_edge(i+n,t,x),
        add_edge(i,i+n*2,k),add_edge(i+n*3,i+n,k);
    for(int i=1;i<=n;i++)   
        for(int j=1;j<=n;j++)
            if(mp[i][j]) add_edge(i,j+n,1);
            else add_edge(i+n*2,j+n*3,1);
    while(bfs()) res+=dfs(s,inf);
    return res==n*x;
}
int main(){
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++) 
        for(int j=1;j<=n;j++) 
            mp[i][j]=read();
    s=0,t=n*4+1;    
	int l=0,r=n,ans;
    while(l<=r) {
        int mid=l+r>>1;
        if(check(mid)) ans=mid,l=mid+1;
        else r=mid-1;
    }
    printf("%d\n",ans);
	return 0;
}

https://www.luogu.com.cn/blog/Multifuctional/fu-zai-ping-heng-wen-ti

posted @ 2020-08-17 20:58  ke_xin  阅读(41)  评论(0编辑  收藏  举报