网络流学习笔记

网络流学习笔记

目录:

  • 概念汇总
  • 网络
  • 最大流:
    1. FF算法
    2. EK算法
    3. dinic算法
  • 最大流经典模型
  • 最大流习题
  • 最小割
  • 最小割经典模型
  • 费用流

概念汇总:

  • 网络流:一种类比水流的解决问题的方法。

  • 网络:拥有源点汇点有向图

  • :一条有向边,简称边。

  • 弧的流量:称作"边的流量"或简称流量,在一个流量网络中每天边都会有一个流量 \(f(x,y)\)

    • warning:根据 \(f(x,y)\) 的定义,\(f(x,y)\) 可能为负。
  • 弧的容量:称作"边的容量"或简称容量,每条边都会有一个容量 \(c(x,y)\)

  • 源点:可理解为起点,入度为 \(0\)流入的流量可为任意值。

  • 汇点:可理解为终点,出度为 \(0\)流出的流量可为任意值。

  • 容量网络:每条边均给出了容量的网络。

  • 流量网络:每条边均给出了流量的网络。

  • 弧的残留容量:简称残留容量。对于每条边,残留容量=容量-流量

  • 残量网络:每条边均有残留容量的网络。对于一个网络,残量网络=容量网络-流量网络

    • PS:初始的残留容量网络即为容量网络
  • 网络的流量:在某种方案下形成的流量网络中流入汇点的容量。

一、网络

网络是一个有向图 \(G=(V,E)\),对于 \((x,y)\in E\) 都有一个权值 \(c(x,y)\),将其称为容量

网络中有两个特殊点:源点 \(S\)汇点 \(T\)

二、流

\(f(x,y)\) 是一个定义在 \(x\in V\)\(y\in V\) 上的实数函数,\(f\)网络的流函数,对于 \((x,y)\in E\)\(f(x,y)\) 称为这条边的流量,\(c(x,y)-f(x,y)\) 称为边的剩余容量,记为 \(c_f(x,y)\)

\(f(x,y)\) 满足以下性质:

  • 对于每个 \((x,y)\in E\)\(f(x,y)\le c(x,y)\),即每条边的流量不超过其容量。那么显而易见的每条边的剩余容量是一个非负整数。

  • \(f(x,y)=-f(y,x)\),这里的符号代表方向,和物理上加速度或其它矢量的正负意义一样。

  • \(\forall x\not =S,x\not =T,\sum _{(u,v)\in E} f(u,x)= \sum (x,v) f(x,v)\),即从源点流出的流量等于汇点流入的流量。

三、最大流

最大流的含义是希望在源点上指定合适流入的流量,使得汇点中流出的流量尽可能多,并且在流经中间点时满足上文所说的三个性质,即网络的流量最大值

最大流网络即指达到最大流流量网络

求解最大流有三种方法:

1.Ford-Fulkerson 增广:

简称 FF 算法。

显而易见,我们可以类似将管道逐渐一点点的放水,直到跑满为止。

也就是找到一条从 \(S\)\(T\) 的路径 \(P\),使得 \(\forall (x,y)\in P,c_f(x,y)>0\),即所有边的剩余容量均不为空。

将这条路径跑满,即对于任意 \((x,y)\in P\)\(c_f(x,y)\) 都自减 \(\min_{(x,y)\in P} c_f(x,y)\),即将 \(P\) 中所有的边都减去 \(P\) 中最小的边的剩余容量。

但直接 dfs 依次找会出现先将某一条路径 \(P\) 全自减 \(k\),导致其中某一条存在于另一个路径 \(P'\) 的边 \((u,v)\in P\)\(c_f(u,v)\) 变为了 \(0\),导致原本应该选择 \(P'\) 这条路径但因为 \(c_f(u,v)=0\) 而未选择将 \(P'\) 跑满的情况。

所以考虑可撤销贪心,即建立一条反向边来实现允许撤销的操作。

具体操作为:建立一条初始容量为 \(0\) 的反向边,在每次一个路径 \(P\) 中的边跑满后将这条路径上的所有边的反向边的容量增加 \(k\)

感性理解即为一个水管,流出去了多少水就能流回来多少水,来达到撤销的效果。

Ford-Fulkerson 算法使用 dfs 寻找增广路,每次寻找一个增广路。复杂度 \(O((n+m)f)\),其中 \(f\) 为最大流。

#include<bits/stdc++.h>
//#define int long long
#define inf 1e9
#define linf 1e18
#define db double
#define ldb long double
#define il inline
#define sd std::
#define F(i,a,b) for(int i=(a);i<=(b);i++)
#define f(i,a,b) for(itn i=(a);i>=(b);i--)
#define me(x,y) memset(x,y,sizeof x)
#define pii sd pair<int,int>
#define X first
#define Y second
#define umap(x,y) sd unorded_map<x,y>
#define pque(x) sd priority_queue<x>
#define Fr(a) for(auto it:a)
#define kg putchar(' ')
#define dbg(x) sd cout<<#x<<":"<<x<<sd endl
#define MAX(x,y) (x>y?x:y)
#define MIN(x,y) (x<y?x:y)
il int read(){int w=1,c=0;char ch=getchar();for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;for(;ch>='0'&&ch<='9';ch=getchar()) c=(c<<3)+(c<<1)+ch-48;return w*c;}
void printt(int x){if(x>9) printt(x/10);putchar(x%10+48);}
il void print(int x){if(x<0) putchar('-'),x=-x;printt(x);}
il void printk(int x){print(x);kg;}
il void put(int x){print(x);puts("");}
const int N=5010,M=210;
struct node
{
	int nex;
	int to;
	int w;
}a[N<<1];
int tot,head[M];
void add(int u,int v,int w)
{
	a[++tot].nex=head[u];
	head[u]=tot;
	a[tot].to=v;
	a[tot].w=w;
}
int n,m,s,t;
int vis[M];
int dfs(int u,int flow)
{
	if(u==t) return flow;
	for(int i=head[u];i;i=a[i].nex)
	{
		int v=a[i].to,w=a[i].w,k;
		if(w&&!vis[v]&&(k=dfs(v,MIN(flow,w)))!=-1)
		{
			a[i].w-=k;
			a[i^1].w+=k;
			return k;
		}
	}
	return -1;
}
il void solve()
{
	n=read(),m=read(),s=read(),t=read();
	F(i,1,m)
	{
		int x=read(),y=read(),z=read();
		add(x,y,z);
		add(y,x,0);
	}
	int p,ans=0;
	while((p=dfs(s,inf))!=-1)
	{
		ans+=p;
		me(vis,0);
	}
	put(ans);
}
int main()
{
	int T=1;
//	T=read();
	while(T--) solve();
}

2. Edmond-Karp 算法

简称 EK 算法。

EK 算法即用 BFS 实现的 FF 算法。通过 BFS 找到一条 S 到 T 包含边数最少的路径,然后自减。

(打锅了,代码后面再补吧。)

3.Dinic 阻塞流算法

简称 Dinic 算法。

阻塞流:即一个源点无法再流入任何流量的流量网络。

注意:阻塞流不一定是最大流,但最大流一定是阻塞流。

EK 算法的劣处在于其每次 bfs 一次之后只能找到一条增广路。

所以 Dinic 算法对寻找过程进行优化,即每次找到多条增广路。

Dinic 算法在寻找增广路之前会将图分层,第 \(i\) 层代表起点走 \(i\) 步到达的点集,并保留满足终点层数比起点层数多 \(1\) 的边。

然后在分层图中寻找从 \(s\)\(t\) 的路径跑满就好了。

具体实现过程可见 bilibili视频

#include<bits/stdc++.h>
#define sd std::
#define int long long
#define inf 0x3f3f3f3f
#define linf 1e18
#define il inline
#define db double
#define ldb long double
#define F(i,a,b) for(int i=(a);i<=(b);i++)
#define f(i,a,b) for(int i=(a);i>=(b);i--)
#define MIN(x,y) (x<y?x:y)
#define MAX(x,y) (x>y?x:y)
#define me(x,y) memset(x,y,sizeof x)
#define pii sd pair<int,int>
#define umap(x,y) sd unordered_map<x,y>
#define pque(x) sd priority_queue<x>
#define X first
#define Y second
#define kg putchar(' ')
#define Fr(a) for(auto it:a)
#define dbg(x) sd cout<<#x<<": "<<x<<sd endl
il int read(){int w=1,c=0;char ch=getchar();for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;for(;ch>='0'&&ch<='9';ch=getchar()) c=(c<<3)+(c<<1)+ch-48;return w*c;}
void printt(int x){if(x>9) printt(x/10);putchar(x%10+48);}
il void print(int x){if(x<0) putchar('-'),printt(-x);else printt(x);}
il void put(int x){print(x);putchar('\n');}
il void printk(int x){print(x);kg;}
const int N=1e5+10;
struct node
{
	int nex;
	int to;
	int w;
}a[N];
int tot=1,head[N],now[N];
void add(int u,int v,int w)
{
	a[++tot].nex=head[u];
	head[u]=tot;
	a[tot].to=v;
	a[tot].w=w;
}
int d[N];//d为层数 
int n,m,s,t;
int bfs()
{
	me(d,0);
	sd queue<int> q;
	q.push(s);
	d[s]=1;
	now[s]=head[s];
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		for(int i=head[u];i;i=a[i].nex)
		{
			int v=a[i].to,w=a[i].w;
			if(w>0&&!d[v])
			{
				d[v]=d[u]+1;
				now[v]=head[v];
				if(v==t) return 1;
				q.push(v);
			}
		}
	}
	return 0;
}
int dfs(int u,int flow)
{
	if(u==t) return flow;
	int tmp=flow;
	for(int i=now[u];i&&tmp;i=a[i].nex)
	{
		int v=a[i].to,w=a[i].w;
		now[u]=i;
		if(d[u]+1==d[v]&&w)
		{
			int k=dfs(v,MIN(tmp,w));
			if(!k) d[v]=0;
			a[i].w-=k;
			a[i^1].w+=k;
			tmp-=k;
		}
	}
	return flow-tmp;
}
il void solve()
{
	n=read(),m=read(),s=read(),t=read();
	F(i,1,m)
	{
		int x=read(),y=read(),z=read();
		add(x,y,z);
		add(y,x,0);
	}
	int ans=0;
	while(bfs()) ans+=dfs(s,INT_MAX);
	return put(ans);
}
signed main()
{
	int T=1;
//	T=read();
	while(T--) solve();
    return 0;
}

