网络流

洛谷题单

1.网络流(网络最大流)

  • 网络(自来水管道网 ):一张有向图,其上的 边权 称为容量。拥有一个源点和汇点。

  • 流(水流): 每条边上的流不能超过它的容量,对于除源点汇点以外的所有点,流入量=流出量

从源点流出,全部流入汇点(假设源点流出的流量足够多)

image-20230209124202308

增广路:从源点到汇点的路径,其上所有边的残余容量均大于0

每找到一条增广路,增加的流量=这条路径上所有容量的最小值

残余容量:增广路上扣除 增加的流量 后的 剩余

image-20230209124443356

若选择1 \(\to\) 2 \(\to\) 3 \(\to\) 4 ,求得最大流=1

若选择1 \(\to\) 3 \(\to\) 4 ,1 \(\to\) 2 \(\to\) 4 最大流=2

反向边:在建边的同时,在反方向建一条边权为0的边(抵消,反悔机制)

image-20230209124721277

Ford-Fulkerson:

深搜 \(O(ef)\) 边数×最大流

int n, m, s, t; // s是源点,t是汇点
bool ck[N];
int dfs(int x = s, int res = INF)
{
    if (x == t) return res; // 到达终点,返回这条增广路的流量
    ck[x] = 1;
    for (int i=hd[x];i;i=nx[i])
    {
        // 返回的条件是残余容量大于0、未访问过该点、且接下来可以达到终点(递归地实现)
        // 传递下去的流量是边的容量与当前流量中的较小值
        if (d[i] > 0 && !ck[to] && (c = dfs(to[i], min(d[i], res))) != -1)
        {
            d[i] -= c;
            d[i^1] += c;
            // 这是链式前向星取反向边的一种简易的方法
            // 建图时要把cnt置为1,且要保证反向边紧接着正向边建立
            return c;
        }
    }
    return -1; // 无法到达终点
}
inline int FF()
{
    int ans = 0, c;
    while ((c = dfs()) != -1)
    {
        memset(ck, 0, sizeof(ck));
        ans += c;
    }
    return ans;
}

Edmond-Karp:

广搜 \(O(n e^2)\) 点数 × 边数的平方

int n, m, s, t, last[MAXN], flow[MAXN];
inline int bfs()
{
    memset(last, -1, sizeof(last));
    queue<int> q;
    q.push(s);
    flow[s] = INF;
    while (!q.empty())
    {
        int p = q.front();
        q.pop();
        if (p == t) /*到达汇点,结束搜索*/ break;
        for (int i=hd[x];i;i=nx[i])
        {
            if (d[i] > 0 && last[to[i]] == -1) // 如果残余容量大于0且未访问过(所以last保持在-1)
            {
                last[to[i]] = i;
                flow[to[i]] = min(flow[p], vol);
                q.push(to);
            }
        }
    }
    return last[t] != -1;
}
inline int EK()
{
    int maxflow = 0;
    while (bfs())
    {
        maxflow += flow[t];
        for (int i = t; i != s; i = to[last[i] ^ 1]) // 从汇点原路返回更新残余容量
        {
            edges[last[i]].w -= flow[t];
            edges[last[i] ^ 1].w += flow[t];
        }
    }
    return maxflow;
}

Dinic:

BFS分层,DFS寻找

多路增广,当前弧优化(一条边被走过一遍之后不会再走)

#include <bits/stdc++.h>
#define per(i,a,b) for(int i(a);i<=b;++i)
using namespace std;
const int N=210,M=5010,inf=0x3f3f3f3f;
int s,t,hd[N],to[M<<1],nx[M<<1],d[M<<1],id=1,dis[N],cur[N]/*hd*/;
void addd(int x,int y,int z){to[++id]=y,d[id]=z,nx[id]=hd[x],hd[x]=id;}
queue<int>q;
bool bfs()
{
	memset(dis,-1,sizeof(lv));//分层图
	lv[s]=0;
	memcpy(cur,hd,sizeof(hd));//当前弧优化(一条边被走过一遍之后不会再走)
	q.push(s);
	int x;
	while(!q.empty())
	{
	        x=q.front(),q.pop();
		for(int i=hd[x];i;i=nx[i])
		{
			if(d[i]&&dis[to[i]]==-1) 
                        {
                          dis[to[i]]=dis[x]+1;
                          q.push(to[i]);
                        }
		}
	}
	return dis[t]!=-1;
}
long long dfs(int x=s,long long res=LONG_LONG_MAX)
{
	if(x==t) return res;
	long long rs=res,c;
	for(int i=cur[x];i&&rs;i=nx[i])//多路增广
	{
		cur[x]=i;
		//如果该点被增广之后,rs还>0;
		//说明要么该边容量用完,要么该边不能到达t
		//所以该边可以删去
		if(d[i]&&dis[to[i]]==dis[x]+1)
		{
			c=dfs(to[i],min((long long)d[i],rs));
			rs-=c;
			d[i]-=c,d[i^1]+=c;
		}
	}
	return res-rs;
}
signed main()
{
	// freopen(".in","r",stdin);
    // freopen(".out","w",stdout);
	int n,m,x,y,z;
	cin>>n>>m>>s>>t;
	per(i,1,m) scanf("%d%d%d",&x,&y,&z),addd(x,y,z),addd(y,x,0);
	long long ans=0;
	while(bfs()) ans+=dfs();
	printf("%lld\n",ans);
	return 0;
}

