网络流、费用流整理

网络流最大流问题的难点还是在于对模型的转化:分析问题构造网络,然后用模板处理。

对于网络流,直观的理解就是在有向图中,每个边都有流量限制。求网络可允许的最大流量,就是最大流问题。求解最大流使用Dinic算法,网络构建好之后,每次bfs在残差网络上检查是否有残余的流可以到达终点,如果有增广路,再通过dfs搜索并回溯,从而得到这一次增广路的结果。当不存在增广路时,算法也就结束

因为对残差网络的理解不是特别深刻,所以当有要求记录匹配结果或二分判断是否可行的情况时,不要在残差网络上修改,尝试对原图进行变动再重新求网络流。二分判断:POJ2112;记录结果:POJ1486

图的割:要将图中节点分为不相连的两部分,所需要打断的边的容量之和。最大量最小割定理:最大流等于最小割。POJ3469,最小割的典型例题

网络流也可以用于处理二分图的相关问题。|最大流|=|二分图最大匹配|=|最小顶点覆盖|。而在二分图中,|最大匹配|+|最小边覆盖|=|最小顶点覆盖|+|最大独立集|=|V|。对于二分图问题,将其构建为合适的网络即可处理问题。二分图中表示有联系的边转化为容量为1的有向边,同时从原点到每个u连一条容量为1的有向边,从每个v连一条容量为1的有向边到汇点。POJ1274(普通二分匹配)、POJ1466(最大独立集)、POJ3692(最大独立集)、POJ2724(最小边覆盖)、POJ2226(最小顶点覆盖)。最小边覆盖和最小顶点覆盖可能会比较容易混,要分清概念

最小点覆盖:实质是个点集,点集里面的点能覆盖所有的边,最小点覆盖就是满足这个要求的点集中点数最小的那个

最小边覆盖:实质是个边集,这个集合里的边能覆盖所有的点,最小边覆盖是满足这个要求的所有边集中边数最少的一个

独立集:图 G 中两两互不相邻的顶点构成的集合。

关于网络流中一些常见问题的处理技巧:

1.要求每个点只能被访问k次。将每个点拆为入点和出点,入点和出点之间连容量为k的边。例题:POJ3281

2.一组问题,要分到两个集合中分别解决,在两个集合之间进行迁移会产生费用,要求最后得到最小费用。这类问题是最小割问题,将原来点的集合用最小代价分割成两个集合,就是在求最大流,POJ3469

#include<queue>
#include<stdio.h>
#include<math.h>
using namespace std;
const int MAXV=1000000,MAXE=1000000;
int n,m,cnt,S,T,etot,head[MAXV],lev[MAXV];
struct Edge{
    int next,to,c;
}edge[MAXE];
queue<int> que;

void addedge(int x,int y,int c){
    edge[etot].next=head[x];
    edge[etot].to=y;
    edge[etot].c=c;
    head[x]=etot++;
}
void Addedge(int x,int y,int c){
    addedge(x,y,c);
    addedge(y,x,0);
}
bool bfs(){
    for(int i=0;i<=cnt;i++) lev[i]=0;
    while(!que.empty()) que.pop();
    lev[S]=1;
    que.push(S);
    while(!que.empty()){
        int u=que.front();
        que.pop();
        for(int i=head[u];i!=-1;i=edge[i].next){
            int v=edge[i].to;
            if(lev[v]==0&&edge[i].c>0){
                lev[v]=lev[u]+1;
                que.push(v);
                if(v==T) return true;
            }
        }
    }
    return false;
}
int dfs(int u,int f){
    if(u==T) return f;
    int tag=0;
    for(int i=head[u];i!=-1;i=edge[i].next){
        int v=edge[i].to;
        if(lev[v]==lev[u]+1&&edge[i].c>0){
            int tmp=dfs(v,min(f-tag,edge[i].c));
            edge[i].c-=tmp;
            edge[i^1].c+=tmp;
            tag+=tmp;
            if(tag==f) return tag;
        }
    }
    return tag;
}
int main(){
    scanf("%d%d",&n,&m);
    S=n+1;T=n+2;cnt=n+3;
    for(int i=0;i<=cnt;i++) head[i]=-1;
    for(int i=1;i<=n;i++){
        int a,b;
        scanf("%d%d",&a,&b);
        Addedge(S,i,a);
        Addedge(i,T,b);
    }
    for(int i=1;i<=m;i++){
        int a,b,w;
        scanf("%d%d%d",&a,&b,&w);
        Addedge(a,b,w);
        Addedge(b,a,w);
    }
    int ans=0;
    while(bfs()){
        ans+=dfs(S,0x7fffffff);
    }
    printf("%d\n",ans);
    return 0;
}

3.多个源点和终点。新加终极源点和终极终点来管理源点和终点。终极源点终极终点的边没有流量限制。