四、最大流经典模型

1.二分图最大匹配问题

例题:飞行员配对方案问题

将外籍飞行员和英国飞行员连边,每个点只能选一次,求最多能选多少条边。

即二分图的最大匹配。

这个问题我们考虑新建立一个源点和汇点,源点与二分图左部所有点相连,汇点与二分图右部所有点相连,所有边的容量均为 \(1\),然后跑最大流。

#include<bits/stdc++.h>
#define sd std::
#define int long long
#define inf 0x3f3f3f3f
#define linf 1e18
#define il inline
#define db double
#define ldb long double
#define F(i,a,b) for(int i=(a);i<=(b);i++)
#define f(i,a,b) for(int i=(a);i>=(b);i--)
#define MIN(x,y) (x<y?x:y)
#define MAX(x,y) (x>y?x:y)
#define me(x,y) memset(x,y,sizeof x)
#define pii sd pair<int,int>
#define umap(x,y) sd unordered_map<x,y>
#define pque(x) sd priority_queue<x>
#define X first
#define Y second
#define kg putchar(' ')
#define Fr(a) for(auto it:a)
#define dbg(x) sd cout<<#x<<": "<<x<<sd endl
il int read(){int w=1,c=0;char ch=getchar();for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;for(;ch>='0'&&ch<='9';ch=getchar()) c=(c<<3)+(c<<1)+ch-48;return w*c;}
void printt(int x){if(x>9) printt(x/10);putchar(x%10+48);}
il void print(int x){if(x<0) putchar('-'),printt(-x);else printt(x);}
il void put(int x){print(x);putchar('\n');}
il void printk(int x){print(x);kg;}
const int N=1e5+10;
struct node
{
	int nex;
	int to;
	int w;
}a[N];
int tot=1,head[N],now[N];
void add(int u,int v,int w)
{
	a[++tot].nex=head[u];
	head[u]=tot;
	a[tot].to=v;
	a[tot].w=w;
}
int d[N];//d为层数 
int n,m,s,t;
int bfs()
{
	me(d,0);
	sd queue<int> q;
	q.push(s);
	d[s]=1;
	now[s]=head[s];
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		for(int i=head[u];i;i=a[i].nex)
		{
			int v=a[i].to,w=a[i].w;
			if(w>0&&!d[v])
			{
				d[v]=d[u]+1;
				now[v]=head[v];
				if(v==t) return 1;
				q.push(v);
			}
		}
	}
	return 0;
}
int dfs(int u,int flow)
{
	if(u==t) return flow;
	int tmp=flow;
	for(int i=now[u];i&&tmp;i=a[i].nex)
	{
		int v=a[i].to,w=a[i].w;
		now[u]=i;
		if(d[u]+1==d[v]&&w)
		{
			int k=dfs(v,MIN(tmp,w));
			if(!k) d[v]=0;
			a[i].w-=k;
			a[i^1].w+=k;
			tmp-=k;
		}
	}
	return flow-tmp;
}
il int dinic()
{
	int ans=0;
	while(bfs()) ans+=dfs(s,INT_MAX);
	return ans;
}
il void solve()
{
	m=read();n=read();s=0,t=n+1;
	F(i,1,m) add(s,i,1),add(i,s,0);
	F(i,m+1,n) add(i,t,1),add(t,i,0);
	int x,y;
	while(1)
	{
		x=read(),y=read();
		add(x,y,1);
		add(y,x,0);
		if(x==-1&&y==-1) break;
	}
	put(dinic());
	for(int i=2;i<=tot;i+=2) if(a[i].to!=s&&a[i^1].to!=s&&a[i^1].to!=t&&a[i].to!=t) if(a[i^1].w>0) printk(a[i^1].to),put(a[i].to);
}
signed main()
{
	int T=1;
//	T=read();
	while(T--) solve();
    return 0;
}

变形 \(1\)教辅的组成

相当于有三个集合,集合中间连边,每个点只能选一次,求最多可以选多少个三元组。和二分图最大匹配类似。

直接连边跑最大流会导致中间的书点被使用多次,所以考虑将中间点拆点,左连练习册右连答案,容量为 \(1\)