最小割最大流定理

从网络中选择一些边,删去这些边后,剩下两个不连通的点集,分别包括源点和汇点

最小割:删去边的容量和最小

神奇定理:最小割=最大流

一个比喻,割就是恐怖分子切断自来水厂到你家之间的水管 (你们恐怖分子就这点志向吗?),切断每个水管的代价就是水管的宽度(边的容量)。然后当代价最小时,从切断的水管中流出的流量之和就是最大流。

试题库问题 (洛谷2763 )

题意:

n道题,每道题属于p个类别;从题库中抽取m道题,每个类别\(a_i\)

思路:

利用网络流的反悔机制,进行匹配

S \(\to\) 类别(k) 边权\(a_i\)

$k_i \to $ 题(n) 边权 1

\(\to\) T 边权 1

最大流=m 有解

输出:枚举题的类别 (1~k) 如果\(dis[k,x]=0\) 表示 x 被选入了第 k 类题

方格取数问题(洛谷2774)

目标:

从方格中取数,使任意两个数所在方格没有公共边

思路

需要舍弃一些点,使得剩下的点满足 任意两个数所在方格没有公共边

最大和=全局和-舍弃和,要使舍弃和最小 \(\Longrightarrow\)最小割=最大流

转化

将方格图转换为网络图

方格分为两类(横纵坐标之和的奇偶)相邻的方格横纵坐标奇偶性一定不同

if((i+j-1)&1)//偶
{
    addd(s,e(i,j),x),addd(e(i,j),s,0);//x为该格权值
    per(k,0,3)
    {
        x=i+f[k][1],y=j+f[k][0];
        if(b(x,1,n)&&b(y,1,m))
            addd(e(i,j),e(x,y),inf),addd(e(x,y),e(i,j),0);//互斥的方格连边
    }
}
else addd(e(i,j),t,x),addd(t,e(i,j),0);

当图符合要求的时候,S,T 不连通,分为两个子图

ans= sum - 最大流

参考文章:算法学习笔记(28): 网络流

2.费用流(最小费用最大流)

费用= 每条边的流量 × 单位流量的费用

\(Cost(flow)=\sum_{<x,y>∈E}cost(x,y) × flow(x,y)\)

MCMF( spfa找最短路,回溯更新 )

bool spfa()//寻找源点到汇点的最小费用路 (以费用为边权,找最短路)
{
	int x;
	ck.reset();
	memset(dis,0x3f,sizeof(dis));
	dis[s]=0,q.push(s);
	while(!q.empty())
	{
		x=q.front(),q.pop();
		ck[x]=0;
		for(int i=hd[x];i;i=nx[i])
		{
			if(dis[to[i]]>dis[x]+c[i]&&d[i]>0)
			{
				dis[to[i]]=dis[x]+c[i];
				pre[to[i]]={x,i};//从x点经过i边到达了 当前点 (to[i])
				if(!ck[to[i]])
				{
					ck[to[i]]=1;
					q.push(to[i]);
				}
			}
		}
	}
	return dis[t]!=inf;
}
void sol()
{
    int ans=0;
	int res,v;
	while(spfa())
	{//从记录的路径进行回溯更新
		res=inf,v=t;
		while(v!=s)
		{
			res=min(res,d[pre[v].i]);//最大流
			v=pre[v].x;
		}
		v=t;
		while(v!=s)
		{
			d[pre[v].i]-=res,d[pre[v].i^1]+=res;
			v=pre[v].x;
		}
		ct+=dis[t]*res;
		ans+=res;
	}
}

3.最大权闭合子图

太空飞行计划(洛谷2762)

闭合子图

对于有向图 ,\(G(V,E)\) 它的一个闭合子图是一个点集,并且满足点集内所有的点,他们的出边指向的点都是点集内的点,然后将出边添加上,就是一个子图了。(入边无所谓)

闭合子图 \(\to\) 网络流

建一个源点,和一个汇点,
源点连向权值为正的点,流量为点权
权值为负的点向汇点连一条边,流量为点权的绝对值
点与点之间的边不变,流量为 INF

简单割(最小割):只割源点出去的边或到汇点的边。

