<学习笔记> 网络流

最大流

code
int head[N],nex[N*N*8],ver[N*N*8],edge[N*N*8],tot=1;
void add(int x,int y,int v){
    ver[++tot]=y,nex[tot]=head[x],head[x]=tot,edge[tot]=v;
    ver[++tot]=x,nex[tot]=head[y],head[y]=tot,edge[tot]=0;
}
int st,ed;
queue<int> q;
int dep[N],cur[N];
int bfs(){
    memset(dep,0,sizeof(int)*(ed+1));
    dep[st]=1;
    q.push(st);
    while(!q.empty()){
        int x=q.front();
        q.pop();
        for(int i=head[x];i;i=nex[i]){
            int y=ver[i];
            if(dep[y] || edge[i]<=0) continue;
            dep[y]=dep[x]+1;
            q.push(y);
        }
    }
    if(dep[ed]) return 1;
    else return 0;
}
int dfs(int x,int flow){
    if(x==ed) return flow;
    for(int i=cur[x];i;i=nex[i]){
        cur[x]=i;
        int y=ver[i];
        if(dep[y]==dep[x]+1 && edge[i]>0){
            int d=dfs(y,min(flow,edge[i]));
            if(d>0){
                edge[i]-=d;
                edge[i^1]+=d;
                return d;
            }
        }
    }
    return 0;
}
int Dinic(){
    int ans=0;
    while(bfs()){
        for(int i=1;i<=ed;i++) cur[i]=head[i];
        while(int d=dfs(st,inf)){
            ans+=d;
        }
    }
    return ans;
}

最大流最小割定理

对于一个网络流图 \(G=(V,E)\) 的最小割等于其最大流。

问题模型:

\(n\) 个物品和两个集合 \(A,B\) ,如果一个物品没有放入 \(A\) 集合会花费 \(a_i\) ,没有放入 \(B\) 集合会花费 \(b_i\) ;还有若干个形如 \(u_i,v_i,w_i\) 限制条件,表示如果 \(u_i\)\(v_i\) 同时不在一个集合会花费 \(w_i\) 。每个物品必须且只能属于一个集合,求最小的代价。

组合收益

例题 线性代数
先钦定 \(b\) 的贡献全算上,然后考虑减少的量,要使其最小,又发现 \(a_i\) 等于 \(0,1\) 有不同贡献,\(a_i\)\(a_j\) 组合又有不同贡献,所以考虑最小割,会有一下关系:

\[a+b=c_x+c_y (x,y=1) \]

\[c+d=b_{xy}+b_{xx}+b_{yy}+b_{yx} (x,y=0) \]

\[a+d+v2=c_x+b_{xy}+b_{yy}+b_{yx} (x=1,y=0) \]

\[b+c+v1=c_y+b_{xy}+b_{yx}+b_{xx} (x=0,y=1) \]

用解方程的方法让 \(a=c_x , b=c_y , c=b_{xx}+b_{xy} , d=b_{yy}+b_{yx}\),然后求出 \(v_1,v_2\),给 \(c,d\)求个和发现 \(c=\sum_{i}b_{xi}\) \(d=\sum_{i}b_{yi}\)

例题 employ人员雇佣

最小割求带权点覆盖 & 最大独立集

\(p\) 权值为 \(v_p\),对于左侧的点连 \((st,p,v_p)\),右侧的点 \((p,ed,v_p)\),之间不共存的点连 \((x,y,inf)\)

最小割就是带权最小点覆盖,类似不带权,带权最大独立集=\(\sum v_i\)- \(-\) 最小点覆盖

例题 bzoj3158千钧一发

离散变量模型

例题 切糕

考虑没有 \(d\) 的限制,就是对于每个纵列两成一条链,跑最小割,那断就是选那一层。

在考虑有 \(d\) 的限制,对于每个点,从它的上界向它连一条 \(inf\) 的边。

如何理解:与 \(st\) 相连表示这个点是下层,与 \(ed\) 相连表示这个点是上层。如果 \(1->2\) 断了的话,那么 \(13 -> 4 -> 5 -> 6 -> 2\) 也必须断一条,因为 \(6 -> 2\) 断不了,所以 \(13 -> 4 -> 5 -> 6\) 会有一割,这样就将 \(d\) 的限制表示出来了。至于每条链为啥只割一条边,看这里吧

最大权值闭合图