#include<bits/stdc++.h>
#define sd std::
//#define int long long
#define inf 0x3f3f3f3f
#define linf 1e18
#define il inline
#define db double
#define ldb long double
#define F(i,a,b) for(int i=(a);i<=(b);i++)
#define f(i,a,b) for(int i=(a);i>=(b);i--)
#define MIN(x,y) (x<y?x:y)
#define MAX(x,y) (x>y?x:y)
#define me(x,y) memset(x,y,sizeof x)
#define pii sd pair<int,int>
#define umap(x,y) sd unordered_map<x,y>
#define pque(x) sd priority_queue<x>
#define X first
#define Y second
#define kg putchar(' ')
#define Fr(a) for(auto it:a)
#define dbg(x) sd cout<<#x<<": "<<x<<sd endl
il int read(){int w=1,c=0;char ch=getchar();for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;for(;ch>='0'&&ch<='9';ch=getchar()) c=(c<<3)+(c<<1)+ch-48;return w*c;}
void printt(int x){if(x>9) printt(x/10);putchar(x%10+48);}
il void print(int x){if(x<0) putchar('-'),printt(-x);else printt(x);}
il void put(int x){print(x);putchar('\n');}
il void printk(int x){print(x);kg;}
const int N=4e5+10;
struct node
{
	int nex;
	int to;
	int w;
}a[N];
int tot=1,head[N],now[N];
void ad(int u,int v,int w)
{
	a[++tot].nex=head[u];
	head[u]=tot;
	a[tot].to=v;
	a[tot].w=w;
}
void add(int u,int v,int w)
{
	ad(u,v,w);
	ad(v,u,0);
}
int d[N];
int n1,n2,n3,s,t;
int bfs()
{
	sd queue<int> q;
	me(d,0);
	q.push(s);
	d[s]=1;
	now[s]=head[s];
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(int i=head[u];i;i=a[i].nex)
		{
			int v=a[i].to,w=a[i].w;
			if(w>0&&!d[v])
			{
				d[v]=d[u]+1;
				now[v]=head[v];
				if(v==t) return 1;
				q.push(v);
			}
		}
	}
	return 0;
}
int dfs(int u,int flow)
{
	if(u==t) return flow;
	int tmp=flow;
	for(int i=head[u];i&&tmp;i=a[i].nex)
	{
		int v=a[i].to,w=a[i].w;
		now[u]=i;
		if(w&&d[v]==d[u]+1)
		{
			int k=dfs(v,MIN(tmp,w));
			if(!k) d[v]=0;
			a[i].w-=k;
			a[i^1].w+=k;
			tmp-=k;
		}
	}
	return flow-tmp;
}
int dinic()
{
	int ans=0;
	while(bfs()) ans+=dfs(s,INT_MAX);
	return ans;
}
//标号:1~n1 为书,n1+1~n1+n2 为练习册,n1+n2+1~n2+n1+n3 为答案,n2+n1+n3+1~n1*2+n2+n3 为书拆出来的点 
//书左连练习册,右连答案 
il void solve()
{
	n1=read(),n2=read(),n3=read();
	int m1=read();
	F(i,1,m1)
	{
		int x=read(),y=read()+n1;
		add(y,x,1);
	}
	int m2=read();
	F(i,1,m2)
	{
		int x=read()+n1+n2+n3,y=read()+n1+n2;
		add(x,y,1);
	}
	F(i,1,n1) add(i,i+n1+n2+n3,1);
	s=0;t=n1+n1+n2+n3+1;
	F(i,1,n2) add(s,i+n1,1);
	F(i,1,n3) add(i+n1+n2,t,1);
	put(dinic());
}
int main()
{
	int T=1;
//	T=read();
	while(T--) solve();
    return 0;
}

变形 \(2\)HEOI2016/TJOI2016 游戏

这是一个网络流的棋盘模型,即将棋盘转化为二分图最大匹配问题。

先考虑最简单的问题:

每个炸弹会影响其行和列,问最多能放置多少个炸弹。

考虑将行和列分别作为二分图的两端,将每一行每一列都连边。那么问题就变为:

二分图左右部的端点只能匹配一次,问最多能选择多少条边。即这个二分图的最大匹配。

接下来再考虑有障碍(有#)的情况:

发现 # 会将其行和列分成若干个互不影响的小段,所以考虑先将行列分段,再将每一行段向列段连边,列段向汇点连边,行段向源点连边。

#include<bits/stdc++.h>
#define sd std::
#define int long long
#define inf 0x3f3f3f3f
#define linf 1e18
#define il inline
#define db double
#define ldb long double
#define F(i,a,b) for(int i=(a);i<=(b);i++)
#define f(i,a,b) for(int i=(a);i>=(b);i--)
#define MIN(x,y) (x<y?x:y)
#define MAX(x,y) (x>y?x:y)
#define me(x,y) memset(x,y,sizeof x)
#define pii sd pair<int,int>
#define umap(x,y) sd unordered_map<x,y>
#define pque(x) sd priority_queue<x>
#define X first
#define Y second
#define kg putchar(' ')
#define Fr(a) for(auto it:a)
#define dbg(x) sd cout<<#x<<": "<<x<<sd endl
il int read(){int w=1,c=0;char ch=getchar();for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;for(;ch>='0'&&ch<='9';ch=getchar()) c=(c<<3)+(c<<1)+ch-48;return w*c;}
void printt(int x){if(x>9) printt(x/10);putchar(x%10+48);}
il void print(int x){if(x<0) putchar('-'),printt(-x);else printt(x);}
il void put(int x){print(x);putchar('\n');}
il void printk(int x){print(x);kg;}
const int N=60,M=114514;
struct node
{
	int nex;
	int to;
	int w;
}a[M];
int tot=1,head[M],now[M];
void ad(int u,int v,int w)
{
	a[++tot].nex=head[u];
	head[u]=tot;
	a[tot].to=v;
	a[tot].w=w;
}
void add(int u,int v,int w)
{
	ad(u,v,w);
	ad(v,u,0);
}
int d[M],n,m,s,t;
int bfs()
{
	me(d,0);
	sd queue<int> q;
	now[s]=head[s];
	d[s]=1;
	q.push(s);
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(int i=head[u];i;i=a[i].nex)
		{
			int v=a[i].to,w=a[i].w;
			if(w&&!d[v])
			{
				d[v]=d[u]+1;
				now[v]=head[v];
				q.push(v);
				if(v==t) return 1;
			}
		}
	}
	return 0;
}
int dfs(int u,int flow)
{
	if(u==t||!flow) return flow;
	int tmp=flow;
	for(int i=now[u];i;i=a[i].nex)
	{
		now[u]=i;
		int v=a[i].to,w=a[i].w;
		if(w&&d[v]==d[u]+1)
		{
			int k=dfs(v,MIN(tmp,w));
			if(!k) d[v]=0;
			a[i].w-=k;
			a[i^1].w+=k;
			tmp-=k;
		}
	}
	return flow-tmp;
}
int dinic()
{
	int ans=0;
	while(bfs()) ans+=dfs(s,INT_MAX);
	return ans;
}
int r,c;
char b[N][N];
int coh[N][N],col[N][N];
int num;//给每一个行段列段都搞个编号 
il void solve()
{
	r=read(),c=read();
	s=0,t=r*c+1;
	F(i,1,r) F(j,1,c) sd cin>>b[i][j];
	F(i,1,r) F(j,1,c)//给每个行段染色 
	{
		if(b[i][j]=='#')
		{
			num++;
			continue;
		}
		if(j==1) num++;
		coh[i][j]=num;
	}
	F(j,1,c) F(i,1,r)//给每个列段染色 
	{
		if(b[i][j]=='#')
		{
			num++;
			continue;
		}
		if(i==1) num++;
		col[i][j]=num;
	}
	int now=0;
	F(i,1,r) F(j,1,c)
	{
		if(b[i][j]=='#') continue;
		if(coh[i][j]!=now) add(s,coh[i][j],1),now=coh[i][j];//行段和源点连边 
	}
	F(j,1,c) F(i,1,r)
	{
		if(b[i][j]=='#') continue;
		if(col[i][j]!=now) add(col[i][j],t,1),now=col[i][j];//列段和汇点连边 
	}
	F(i,1,r) F(j,1,c) if(b[i][j]=='*') add(coh[i][j],col[i][j],1);
	put(dinic());
}
signed main()
{
	int T=1;
//	T=read();
	while(T--) solve();
    return 0;
}


2.二分图多重匹配

源汇点连接的边容量为其点能够选择的次数,内部连边容量为 \(1\)

3.不相交最小路径覆盖

在一个 DAG 中找出最少的路径使得这些路径经过了所有的点。

最小路径覆盖分为相交不相交,这里的相交指顶点不相交。

考虑将每个点进行拆点,拆成 \(u\)\(u+n\),对于图中 \(u\to v\),即 \(u\to v+n\)

