网络流学习笔记
网络流学习笔记
目录:
- 概念汇总
- 网络
- 流
- 最大流:
- FF算法
- EK算法
- 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. 圆桌问题
将每个单位与源点连边,容量为其人数。
将每个餐桌与汇点连边,容量为其容纳人数。
将每个单位与每个餐桌连边,容量为 \(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] 蜥蜴
考虑将起点与源点连接,容量 \(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] 线性代数
假线性代数真网络流。
先将式子展开,会有点麻烦:
设:
则:
因为式子太长了,所以令 \(f(i)=\sum \limits _{j=1}^n a_{1,j}\times b_{j,i}\)。
因为矩阵乘法满足分配律,将外面的 \(A^T\) 乘进来,发现是一个 \(1\times 1\) 的矩阵,也就是矩阵内只有一个元素:
再把式子里的第二个多项式展开:
设:
则:
那么原式为:
问题转化为:\(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 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步