即给定一张有向图,每个点都有一个权值(可以为正或负或 0),你需要选择一个权值和最大的子图,使得子图中每个点的后继都在子图中。

我们可以先先全选正数,然后假如这个数 \(A_i\) 为正,则与 \(st\) 连容量为 \(A_i\) 的边,否则与 \(ed\) 连容量为 \(-A_i\) 的边。对于选 \(x\) 必须选 \(y\) 的情况,我们 \(x \rightarrow y\) 容量为 \(inf\),可以发现,如果选 \(x\),则 \(x\)\(ed\) 断开,假如不选 \(y\),则 \(y\)\(ed\) 联通,因为 \((x,y)\) 不可能为割,所以存在 \(st \rightarrow x \rightarrow y \rightarrow ed\),不符;所以 \(y\) 必须选。如果 \(x\) 不选,则对 \(y\) 没有影响。总的来说就是用 \(inf\) 的边表示选一个必须选另一个。

例题寿司餐厅 最大获利

费用流

每回 \(spfa\) 找费用最小的流。

code
void add(int x,int y,int w,int c){
	ver[++tot]=y,nex[tot]=head[x],head[x]=tot,edge[tot]=w,cost[tot]=c;
}
queue<int> q;
int gpre[N],gpath[N];
int dist[N];
int st,ed;
int spfa(int s,int t){
	memset(gpre,-1,sizeof(gpre));
	memset(dist,0x7f,sizeof(dist));
	q.push(s);
	dist[s]=0;
	while(!q.empty()){
		int x=q.front();
		q.pop();
		for(int i=head[x];i;i=nex[i]){
			int y=ver[i];
			if(edge[i] && dist[y]>dist[x]+cost[i]){
				dist[y]=dist[x]+cost[i];
				gpre[y]=x;
				gpath[y]=i;
				q.push(y);
			}
		}
	}
	if(gpre[t]==-1) return 0;
	else return 1;
}
pair<int,int> EKF(){
	int flow=0;
	int ct=0;
	while(spfa(st,ed)){
		int mi=(1ll<<20);
		for(int x=ed;x!=st;x=gpre[x]){
			mi=min(mi,edge[gpath[x]]);
		}
		flow+=mi;
		for(int x=ed;x!=st;x=gpre[x]){
			ct+=cost[gpath[x]]*mi;
			edge[gpath[x]]-=mi;
			edge[gpath[x]^1]+=mi;
		}
	}
	return make_pair(flow,ct);
}

上面是保证最大流的前提下最小费用,下面是保证最小费用前提下最大流(相当于可行流)(区别很小)

code
int gpre[N*2],gpath[N*2],dist[N*2];
bool flat[N*2];
queue<int> q; 
int spfa(int s,int t){
	memset(gpre,-1,sizeof(gpre));
	memset(dist,0x7f,sizeof(dist));
	q.push(s);
	dist[s]=0;
	while(!q.empty()){
		int x=q.front();
		q.pop();
		for(int i=head[x];i;i=nex[i]){
			int y=ver[i];
			if(edge[i] && dist[y]>dist[x]+cost[i]){
				dist[y]=dist[x]+cost[i];
				gpre[y]=x;
				gpath[y]=i;
				q.push(y);
			}
		}
	}
	if(gpre[t]==-1) return 0;
	else return 1;
}
int n,m,k;
int cnt=(1ll<<60);
int EKF(){
	int ct=0;
	while(m--&&spfa(st,ed)){//---------------------------------------------<---------
		int mi=1e9;
		for(int x=ed;x!=st;x=gpre[x]){
			mi=min(mi,edge[gpath[x]]);
		}
		int qwe=0;
		for(int x=ed;x!=st;x=gpre[x]){
			qwe+=cost[gpath[x]]*mi;
			edge[gpath[x]]-=mi;
			edge[gpath[x]^1]+=mi;
		}
		if(qwe>0) continue; 
		ct+=qwe;
	}
	return ct;
}

Dij 费用流

考虑到 \(\mathrm{spfa}\) 已经死了,所以可以用这个。

注意到费用流是存在负边权的,所以直接用 \(dij\) 不可行,所以我们考虑给他加一个势能函数 \(h[x]\),他其实就是上一回的 \(dis[x]\)。对于一条 \(u \rightarrow v\) 的边,我们知道 $h[u]+edge(u,v) \leq h[v] $,也就是 $h[u]-h[v]+edge(u,v) \leq 0 $。所以我们假如给每条边都加上一个 \(h[u]-h[v]\),那么所有的边权就非负了。