然后图就变成了一个二分图,最终答案为 \(n\) 减去最大匹配数。

对于输出方案,可以记录每个点的后继。

#include<bits/stdc++.h>
#define sd std::
//#define int long long
#define inf 0x3f3f3f3f
#define linf 1e18
#define il inline
#define db double
#define ldb long double
#define F(i,a,b) for(int i=(a);i<=(b);i++)
#define f(i,a,b) for(int i=(a);i>=(b);i--)
#define MIN(x,y) (x<y?x:y)
#define MAX(x,y) (x>y?x:y)
#define me(x,y) memset(x,y,sizeof x)
#define pii sd pair<int,int>
#define umap(x,y) sd unordered_map<x,y>
#define pque(x) sd priority_queue<x>
#define X first
#define Y second
#define kg putchar(' ')
#define Fr(a) for(auto it:a)
#define dbg(x) sd cout<<#x<<": "<<x<<sd endl
il int read(){int w=1,c=0;char ch=getchar();for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;for(;ch>='0'&&ch<='9';ch=getchar()) c=(c<<3)+(c<<1)+ch-48;return w*c;}
void printt(int x){if(x>9) printt(x/10);putchar(x%10+48);}
il void print(int x){if(x<0) putchar('-'),printt(-x);else printt(x);}
il void put(int x){print(x);putchar('\n');}
il void printk(int x){print(x);kg;}
const int N=1e5+10;
struct node
{
	int nex;
	int to;
	int w;
}a[N];
int tot=1,head[N],now[N];
void ad(int u,int v,int w)
{
	a[++tot].nex=head[u];
	head[u]=tot;
	a[tot].to=v;
	a[tot].w=w;
}
void add(int u,int v,int w)
{
	ad(u,v,w);
	ad(v,u,0);
}
int n,m,s,t;
int d[N],ne[N];
int bfs()
{
	me(d,0);
	sd queue<int> q;
	d[s]=1;
	now[s]=head[s];
	q.push(s);
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		for(int i=head[u];i;i=a[i].nex)
		{
			int v=a[i].to,w=a[i].w;
			if(w>0&&!d[v])
			{
				d[v]=d[u]+1;
				now[v]=head[v];
				q.push(v);
				if(v==t) return 1;
			}
		}
	}
	return 0;
}
int dfs(int u,int flow)
{
	if(u==t||!flow) return flow;
	int tmp=flow;
	for(int i=now[u];i;i=a[i].nex)
	{
		now[u]=i;
		int v=a[i].to,w=a[i].w;
		if(w&&d[v]==d[u]+1)
		{
			int k=dfs(v,MIN(w,tmp));
			if(!k) d[v]=0;
			a[i].w-=k;
			a[i^1].w+=k;
			tmp-=k;
			if(k) ne[u]=v-n;
		}
	}
	return flow-tmp;
}
int dinic()
{
	int ans=0;
	while(bfs()) ans+=dfs(s,INT_MAX);
	return ans;
}
int cnt[N];//记录每个点是多少个点的后继 
void out(int x)
{
	printk(x);
	if(ne[x]) out(ne[x]);
}
il void solve()
{
	n=read(),m=read();
	s=0,t=n+n+1;
	F(i,1,m)
	{
		int x=read(),y=read();
		add(x,y+n,1);
	}
	F(i,1,n) add(s,i,1),add(i+n,t,1);
	int ans=n-dinic();
	if(ans==0) return put(0);
	F(i,1,n)
	{
		if(ne[i]>0) cnt[ne[i]]++;
	}
	F(i,1,n) if(!cnt[i]) out(i),puts("");
	put(ans);
}
int main()
{
	int T=1;
//	T=read();
	while(T--) solve();
    return 0;
}

4.可相交路径最小覆盖

将原图用 floyd 进行传递闭包之后,如果存在 \(x\)\(y\),则添加边 \(x\to y\)

然后问题就转化为了不相交路径覆盖。

五、最大流练习题

1. 圆桌问题

link

将每个单位与源点连边,容量为其人数。

将每个餐桌与汇点连边,容量为其容纳人数。

将每个单位与每个餐桌连边,容量为 \(1\),因为每个餐桌仅能容纳一个单位的一个人,即只能流一份。

#include<bits/stdc++.h>
#define sd std::
#define int long long
#define inf 0x3f3f3f3f
#define linf 1e18
#define il inline
#define db double
#define ldb long double
#define F(i,a,b) for(int i=(a);i<=(b);i++)
#define f(i,a,b) for(int i=(a);i>=(b);i--)
#define MIN(x,y) (x<y?x:y)
#define MAX(x,y) (x>y?x:y)
#define me(x,y) memset(x,y,sizeof x)
#define pii sd pair<int,int>
#define umap(x,y) sd unordered_map<x,y>
#define pque(x) sd priority_queue<x>
#define X first
#define Y second
#define kg putchar(' ')
#define Fr(a) for(auto it:a)
#define dbg(x) sd cout<<#x<<": "<<x<<sd endl
il int read(){int w=1,c=0;char ch=getchar();for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;for(;ch>='0'&&ch<='9';ch=getchar()) c=(c<<3)+(c<<1)+ch-48;return w*c;}
void printt(int x){if(x>9) printt(x/10);putchar(x%10+48);}
il void print(int x){if(x<0) putchar('-'),printt(-x);else printt(x);}
il void put(int x){print(x);putchar('\n');}
il void printk(int x){print(x);kg;}
const int N=1e5+10;
struct node
{
	int nex;
	int to;
	int w;
}a[N];
int tot=1,head[N],now[N];
void ad(int u,int v,int w)
{
	a[++tot].nex=head[u];
	head[u]=tot;
	a[tot].to=v;
	a[tot].w=w;
}
void add(int u,int v,int w)
{
	ad(u,v,w);
	ad(v,u,0);
} 
int d[N];
int n,m,s,t;
int bfs()
{
	me(d,0);
	sd queue<int> q;
	q.push(s);
	now[s]=head[s];
	d[s]=1;
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(int i=head[u];i;i=a[i].nex)
		{
			int v=a[i].to,w=a[i].w;
			if(w&&!d[v])
			{
				d[v]=d[u]+1;
				now[v]=head[v];
				q.push(v);
				if(v==t) return 1;
			}
		}
	}
	return 0;
}
int dfs(int u,int flow)
{
	if(u==t||!flow) return flow;
	int tmp=flow;
	for(int i=now[u];i;i=a[i].nex)
	{
		now[u]=i;
		int v=a[i].to,w=a[i].w;
		if(d[v]==d[u]+1&&w)
		{
			int k=dfs(v,MIN(tmp,w));
			if(!k) d[v]=0;
			a[i].w-=k;
			a[i^1].w+=k;
			tmp-=k;
		}
	}
	return flow-tmp;
}
int dinic()
{
	int ans=0;
	while(bfs()) ans+=dfs(s,INT_MAX);
	return ans;
}
int r,c;
//编号:单位:1~n 
il void solve()
{
	r=read(),c=read();
	s=0,t=r+c+1;
	F(i,1,r)
	{
		int x=read();
		n+=x;
		add(s,i,x);
	}
	F(i,1,c)
	{
		int x=read();
		add(i+r,t,x);
	}
	F(i,1,r)
	{
		F(j,1,c)
		{
			add(i,j+r,1);
		}
	}
	int ans=dinic();
	if(ans==n)
	{
		put(1);
		F(u,1,r)
		{
			for(int i=head[u];i;i=a[i].nex)
			{
				int v=a[i].to,w=a[i].w;
				if(a[i^1].w) printk(v-r);
			}
			puts("");
		}
	}
	else return put(0);
}
signed main()
{
	int T=1;
//	T=read();
	while(T--) solve();
    return 0;
}

2.[SCOI2007] 蜥蜴

link

考虑将起点与源点连接,容量 \(1\) 因为只有一个蜥蜴。能够跳出去的点(地图边缘)与汇点连接,容量为 inf 因为可以从这个点出去无数个。