4.无向图。Addedge两次,正反边的容量都是c。注意分析题意,在无向图中求得的结果可能会除2,例题POJ1466

5.求最大闭合子图权值、最大密度子图

闭合图:在一个图中,我们选取一些点构成集合,记为V,且集合中的出边(即集合中的点的向外连出的弧),所指向的终点(弧头)也在V中,则我们称V为闭合图。

最大权闭合图:在所有闭合图中,集合中点的权值之和最大的V,我们称V为最大权闭合图。

这两种类型,就是论文题,参考胡伯涛:《最小割模型在信息学竞赛中的应用》。对于最大闭合子图权值的问题,设立一个超级源点和一个超级汇点,从超级源点向所有权值为正的点连一条单向边,边的权值为点的权值;从所有权值为负的点向超级汇点连一条单向边,边的权值为点的权值的绝对值;然后原图中所有的边都设为无穷大。求出此题的最小割,用所有权值为正的点之和减掉最小割即为最大闭合图权值。POJ2981

#include<stdio.h>
#include<queue>
using namespace std;
const int MAXE=1000000,MAXV=1000000;
const long long inf=0x3f3f3f3f3f3f3f3f;
int n,m,etot,S,T,cnt,head[MAXV],lev[MAXV],num;
long long sum,ans;
bool vis[MAXV];
struct e{
    int next,to;
    long long c;
}edge[MAXE];
queue<int> q;

void addedge(int a,int b,long long c){
    edge[etot].next=head[a];
    edge[etot].to=b;
    edge[etot].c=c;
    head[a]=etot++;
}
void Addedge(int a,int b,long long c){
    addedge(a,b,c);
    addedge(b,a,0);
}
bool bfs(){
    for(int i=0;i<=cnt;i++) lev[i]=0;
    while(!q.empty()) q.pop();

    lev[S]=1;
    q.push(S);
    while(!q.empty()){
        int u=q.front();
        q.pop();
        for(int i=head[u];i!=-1;i=edge[i].next){
            int v=edge[i].to;
            if(lev[v]==0&&edge[i].c>0){
                lev[v]=lev[u]+1;
                q.push(v);
                if(v==T) return true;
            }
        }
    }
    return false;
}
long long dfs(int u,long long f){
    if(u==T) return f;
    long long tag=0;
    for(int i=head[u];i!=-1;i=edge[i].next){
        int v=edge[i].to;
        if(lev[v]==lev[u]+1&&edge[i].c>0){
            long long tmp=dfs(v,min(f-tag,edge[i].c));
            edge[i].c-=tmp;
            edge[i^1].c+=tmp;
            tag+=tmp;
            if(tag==f) return tag;
        }
    }
    return tag;
}
void dfs(int u){
    num++;
    vis[u]=true;
    for(int i=head[u];i!=-1;i=edge[i].next){
        if(edge[i].c>0&&!vis[edge[i].to]) dfs(edge[i].to);
    }
}
int main(){
    scanf("%d%d",&n,&m);
    S=n+1;T=S+1;cnt=T+1;
    for(int i=0;i<=cnt;i++) head[i]=-1;
    for(int i=1;i<=n;i++){
        long long t;
        scanf("%lld",&t);
        if(t>0){
            Addedge(S,i,t);
            sum+=t;
        }
        else if(t<0) Addedge(i,T,-t);
    }
    while(m--){
        int a,b;
        scanf("%d%d",&a,&b);
        Addedge(a,b,inf);
    }

    while(bfs()){
        ans+=dfs(S,inf);
    }
    dfs(S);
    printf("%d %lld\n",num-1,sum-ans);
    return 0;
}

最大密度子图,使选定的子图中边数/点数最大。将原图中每条无向边改为两条容量为1的有向边;新建源点到原图每个点v构建有向边(S,v) , 容量为U(一个为了使流量不为负而统一添加的大数);新建汇点,从原图中每个点到汇点构建有向边(v,T),容量为(U+2*g-dv) ,其中dv为点v的度,g为二分猜测的密度。h(g)=(U*n-maxflow)/2为当前密度下的最大流...POJ3155。。。详情见论文

 


接下来是网络流的拓展,费用流。在网络流模型的基础上,每条边都增加了“费用”的属性,在有最大流的同时费用最小(或最大)。在求解最大流的思路上拓展,每当求增广路时,确定的增广路应当是目前增广路中费用最小的一条,dfs在这条增广路上更新残差网络,累计流量费用。若能继续拓展,则沿新路增广。

注意:概念拓展之后,每条边的费用都应当是固定的,残差网络中逆向边的费用应当是正向边费用的相反值,所以找最短路不能用dij算法而是spfa算法。