对于求 \(h[x]\) 我们可以先 \(\mathrm{spfa}\) 一遍求出,对于以后的我们不可能每次都 \(spfa\),但是可以让 \(h[x]\) 加上本次的 \(dist[x]\)

证明,每次增广会增加一些边 \(j \rightarrow i\),对于这些边肯定存在 \(d'[i]+w'[i][j]=d'[y]\),因为是最短路,然后

\[d'[i]+w[i][j]+h[i]-h[j]=d'[j] \]

\[(d'[i]+h[i])-(d'[j]+h[j])+w[i][j]=0 \]

\[(d'[j]+h[j])-(d'[i]+h[i])+w[j][i]=0 \]

所以这样操作是合法的。

所以流程就是,先来一遍 \(\mathrm{spfa}\) 然后就可以 \(\mathrm{dij}\),每次记得更新 \(h[x]\),流程就和 \(\mathrm{EKF}\) 一样。

code

struct Dij_EKF{
    const int V=2*1e4+10;
    const int E=2*1e6;
    int head[V],ver[E],nex[E],edge[E],cost[E],tot=1;
    inline void add(int x,int y,int w,int c){
        ver[++tot]=y,nex[tot]=head[x],head[x]=tot,edge[tot]=w,cost[tot]=c;
        ver[++tot]=x,nex[tot]=head[y],head[y]=tot,edge[tot]=0,cost[tot]=-c;
    }
    int gpre[V],gpath[V];
    int dist[V],h[V];
    int st,ed;
    bool flat[V];
    inline int dij(int s,int t){
        memset(dist,0x7f,sizeof(int)*(ed+1));
        memset(flat,0,sizeof(int)*(ed+1));
        gpre[t]=-1;
        dist[s]=0;
        priority_queue<pair<int,int>> q;
        q.push(make_pair(0,s));
        while(!q.empty()){
            int x=q.top().second;
            q.pop();
            if(flat[x]) continue;
            flat[x]=1;
            for(int i=head[x];i;i=nex[i]){
                int y=ver[i];
                if(edge[i] && dist[y]>dist[x]+cost[i]+h[x]-h[y]){
                    dist[y]=dist[x]+cost[i]+h[x]-h[y];
                    gpre[y]=x;
                    gpath[y]=i;
                    q.push(make_pair(-dist[y],y));
                }
            }
        }
        if(dist[t]==dist[0]) return 0;
        else return 1;
    }
    inline void spfa(int s,int t){
        memset(h,0x7f,sizeof(int)*(ed+1));
        queue<int> q;
        q.push(s);
        flat[s]=1; h[s]=0;
        while(!q.empty()){
            int x=q.front();
            q.pop();
            flat[x]=0;
            for(int i=head[x];i;i=nex[i]){
                int y=ver[i];
                if(edge[i] && h[y]>h[x]+cost[i]){
                    h[y]=h[x]+cost[i];
                    if(!flat[y]){
                        q.push(y);
                        flat[y]=1;
                    }
                }
            }
        }
    }
    int EKF(){
        spfa(st,ed);
        int ct=0;
        while(dij(st,ed)){
            int mn=(1ll<<50);
            for(int x=ed;x!=st;x=gpre[x]) mn=min(mn,edge[gpath[x]]);
            for(int i=1;i<=ed;i++) h[i]+=dist[i];
            for(int x=ed;x!=st;x=gpre[x]){
                ct+=cost[gpath[x]]*mn;
                edge[gpath[x]]-=mn;
                edge[gpath[x]^1]+=mn;
            }
        }
        return ct;
    }
}T;

有上下界的网络流

  • 无源汇有上下界可行流(也就是循环流)

可行流算法的核心是将一个不满足流量守恒的初始流调整成满足流量守恒的流。

我们一开始钦定每条边的流量为其下界,可以注意到的是此时每个点的入流量不等于出流量,将这个流量定义初始流。

所以我们进行调整,将原图的容量全部改为 \(up-down\),这样如何调整总流量都在范围内,定义数组 \(A_i\) 等于 \(i\) 的入流量减去出流量。为了满足流量守恒,我们考虑在残量网络上求出一个另不满足流量守恒的附加流,使得这个附加流和我们的初始流合并之后满足流量守恒。

定义源点 \(st\),汇点 \(ed\)。如果一个点 \(A_i>0\) 相当于要求附加流出流量大于入流量,为了让多出的出流量有一个来路,所以 \(st \rightarrow i\),容量为 \(|A_i|\);如果一个点 \(A_i<0\) 相当于要求附加流入流量大于出流量,为了让多出的入流量有一个出路,所以 \(i \rightarrow ed\),容量为 \(|A_i|\)

可以发现 \(A_i\) 之和为 \(0\),因为每条边对两边的贡献互为相反数,设正 \(sum=\sum A_i (A_i>0)\)

我们对 \(st-ed\) 跑最大流,如果最大流等于 \(sum\),也就是可以跑满,说明有可行流的,反之没有。最后每条边可行流的流量 \(=\) 容量下界 \(+\) 附加流流量(跑完后反向边权值)

  • 有源汇有上下界可行流

建出图 \(sst-eed\) 之后连一条 \(eed \rightarrow sst\) 下界为 \(0\),上界为正无穷的边,转化为无源汇有上下界可行流,最后跑出来的可行流大小其实为 \(eed \rightarrow sst\) 反向边的大小。

  • 有源汇有上下界最大流

我们跑完的可行流不一定是最大的,所以我们再在 \(sst-eed\) 之间的残余网络中跑最大流,最后 答案 \(=\) 可行流 \(+\) 之后跑的最大流。

注意这里判断是否可行是看 \(st-ed\) 最大流是否等于 \(sum\)。可行流的大小等于 \(eed \rightarrow sst\) 反向边的大小,不要混淆。

例题Zoj3229 Shoot the Bullet

code
#include<bits/stdc++.h>
using namespace std;
const int maxn=2005;
const int inf=0x7f7f7f7f;
int st,ed;
int head[maxn],ver[maxn*maxn],nex[maxn*maxn],edge[maxn*maxn],tot=1;
void add(int x,int y,int v){
    ver[++tot]=y,nex[tot]=head[x],head[x]=tot,edge[tot]=v;
    ver[++tot]=x,nex[tot]=head[y],head[y]=tot,edge[tot]=0;
}
int A[maxn];
queue<int> q;
int dep[maxn],cur[maxn];
int bfs(){
    memset(dep,0,sizeof(int)*(ed+1));
    dep[st]=1;
    q.push(st);
    while(!q.empty()){
        int x=q.front();
        q.pop();
        for(int i=head[x];i;i=nex[i]){
            int y=ver[i];
            if(edge[i]<=0 || dep[y]) continue;
            dep[y]=dep[x]+1;
            q.push(y);
        }
    }
    if(dep[ed]) return 1;
    else return 0;
}
int dfs(int x,int flow){
    if(x==ed) return flow;
    for(int i=cur[x];i;i=nex[i]){
        cur[x]=i;
        int y=ver[i];
        if(dep[y]==dep[x]+1 && edge[i]>0){
            int d=dfs(y,min(flow,edge[i]));
            if(d>0){
                edge[i]-=d;
                edge[i^1]+=d;
                return d;
            }
        }
    }
    return 0;
}

int Dinic(){
    int ans=0;
    while(bfs()){
        for(int i=1;i<=ed;i++){
            cur[i]=head[i];
        }
        while(int d=dfs(st,inf)){
            ans+=d;
        }
    }
    return ans;
}
signed main(){
    // freopen("P5192_1.in","r",stdin);
    // freopen("in.in","r",stdin);
    int n,m;
    while(scanf("%d%d",&n,&m)==2){
        memset(A,0,sizeof(A));
        tot=1;
        memset(head,0,sizeof(head));
        int sst=n+m+1,eed=sst+1;
        st=eed+1,ed=st+1;
        for(int i=1;i<=m;i++){
            int g;
            scanf("%d",&g);
            add(n+i,eed,inf-g);
            A[eed]+=g;
            A[n+i]-=g;
        }
        int sum=0;
        for(int i=1;i<=n;i++){
            int c,d;
            scanf("%d%d",&c,&d);
            add(sst,i,d);
            for(int j=1;j<=c;j++){
                int t,l,r;
                scanf("%d%d%d",&t,&l,&r);
                t++;
                add(i,n+t,r-l);
                A[i]-=l;
                A[n+t]+=l;
            }
        }
        add(eed,sst,inf);
        int pos=tot;
        for(int i=1;i<=eed;i++){
            if(A[i]==0) continue;
            if(A[i]<0) add(i,ed,-A[i]);
            else add(st,i,A[i]);
            if(A[i]>0) sum+=A[i];
        }
        if(Dinic()!=sum){
            printf("-1\n\n");
        }
        else{
            int ans=edge[pos];
            for(int i=head[st];i;i=nex[i]){
                edge[i]=edge[i^1]=0;
            }
            for(int i=head[ed];i;i=nex[i]){
                edge[i]=edge[i^1]=0;
            }
            for(int i=head[sst];i;i=nex[i]){
                if(ver[i]==eed){
                    edge[i]=edge[i^1]=0;
                }
            }
            st=sst,ed=eed;
            ans+=Dinic();
            printf("%d\n\n",ans);
        }
    }
}
  • 有源汇上下界最小流

考虑反边的含义,反边每增加一,相当于正边减小一,所以我们给 \(sst-eed\) 反边跑最大流,那么 答案 \(=\) 可行流 \(-\) 反边最大流。

例题清理雪道

code
#include<bits/stdc++.h>
using namespace std;
const int maxn=2000;
const int inf=(1<<20);
int st,ed;
int head[maxn*2],nex[maxn*maxn],ver[maxn*maxn],edge[maxn*maxn],tot=1;
void add(int x,int y,int v){
    ver[++tot]=y,nex[tot]=head[x],head[x]=tot,edge[tot]=v;
    ver[++tot]=x,nex[tot]=head[y],head[y]=tot,edge[tot]=0;
}
int A[maxn*maxn];
queue<int> q;
int dep[maxn],cur[maxn];
int bfs(){
    memset(dep,0,sizeof(int)*(max(st,ed)+1));
    dep[st]=1;
    q.push(st);
    while(!q.empty()){
        int x=q.front();
        q.pop();
        for(int i=head[x];i;i=nex[i]){
            int y=ver[i];
            if(edge[i]<=0 || dep[y]) continue;
            dep[y]=dep[x]+1;
            q.push(y);
        }
    }
    if(dep[ed]) return 1;
    else return 0;
}
int dfs(int x,int flow){
    if(x==ed) return flow;
    for(int i=cur[x];i;i=nex[i]){
        cur[x]=i;
        int y=ver[i];
        if(dep[y]==dep[x]+1 && edge[i]>0){
            int d=dfs(y,min(flow,edge[i]));
            if(d>0){
                edge[i]-=d;
                edge[i^1]+=d;
                return d;
            }
        }
    }
    return 0;
}
int Dinic(){
    int ans=0;
    while(bfs()){
        int mx=max(ed,st);
        for(int i=1;i<=mx;++i){
            cur[i]=head[i];
        }
        while(int d=dfs(st,inf)){
            ans+=d;
        }
    }
    return ans;
}
signed main(){
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        int m;
        scanf("%d",&m);
        for(int j=1;j<=m;j++){
            int x;
            scanf("%d",&x);
            add(i,x,inf-1);
            A[i]--,A[x]++;
        }
    }
    int sst=n*2+1,eed=sst+1;
    st=eed+1,ed=st+1;
    for(int i=1;i<=n;i++){
        add(sst,i,inf);
        add(i,eed,inf);
    }
    add(eed,sst,inf);
    int pos=tot;
    int sum=0;
    for(int i=1;i<=eed;i++){
        if(A[i]==0) continue;
        if(A[i]<0) add(i,ed,-A[i]);
        else add(st,i,A[i]);
        if(A[i]>0) sum+=A[i];
    }
    Dinic();
    int ans=edge[pos];
    for(int i=head[st];i;i=nex[i]){
        edge[i]=edge[i^1]=0;
    }
    for(int i=head[ed];i;i=nex[i]){
        edge[i]=edge[i^1]=0;
    }
    for(int i=head[sst];i;i=nex[i]){
        if(ver[i]==eed){
            edge[i]=edge[i^1]=0;
        }
    }
    st=eed,ed=sst;
    ans-=Dinic();
    printf("%d",ans);
}

证明

参考资料

posted @ 2023-12-05 09:16  _bloss  阅读(45)  评论(0)    收藏  举报