#include<bits/stdc++.h>
#define sd std::
#define int long long
#define inf 0x7f7f7f7f
#define linf 1e18
#define il inline
#define db double
#define ldb long double
#define F(i,a,b) for(int i=(a);i<=(b);i++)
#define f(i,a,b) for(int i=(a);i>=(b);i--)
#define MIN(x,y) (x<y?x:y)
#define MAX(x,y) (x>y?x:y)
#define me(x,y) memset(x,y,sizeof x)
#define pii sd pair<int,int>
#define umap(x,y) sd unordered_map<x,y>
#define pque(x) sd priority_queue<x>
#define X first
#define Y second
#define kg putchar(' ')
#define Fr(a) for(auto it:a)
#define dbg(x) sd cout<<#x<<": "<<x<<sd endl
il int read(){int w=1,c=0;char ch=getchar();for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;for(;ch>='0'&&ch<='9';ch=getchar()) c=(c<<3)+(c<<1)+ch-48;return w*c;}
void printt(int x){if(x>9) printt(x/10);putchar(x%10+48);}
il void print(int x){if(x<0) putchar('-'),printt(-x);else printt(x);}
il void put(int x){print(x);putchar('\n');}
il void printk(int x){print(x);kg;}
const int N=510,M=1e5+10;
char c[N][N],h[N][N];
struct node
{
	int nex;
	int to;
	int w;
}a[M];
int tot=1,head[M],now[M];
void ad(int u,int v,int w)
{
	a[++tot].nex=head[u];
	head[u]=tot;
	a[tot].to=v;
	a[tot].w=w;
}
void add(int u,int v,int w)
{
	ad(u,v,w);
	ad(v,u,0);
}
int d[M];
int n,s,t,r,l,D;
int H(int x,int y){return (x-1)*l+y;}//获取编号 
void insert(int u,int x,int y)//将u与(x,y)连边 
{
	if(x<1||y<1||x>r||y>l) return add(u,t,inf);
	int v=H(x,y);
	if(h[x][y]=='0') return;
	return add(u,v,inf);
}
int bfs()
{
	me(d,0);
	sd queue<int> q;
	q.push(s);
	d[s]=1;
	now[s]=head[s];
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(int i=head[u];i;i=a[i].nex)
		{
			int v=a[i].to,w=a[i].w;
			if(w&&!d[v])
			{
				d[v]=d[u]+1;
				now[v]=head[v];
				q.push(v);
				if(v==t) return 1;
			}
		}
	}
	return 0;
}
int dfs(int u,int flow)
{
	if(u==t||!flow) return flow;
	int tmp=flow;
	for(int i=now[u];i;i=a[i].nex)
	{
		now[u]=i;
		int v=a[i].to,w=a[i].w;
		if(w&&d[v]==d[u]+1)
		{
			int k=dfs(v,MIN(tmp,w));
			if(!k) d[v]=0;
			a[i].w-=k;
			a[i^1].w+=k;
			tmp-=k;
		}
	}
	return flow-tmp;
}
int dinic()
{
	int ans=0;
	while(bfs()) ans+=dfs(s,INT_MAX);
	return ans;
}
int cnt=0;
void link(int i,int j)
{
	if(h[i][j]=='0') return;
	if(c[i][j]=='L') add(s,H(i,j),1),cnt++;
	add(H(i,j),H(i,j)+r*l,h[i][j]-'0');
	if(i==1||j==1||i==r||j==l) add(H(i,j)+l*r,t,inf);
	int p=H(i,j)+r*l;
	if(D>=1)
	{
		insert(p,i-1,j);insert(p,i+1,j);
		insert(p,i,j-1);insert(p,i,j+1);
	}
	if(D>=2)
	{
		insert(p,i-2,j);insert(p,i+2,j);
		insert(p,i,j-2);insert(p,i,j+2);
		insert(p,i-1,j-1);insert(p,i-1,j+1);
		insert(p,i+1,j-1);insert(p,i+1,j+1);
	}
	if(D>=3)
	{
		insert(p,i-3,j);insert(p,i+3,j);insert(p,i,j-3);insert(p,i,j+3);
		insert(p,i-2,j-2);insert(p,i-2,j+2);insert(p,i+2,j-2);insert(p,i+2,j+2);
		insert(p,i-1,j-2);insert(p,i-1,j+2);insert(p,i+1,j-2);insert(p,i+1,j+2);
		insert(p,i-2,j-1);insert(p,i-2,j+1);insert(p,i+2,j-1);insert(p,i+2,j+1);
	}
	if(D>=4)
	{
		insert(p,i-4,j);insert(p,i+4,j);insert(p,i,j-4);insert(p,i,j+4);
		insert(p,i-1,j-3);insert(p,i-1,j+3);insert(p,i+1,j-3);insert(p,i+1,j+3);
		insert(p,i-3,j-1);insert(p,i-3,j+1);insert(p,i+3,j-1);insert(p,i+3,j+1);
		insert(p,i-3,j-2);insert(p,i-3,j+2);insert(p,i+3,j-2);insert(p,i+3,j+2);
		insert(p,i-2,j-3);insert(p,i-2,j+3);insert(p,i+2,j-3);insert(p,i+2,j+3);
	}
}
il void solve()
{
	r=read(),l=read(),D=read();
	s=0,t=l*r+l*r+1;
	F(i,1,r) F(j,1,l) sd cin>>h[i][j];
	F(i,1,r) F(j,1,l) sd cin>>c[i][j];
	F(i,1,r) F(j,1,l) link(i,j);
	put(cnt-dinic());
}
signed main()
{
	int T=1;
//	T=read();
	while(T--) solve();
    return 0;
}

六、最小割