注意:无论是最大费用还是最小费用,都是在最大流情况下的结果。至于求最大费用或是最小费用,可以修改spfa

费用流的问题类型比较固定,基本的最大流模型,POJ2195

#include<stdio.h>
#include<queue>
#include<string.h>
#include<algorithm>
using namespace std;
const int MAXV=1000000,MAXE=1000000,inf=0x3f3f3f3f;
int n,m,etot,cnt,mcnt,hcnt,S,T,dis[105][105],head[MAXV],pre[MAXV],len[MAXV],go[4][2]={1,0,0,-1,-1,0,0,1};
char map[105][105];
bool mark[105][105],vis[MAXV];
struct point{
    int x,y;
}man[105],house[105];
struct e{
    int next,to,c,w;
}edge[MAXE];
queue<point> que;
queue<int> q;

void bfs(int k){
    int x=man[k].x,y=man[k].y;
    while(!que.empty()) que.pop();
    memset(mark,false,sizeof(mark));
    memset(dis,0x3f,sizeof(dis));
    que.push((point){x,y});
    dis[x][y]=0;

    while(!que.empty()){
        point now=que.front();
        que.pop();
        for(int i=0;i<4;i++){
            int nx=now.x+go[i][0],ny=now.y+go[i][1];
            if(nx<0||nx>=n||ny<0||ny>=m||mark[nx][ny]) continue;
            dis[nx][ny]=dis[now.x][now.y]+1;
            mark[nx][ny]=true;
            que.push((point){nx,ny});
        }
    }
}
void addedge(int a,int b,int c,int w){
    edge[etot].next=head[a];
    edge[etot].to=b;
    edge[etot].c=c;
    edge[etot].w=w;
    head[a]=etot++;
}
void Addedge(int a,int b,int c,int w){
    addedge(a,b,c,w);
    addedge(b,a,0,-w);
}
bool spfa(){
    for(int i=0;i<=cnt;i++){
        vis[i]=false;len[i]=inf;
    }
    while(!q.empty()) q.pop();

    q.push(S);
    len[S]=0;
    vis[S]=true;
    while(!q.empty()){
        int u=q.front();
        q.pop();
        vis[u]=false;
        for(int i=head[u];i!=-1;i=edge[i].next){
            int v=edge[i].to;
            if(len[v]>len[u]+edge[i].w&&edge[i].c){
                len[v]=len[u]+edge[i].w;
                pre[v]=i;
                if(!vis[v]){
                    vis[v]=true;
                    q.push(v);
                }
            }
        }
    }
    return len[T]!=inf;
}
int dfs(int &flow){
    int p,cost=inf,ans=0;
    for(int i=T;i!=S;i=edge[p^1].to){
        p=pre[i];
        cost=min(cost,edge[p].c);
    }
    for(int i=T;i!=S;i=edge[p^1].to){
        p=pre[i];
        edge[p].c-=cost;
        edge[p^1].c+=cost;
        ans+=cost*edge[p].w;
    }
    flow+=cost;
    return ans;
}
int main(){
    while(scanf("%d%d",&n,&m)!=EOF){
        if(n==0&&m==0) break;
        mcnt=hcnt=etot=0;
        for(int i=0;i<n;i++){
            scanf("%s",map[i]);
        }
        for(int i=0;i<n;i++){
            for(int j=0;j<m;j++){
                if(map[i][j]=='m') man[mcnt++]=(point){i,j};
                if(map[i][j]=='H') house[hcnt++]=(point){i,j};
            }
        }

        S=mcnt+hcnt;T=S+1;cnt=T+1;
        //printf("%d %d %d %d %d\n",mcnt,hcnt,S,T,cnt);
        for(int i=0;i<=cnt;i++) head[i]=-1;
        for(int i=0;i<mcnt;i++) Addedge(S,i,1,0);
        for(int i=0;i<hcnt;i++) Addedge(i+mcnt,T,1,0);
        for(int i=0;i<mcnt;i++){
            bfs(i);
            for(int j=0;j<hcnt;j++){
                Addedge(i,j+mcnt,1,dis[house[j].x][house[j].y]);
            }
        }

        int ans=0,flow=0;
        while(spfa()){
            ans+=dfs(flow);
            //printf("%d %d\n",flow,ans);
        }
        printf("%d\n",ans);
    }
}

在矩阵图上来回往返一次,不走重复边/重复点,求最小代价。若只去不回,就是普通最短路问题。但是来回+不重复,转化为求两点之间两条没有公共边/公共点的问题,使用拆点技巧避免重复,费用流求解最小费用,POJ2135;若只有两条DP也可以做,多条就只能网络流/费用流POJ3422

其他练习题:POJ3068POJ2175POJ3688POJ3680

posted @ 2020-10-09 16:17  太山多桢  阅读(254)  评论(0编辑  收藏  举报