目的:每一个S出发的边只能流向自己

割完之后 \(\Longrightarrow\) 每个实验所需的仪器都在S的闭合子图里

最大权闭合子图

设:\(v_1\) 为闭合子图 ,\(v_2\) 为闭合子图的补集

割的流量:\(C[S,T]=C[v_1,T]+C[S,v_2]=\sum_{v\in v_1^-} W_v+\sum_{v\in v_2^+} W_v\)

闭合子图权值:\(w_1=\sum_{v\in v_1^+} W_v-\sum_{v\in v_1^-} W_v\)

\(C[S,T]+W_1=\sum_{v\in v_1^+} W_v+\sum_{v\in v_2^+} W_v=\sum_{v\in v^+} W_v\)

\(\therefore 闭合子图最大权 W_1=\sum_{v\in v^+} -C[S,T]\)

\(C[S,T]\) 最小,即为最小割

参考:最大权闭合子图

4.拆点

1.入点,出点

求割点的时候,可以将点拆成入点和出点,割掉中间连接的边就相当于割掉了点

奶牛的电信(洛谷1345)

将每个点拆成两个,当这两个点之间的边被割,代表改电脑坏了

QAQ
教辅的组成(洛谷1231)

将书拆点,练习册连书的入点,书的出点连对应的答案

魔术球问题(洛谷2765)

将一个球拆成入点出点

源点->入点

若两个数的和为平方数,入点连向出点

出点->汇点

边权都为 1

一个一个加入点,在之前的基础上跑最大流,如果流量=0,表示该点没法往柱子上放了,sum+=1,当sum=n的时候停止

玄学找规律:每个数枚举,能放在已有柱子上就直接放,不能放就开新柱子,模拟可A。证明

参考:网络流建模基础

5.点的构造

[HEOI2016/TJOI2016]游戏 (洛谷2825)
#***
*#**
**#*
xxx#

先不考虑硬石头,每摆放一个炸弹,他所在的行和列不能摆放炸弹

问题可以转换成二分图匹配,行(s)和列(t)匹配

硬石子:将一行/列可以分成两行/列

分别处理行和列的点构造,能放炸弹的点进行加边,二分图匹配

#include <bits/stdc++.h>
#define per(i,a,b) for(int i(a);i<=b;++i)
using namespace std;
const int E=55,N=3010,M=3010,inf=0x3f3f3f3f;
int s,t,hd[N],to[M<<1],nx[M<<1],id=1,match[N];
void addd(int x,int y){to[++id]=y,nx[id]=hd[x],hd[x]=id;}
int x[E][E],y[E][E];
char mp[E][E];
bitset<N>ck;
bool fd(int x)
{
	for(int i=hd[x];i;i=nx[i])
	{
		if(!ck[to[i]])
		{
			ck[to[i]]=1;
			if(!match[to[i]]||fd(match[to[i]]))
			{
				match[to[i]]=x;
				return 1;
			}
		}
	}
	return 0;
}
signed main()
{
	// freopen(".in","r",stdin);
    // freopen(".out","w",stdout);
	int n,m,nt=0,mt=0;
	bool fg=1;
	pair<int,int>ls;
	ls=make_pair(0,0);
	cin>>n>>m;
	per(i,1,n)
	{
		cin>>mp[i]+1;
		per(j,1,m)
		{
			if(mp[i][j]=='#')
			{
				fg=1;
				continue;
			}
			if(!fg)
			{
				if(mp[i][j]!='X')
				{
					x[i][j]=x[ls.first][ls.second];
					ls=make_pair(i,j);
				}
			}
			else if(mp[i][j]!='X')
			{
				x[i][j]=x[ls.first][ls.second]+1;
				nt++;
				ls=make_pair(i,j);
				fg=0;
			}
		}
		fg=1;
	}
	fg=1;
	ls=make_pair(0,0);
	per(i,1,m) 
	{
		per(j,1,n)
		{
			if(mp[j][i]=='#')
			{
				fg=1;
				continue;
			}
			if(!fg)
			{
				if(mp[j][i]!='X')
				{
					y[j][i]=y[ls.second][ls.first];
					ls=make_pair(i,j);
				}
			}
			else if(mp[i][j]!='X')
			{
				y[j][i]=y[ls.second][ls.first]+1;
				mt++;
				ls=make_pair(i,j);
				fg=0;
			}
		}
		fg=1;
	}
	per(i,1,n) per(j,1,m) if(mp[i][j]=='*') addd(x[i][j],y[i][j]+nt);
	int ans=0;
	per(i,1,nt)
	{
		ck.reset();
		if(fd(i)) ++ans;
	} 
	printf("%d\n",ans);
	return 0;
}
posted @ 2023-02-09 18:45  f2021yjm  阅读(24)  评论(0编辑  收藏  举报