给定一个网络 \(G=(V,E)\),源点 \(S\),汇点 \(T\)。若一个边集 \(E'\subseteq E\) 被删去后,源点与汇点不再联通,则称该边集为网络的割。边的容量之和最小的割被称为网络的最小割。

最大流最小割定理:任何一个网络的最大流都等于最小割。

如何求解方案?

求出最大流之后,从源点 \(S\) 开始沿残量网络 bfs,标记能到大的点

七、最小割经典模型

1.最大权闭合图

闭合图:指有向图中的一个点集 \(E'\),满足 \(\forall x\in E',(x,y)\in V\),都有 \(y\in E'\),即从集合的任意点出发,能够到达的点都在此点集中。

最大权闭合图:点权和最小的闭合图

新建一个源点 \(S\),向所有正点权的点连一条边,容量为其点权。

新建一个汇点 \(T\),向所有负点权的点连一条边,容量为其点权绝对值。

无向图原边的流量为 inf。

定理:最大权闭合图=所有正点权之和-最小割。

例题:太空飞行计划问题

link

把实验所获得的费用和仪器的费用看作其点权(发现仪器的费用对最终结果的影响是负数,即其点权是负数),实验需要的仪器看作实验向仪器连边。

发现如果要选择此实验,那么除获得其费用外,它所指向的所有仪器都要选择。

则题目转化为:

一个 DAG,每个点有一个正/负点权,如果选择一个点那么其后继都要选择。问最大能选择多少点权。

这不就是最大权闭合图吗?

然后照着模型跑最小割,用所有正权点即所有实验所获得的费用和减去最小割就好了。

#include<bits/stdc++.h>
#define sd std::
//#define int long long
#define inf 1e9
#define linf 1e18
#define il inline
#define db double
#define ldb long double
#define F(i,a,b) for(int i=(a);i<=(b);i++)
#define f(i,a,b) for(int i=(a);i>=(b);i--)
#define MIN(x,y) (x<y?x:y)
#define MAX(x,y) (x>y?x:y)
#define me(x,y) memset(x,y,sizeof x)
#define pii sd pair<int,int>
#define umap(x,y) sd unordered_map<x,y>
#define pque(x) sd priority_queue<x>
#define X first
#define Y second
#define kg putchar(' ')
#define Fr(a) for(auto it:a)
#define dbg(x) sd cout<<#x<<": "<<x<<sd endl
il int read(){int w=1,c=0;char ch=getchar();for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;for(;ch>='0'&&ch<='9';ch=getchar()) c=(c<<3)+(c<<1)+ch-48;return w*c;}
void printt(int x){if(x>9) printt(x/10);putchar(x%10+48);}
il void print(int x){if(x<0) putchar('-'),printt(-x);else printt(x);}
il void put(int x){print(x);putchar('\n');}
il void printk(int x){print(x);kg;}
const int N=1e6+10;
struct node
{
	int nex;
	int to;
	int w;
}a[N];
int tot=1,head[N],now[N];
void ad(int u,int v,int w)
{
	a[++tot].nex=head[u];
	head[u]=tot;
	a[tot].to=v;
	a[tot].w=w;
}
void add(int u,int v,int w)
{
	ad(u,v,w);
	ad(v,u,0);
}
int d[N],n,m,s,t;
int bfs()
{
	me(d,0);
	sd queue<int> q;
	d[s]=1;
	now[s]=head[s];
	q.push(s);
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(int i=head[u];i;i=a[i].nex)
		{
			int v=a[i].to,w=a[i].w;
			if(w&&!d[v])
			{
				d[v]=d[u]+1;
				now[v]=head[v];
				q.push(v);
				if(v==t) return 1;
			}
		}
	}
	return 0;
}
int dfs(int u,int flow)
{
	if(u==t||!flow) return flow;
	int tmp=flow;
	for(int i=now[u];i;i=a[i].nex)
	{
		now[u]=i;
		int v=a[i].to,w=a[i].w;
		if(w&&d[v]==d[u]+1)
		{
			int k=dfs(v,MIN(tmp,w));
			if(!k) d[v]=0;
			a[i].w-=k;
			a[i^1].w+=k;
			tmp-=k;
		}
	}
	return flow-tmp;
}
int dinic()
{
	int ans=0;
	while(bfs()) ans+=dfs(s,INT_MAX);
	return ans;
}
//编号:1~m:实验 m+1~n+m:仪器 
sd vector<int> p[N];
sd priority_queue<int,sd vector<int>,sd greater<int>> qq,pp;
il void solve()
{
	m=read(),n=read();
	s=0,t=n+m+1;
	int ans=0;
	F(i,1,m)
	{
		int x=read();
		ans+=x;
		add(s,i,x);
		int c=0;
		char ch=getchar();
		while(ch!='\n')
		{
			if(ch==' ')
			{
				p[i].emplace_back(c);
				add(i,c+m,inf);
				c=0;
				ch=getchar();
				continue;
			}
			c=(c<<3)+(c<<1)+ch-48;
			ch=getchar();
		}
		p[i].emplace_back(c);
		add(i,c+m,inf);
	}
	F(i,1,n)
	{
		int x=read();
		add(i+m,t,x);
	}
	ans-=dinic();
//	for(int i=head[s];i;i=a[i].nex)
//	{
//		int v=a[i].to;
//		pp.push(v);
//		Fr(p[v]) qq.push(it);
//	}
//	while(!pp.empty()) printk(pp.top()),pp.pop();
//	puts("");
//	int last=0;
//	while(!qq.empty())
//	{
//		if(qq.top()!=last) printk(qq.top());
//		last=qq.top();
//		qq.pop();
//	}
//	puts("");
	F(i,1,m) if(d[i]) printk(i);
	puts("");
	F(i,1,n) if(d[i+m]) printk(i);
	puts("");
	return put(ans);
}
int main()
{
	int T=1;
//	T=read();
	while(T--) solve();
    return 0;
}

2.最小点权覆盖集

即求解出满足以下条件中点权和最小的点集:对于图中任意一条边连接的两个点,都有一个点在此点集中。

二分图中,当所有点权为 \(1\) 时,最小点权覆盖集=最大匹配,最大点权独立集= \(n -\)最小点权覆盖集。

这里讨论最小点权覆盖集与最大点权独立集均为二分图的情况。

首先对图进行二分染色,将点分为左部分和右部分。新建源点 \(S\) 和汇点 \(T\)

\(S\) 向左部节点连边,容量为边权,右部向 \(T\) 连边,容量为点权,图中原有边的容量为无限大,对新图求解最小割就是最小点权覆盖集。

简单来说,在二分图上,最小点权覆盖集=最小割。

3.最大点权独立集

在图 \(G=(V,E)\) 中选择一个点集 \(P\),满足 \(\forall (u,v)\in E,u\in P\),都有 \(v\not \in P\),并最大化 \(P\) 中元素的点权和,即每条边的两个点中只能选择一个点。

简单来说,在二分图上,最大点权独立集=点权和-最小点全覆盖集。

例题:方格取数问题

link

题意:一个方格,当选取 \(x\) 后,其上下左右点均无法选择,求选出的最大点权。

考虑将一个点与它上下左右相连,这样就是每一条边只能选择一个点。

染色之后跑最小割。

但是其实是不需要染色的,因为每个点都与它上下左右连边,染色跑出来是有特殊性的,可以直接连。

但我染完色输出之后才发现的。。。。

#include<bits/stdc++.h>
#define sd std::
//#define int long long
#define inf 0x3f3f3f3f
#define linf 1e18
#define il inline
#define db double
#define ldb long double
#define F(i,a,b) for(int i=(a);i<=(b);i++)
#define f(i,a,b) for(int i=(a);i>=(b);i--)
#define MIN(x,y) (x<y?x:y)
#define MAX(x,y) (x>y?x:y)
#define me(x,y) memset(x,y,sizeof x)
#define pii sd pair<int,int>
#define umap(x,y) sd unordered_map<x,y>
#define pque(x) sd priority_queue<x>
#define X first
#define Y second
#define kg putchar(' ')
#define Fr(a) for(auto it:a)
#define dbg(x) sd cout<<#x<<": "<<x<<sd endl
il int read(){int w=1,c=0;char ch=getchar();for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;for(;ch>='0'&&ch<='9';ch=getchar()) c=(c<<3)+(c<<1)+ch-48;return w*c;}
void printt(int x){if(x>9) printt(x/10);putchar(x%10+48);}
il void print(int x){if(x<0) putchar('-'),printt(-x);else printt(x);}
il void put(int x){print(x);putchar('\n');}
il void printk(int x){print(x);kg;}
const int N=110,M=1e6+10;
struct node
{
	int nex;
	int to;
	int w;
}a[M<<1],e[M<<1];
int tot=1,head[M],now[M];
void ad(int u,int v,int w)
{
	a[++tot].nex=head[u];
	head[u]=tot;
	a[tot].to=v;
	a[tot].w=w;
}
void add(int u,int v,int w)
{
	ad(u,v,w);
	ad(v,u,0);
}
int ot,ead[M];
void add_e(int u,int v)
{
	e[++ot].nex=ead[u];
	ead[u]=ot;
	e[ot].to=v;
} 
int d[M],n,m,s,t;
int bfs()
{
	me(d,0);
	sd queue<int> q;
	d[s]=1;
	now[s]=head[s];
	q.push(s);
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(int i=head[u];i;i=a[i].nex)
		{
			int v=a[i].to,w=a[i].w;
			if(w&&!d[v])
			{
				d[v]=d[u]+1;
				now[v]=head[v];
				q.push(v);
				if(v==t) return 1;
			}
		}
	}
	return 0;
}
int dfs(int u,int flow) 
{
	if(!flow||u==t) return flow;
	int tmp=flow;
	for(int i=now[u];i&&tmp;i=a[i].nex)
	{
		now[u]=i;
		int v=a[i].to,w=a[i].w;
		if(w&&d[v]==d[u]+1)
		{
			int k=dfs(v,MIN(tmp,w));
			if(!k) d[v]=0;
			a[i].w-=k;
			a[i^1].w+=k;
			tmp-=k;
		}
	}
	return flow-tmp;
}
int dinic()
{
	int ans=0;
	while(bfs()) ans+=dfs(s,INT_MAX);
	return ans;
}
int r,c;
int b[N][N];//记录点权 
int H(int i,int j)
{
	return (i-1)*c+j;
}
int color[M],vis[M];//color[i]为1为白,否则为黑 
void draw(int u,int fa)//dfs染色 
{
//	printf("目前点为(%d,%d)\n",u%c==0?u/c:u/c+1,u%c==0?c:u%c);
	for(int i=ead[u];i;i=e[i].nex)
	{
		int v=e[i].to;
//		printf("到达点为(%d,%d)\n",v%c==0?v/c:v/c+1,v%c==0?c:v%c);
		if(v==fa||vis[v]) continue;
		vis[v]=1;
//		printf("真正到达点为(%d,%d)\n",v%c==0?v/c:v/c+1,v%c==0?c:v%c);
		color[v]=!color[u];
		draw(v,u);
	}
}
int dx[5]={-1,1,0,0};
int dy[5]={0,0,-1,1};
int sum;
il void solve()
{
	r=read(),c=read();
	s=0,t=r*c+1;
	F(i,1,r) F(j,1,c) b[i][j]=read(),sum+=b[i][j];
	F(i,1,r) F(j,1,c) F(k,0,3)
	{
		int xx=i+dx[k];
		int yy=j+dy[k];
		if(xx<1||xx>r||yy<1||yy>c) continue;
		add_e(H(i,j),H(xx,yy));
//		printf("(%d,%d)与(%d,%d)连边\n",i,j,xx,yy);
	}
	vis[H(1,1)]=1;
	draw(H(1,1),-1);
	F(i,1,r) F(j,1,c)
	{
		if(color[H(i,j)]==1) add(s,H(i,j),b[i][j]);
		else add(H(i,j),t,b[i][j]);
	}
	F(i,1,r) F(j,1,c) F(k,0,3)
	{
		int xx=i+dx[k];
		int yy=j+dy[k];
		if(xx<1||xx>r||yy<1||yy>c) continue;
		if(color[H(i,j)]) add(H(i,j),H(xx,yy),inf);
		else add(H(xx,yy),H(i,j),inf);
	}
	put(sum-dinic());
}
int main()
{
	int T=1;
//	T=read();
	while(T--) solve();
    return 0;
}

八、最小割练习题

1.[SHOI2007] 善意的投票 / [JLOI2010] 冠军调查

link

先设定一个条件:\(A\to B\) 的边表示 A 和 B 同立场。

假设源点的立场为 \(0\),将源点和满足意愿 \(0\) 的点连边,容量 \(1\)

假设汇点的立场为 \(1\),将汇点和满足意愿 \(1\) 的点连边,容量 \(1\)

发现朋友之间如果没有连边的话条件是成立的。

但加入了朋友怎么办?

假设只有一组朋友意愿为 \(1\)\(0\),根据贪心肯定不会选择最劣的 \(0\)\(1\)(三次冲突)。

那么就剩下几种情况:

  • 让第一个朋友违背自己意愿,这样可以避免违背朋友的冲突。

  • 让第二个朋友违背自己意愿,这样可以避免违背朋友的冲突。

  • 两人都遵循自己的意愿,这样可以避免违背自己意愿的冲突。

那么考虑在网络流中加边,即将朋友双向连边。

如果两个朋友意愿一样不会影响答案。

如果意愿不一样,那么两者连边后就不满足条件了,需要删边。

如果选择删除源点/汇点到一个点的边,那么就相当于源点/汇点和此点的立场不再一样,即选择违背自己意愿。

如果选择删除两个朋友之间的边,说明朋友之间的立场不再一样,即选择违背朋友意愿。

总结一下,删除一条边即选择了发生这种冲突,要使得冲突最小就是最小割。

#include<bits/stdc++.h>
#define sd std::
#define int long long
#define inf 0x3f3f3f3f
#define linf 1e18
#define il inline
#define db double
#define ldb long double
#define F(i,a,b) for(int i=(a);i<=(b);i++)
#define f(i,a,b) for(int i=(a);i>=(b);i--)
#define MIN(x,y) (x<y?x:y)
#define MAX(x,y) (x>y?x:y)
#define me(x,y) memset(x,y,sizeof x)
#define pii sd pair<int,int>
#define umap(x,y) sd unordered_map<x,y>
#define pque(x) sd priority_queue<x>
#define X first
#define Y second
#define kg putchar(' ')
#define Fr(a) for(auto it:a)
#define dbg(x) sd cout<<#x<<": "<<x<<sd endl
il int read(){int w=1,c=0;char ch=getchar();for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;for(;ch>='0'&&ch<='9';ch=getchar()) c=(c<<3)+(c<<1)+ch-48;return w*c;}
void printt(int x){if(x>9) printt(x/10);putchar(x%10+48);}
il void print(int x){if(x<0) putchar('-'),printt(-x);else printt(x);}
il void put(int x){print(x);putchar('\n');}
il void printk(int x){print(x);kg;}
const int N=1e6+10;
struct node
{
	int nex;
	int to;
	int w;
}a[N];
int tot=1,head[N],now[N];
void ad(int u,int v,int w)
{
	a[++tot].nex=head[u];
	head[u]=tot;
	a[tot].to=v;
	a[tot].w=w;
}
void add(int u,int v,int w)
{
	ad(u,v,w);
	ad(v,u,0);
}
int d[N],n,m,s,t;
int bfs()
{
	me(d,0);
	sd queue<int> q;
	d[s]=1;
	now[s]=head[s];
	q.push(s);
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(int i=head[u];i;i=a[i].nex)
		{
			int v=a[i].to,w=a[i].w;
			if(!d[v]&&w)
			{
				d[v]=d[u]+1;
				now[v]=head[v];
				q.push(v);
				if(v==t) return 1;
			}
		}
	}
	return 0;
}
int dfs(int u,int flow)
{
	if(u==t||!flow) return flow;
	int tmp=flow;
	for(int i=head[u];i;i=a[i].nex)
	{
		now[u]=i;
		int v=a[i].to,w=a[i].w;
		if(d[v]==d[u]+1&&w)
		{
			int k=dfs(v,MIN(tmp,w));
			if(!k) d[v]=0;
			a[i].w-=k;
			a[i^1].w+=k;
			tmp-=k;
		}
	}
	return flow-tmp;
}
int dinic()
{
	int ans=0;
	while(bfs()) ans+=dfs(s,INT_MAX);
	return ans;
}
il void solve()
{
	n=read(),m=read();s=0,t=n+1;
	F(i,1,n)
	{
		int x=read();
		if(!x) add(s,i,1);
		else add(i,t,1);
	}
	F(i,1,m)
	{
		int x=read(),y=read();
		add(x,y,1);
		add(y,x,1);
	}
	put(dinic());
}
signed main()
{
	int T=1;
//	T=read();
	while(T--) solve();
    return 0;
}

2.[TJOI2015] 线性代数

假线性代数真网络流。

先将式子展开,会有点麻烦:

设:

\[A=\begin{bmatrix} a_{1,1} & a_{1,2} & \cdots &a_{1,n} \end{bmatrix}\]

\[B=\begin{bmatrix} b_{1,1} & b_{1,2} & \cdots & b_{1,n}\\ b_{2,1} & b_{2,2} & \cdots & b_{2,n}\\ \cdots & \cdots & \cdots & \cdots\\ b_{n,1} & b_{n,2} & \cdots & b_{n,n} \end{bmatrix}\]

则:

\[AB=\begin{bmatrix} \sum \limits _{i=1}^n a_{1,i}\times b_{i,1} & \sum \limits _{i=1}^n a_{1,i}\times b_{i,2}& \cdots & \sum \limits _{i=1}^n a_{1,i}\times b_{i,n} \end{bmatrix}\]

因为式子太长了,所以令 \(f(i)=\sum \limits _{j=1}^n a_{1,j}\times b_{j,i}\)

因为矩阵乘法满足分配律,将外面的 \(A^T\) 乘进来,发现是一个 \(1\times 1\) 的矩阵,也就是矩阵内只有一个元素:

\[AB\times A^T= f(1)\times a_{1,1}+f(2)\times a_{1,2}+\cdots +f(n)\times a_{1,n} \]

\[=\sum\limits_{i=1}^n f(i)\times a_{1,i} \]

\[=\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{n}a_{1,j}\times b_{i,j}\times a_{1,i} \]

再把式子里的第二个多项式展开:

设:

\[C=\begin{bmatrix} c_{1,1} & c_{1,2} & \cdots &c_{1,n} \end{bmatrix}\]

则:

\[C\times A^T=\sum_{i=1}^na_{1,i}\times c_{1,i} \]

那么原式为:

\[(A\times B-C)\times A^T \]

\[= AB\times A^T-C\times A^T \]

\[=\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{n}a_{1,j}\times b_{i,j}\times a_{1,i}-\sum_{i=1}^na_{1,i}\times c_{1,i} \]

问题转化为:\(b_{i,j}\)\(c_{i,j}\) 均已知(都为非负整数),求这个式子最大值。

因为 \(\forall 1\le i\le n,A_{1,i}\in\{1,0\}\),则问题其实就是选择一些 \(b_{i,j}\) 要一些不要,同时 \(b_{i,j}\)\(c_{i,j}\) 有限制关系。

发现如果选择要 \(b_{i,j}\) 的话,那么 \(a_{1,i}\) 以及 \(a_{1,j}\) 就必须为 \(1\),那么就必须要选择 \(c_{1,i}\) 以及 \(c_{1,j}\)

如果将 \(b_{i,j}\)\(c_{1,i}\)\(c_{1,i}\) 连边,将 \(b_{i,j}\)\(c_{1,i}\) 这些看作其点权(\(c_{1,i}\) 的点权需要看作负数因为在式子中对答案的影响是负的),那么其实就是当一个点选择时,其后继必须选择,求最大点权。

这不就是最大权闭合图模型吗?照着跑最小割就行。

#include<bits/stdc++.h>
#define sd std::
#define int long long
#define inf 1e9+1e5
#define linf 1e18
#define il inline
#define db double
#define ldb long double
#define F(i,a,b) for(int i=(a);i<=(b);i++)
#define f(i,a,b) for(int i=(a);i>=(b);i--)
#define MIN(x,y) (x<y?x:y)
#define MAX(x,y) (x>y?x:y)
#define me(x,y) memset(x,y,sizeof x)
#define pii sd pair<int,int>
#define umap(x,y) sd unordered_map<x,y>
#define pque(x) sd priority_queue<x>
#define X first
#define Y second
#define kg putchar(' ')
#define Fr(a) for(auto it:a)
#define dbg(x) sd cout<<#x<<": "<<x<<sd endl
il int read(){int w=1,c=0;char ch=getchar();for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;for(;ch>='0'&&ch<='9';ch=getchar()) c=(c<<3)+(c<<1)+ch-48;return w*c;}
void printt(int x){if(x>9) printt(x/10);putchar(x%10+48);}
il void print(int x){if(x<0) putchar('-'),printt(-x);else printt(x);}
il void put(int x){print(x);putchar('\n');}
il void printk(int x){print(x);kg;}
const int N=600*600*3;
struct node
{
	int nex;
	int to;
	int w;
}a[N<<1];
int tot=1,head[N],now[N];
void ad(int u,int v,int w)
{
	a[++tot].nex=head[u];
	head[u]=tot;
	a[tot].to=v;
	a[tot].w=w;
}
void add(int u,int v,int w)
{
	ad(u,v,w);
	ad(v,u,0);
}
int d[N],n,m,s,t;
int H(int i,int j)
{
	return (i-1)*n+j;
}
int bfs()
{
	me(d,0);
	sd queue<int> q;
	d[s]=1;
	now[s]=head[s];
	q.push(s);
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(int i=head[u];i;i=a[i].nex)
		{
			int v=a[i].to,w=a[i].w;
			if(!d[v]&&w)
			{
				d[v]=d[u]+1;
				now[v]=head[v];
				q.push(v);
				if(v==t) return 1;
			}
		}
	}
	return 0;
}
int dfs(int u,int flow)
{
	if(u==t||!flow) return flow;
	int tmp=flow;
	for(int i=now[u];i&&tmp;i=a[i].nex)
	{
		now[u]=i;
		int v=a[i].to,w=a[i].w;
		if(w&&d[v]==d[u]+1)
		{
			int k=dfs(v,MIN(tmp,w));
			if(!k) d[v]=0;
			a[i].w-=k;
			a[i^1].w+=k;
			tmp-=k;
		}
	}
	return flow-tmp;
}
int dinic()
{
	int ans=0;
	while(bfs()) ans+=dfs(s,INT_MAX);
	return ans;
}
//编号:1~n*n:b数组,n*n+1~n*(n+1):c数组 
il void solve()
{
	n=read();
	s=0,t=n*n+n+1;
	int ans=0;
	F(i,1,n)
	{
		F(j,1,n)
		{
			int x=read();ans+=x;
			add(H(i,j),i+n*n,linf);
			add(H(i,j),j+n*n,linf);
			add(s,H(i,j),x);
		}
	}
	F(i,1,n)
	{
		int x=read();
		add(i+n*n,t,x);
	}
	put(ans-dinic());
}
signed main()
{
	int T=1;
//	T=read();
	while(T--) solve();
    return 0;
}

九、费用流

给定一个网络 \(G=(V,E)\),每条边 \((x,y)\) 除容量限制 \(c(x,y)\) 之外还有一个给定的单位费用 \(w(x,y)\)

即如果边的容量是 \(f(x,y)\),则这条边的花费是 \(f(x,y)\times w(x,y)\)。该网络中总花费最小的最大流方案被称作最小费用最大流;总花费最大的最大流被称作最大费用最大流。二者合称为费用流。

注意费用流的前提是最大流。

十、费用流经典模型

1.最小费用最大流

因为最大流一样,将每条边的单位费用看作路径长度,每次增广找最短路,就可以使得费用最小,将 Dinic 算法中的 BFS 更换为 SPFA 跑最短路。

如果求最大费用最大流,将每条边取反跑最小费用最大流。

最小费用最大流模板:


2.二分图最大权匹配

选择一些边,满足任意两条边都没有公共点的边称为二分图的一组匹配。二分图最大权匹配就是找到边权和最大的匹配。

首先建源汇。

\(S\) 向二分图的每个左部点连一条流量为 \(1\),费用为 \(0\) 的边,从二分图的每个右部点向 \(T\) 连一条容量为 \(1\),费用为 \(0\) 的边。

接下来对于二分图中连接左部点 \(u\) 和右部点 \(v\),流量 \(1\) 费用 \(w\)

但直接跑最大流最大费用是错误的,很容易想到会出现最大流方案不是费用最大方案的情况。

解决这个问题就需要确保在最大费用的情况下一定最大流。所以考虑将所有左部点向 \(T\) 连一条流量 \(1\),费用 \(0\) 的边,这样一定满足最大流。

3.最大权不相交路径

和最多不相交路径基本相同。

每条边有一个边权,在不相交路径尽可能多的情况下最大化费用。

将每个点拆点,然后加上费用跑费用流即可。

本文作者:E_M_T

本文链接:https://www.cnblogs.com/E-M-T/articles/18587274

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   _E_M_T  阅读(31)  评论(2编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起