目录
-
最大匹配 匈牙利算法
-
网络流初步:费用流(带权最大匹配)Edmonds-Karp 增广路算法
参考资料
b23.tv
匈牙利算法
b23.tv
费用流
概念
二分图(二部图)的最大匹配:
设 G G G 为二分图 , 若在 $G $ 的子图 $M $ 中 , 任意两条边都没有公共节点 , 那么称 $M $ 为二分图 $G $ 的一组匹配 。 在二分图中 ,包含边数最多的一组匹配称为二分图的最大匹配。
如在下图中,1-4,5-3就是一组匹配。1-4,5-3,2-7就是下图的最大匹配
匈牙利算法
匈牙利算法通过不停地找增广路来增加配边找不到增广路时 , 达到最大匹配 。 可以用 DFS 或 BFS 来实现 。
算法简单,下面结合代码和例题
题目描述
给定一个二分图,其左部点的个数为 n n n,右部点的个数为 m m m,边数为 e e e,求其最大匹配的边数。
左部点从 1 1 1 至 n n n 编号,右部点从 1 1 1 至 m m m 编号。
输出一行一个整数,代表二分图最大匹配的边数。
对于全部的测试点,保证:
-
1 ≤ n , m ≤ 500 1 \leq n, m \leq 500 1≤n,m≤500。
-
1 ≤ e ≤ 5 × 1 0 4 1 \leq e \leq 5 \times 10^4 1≤e≤5×104。
-
1 ≤ u ≤ n 1 \leq u \leq n 1≤u≤n, 1 ≤ v ≤ m 1 \leq v \leq m 1≤v≤m。
不保证给出的图没有重边。
solu
#include<bits/stdc++.h>
using namespace std;
const int N=5e4+5;
int n,m,E,ans;
//int h[N],e[N];
vector <int> e[N];
int vis[N],match[N];
void add(int a,int b){
e[a].push_back(b);
}
bool dfs(int u){
for(int i=0;i<e[u].size();i++){//扫描所有可能成为配对的右点(即有连边的点)
int v=e[u][i];
if(vis[v])continue;//即(在上一层函数中)已经被访问过
vis[v]=1;//不是上一层函数中想要的点,那么这一层就可能可以匹配成功
if(!match[v]||dfs(match[v])){//如果 点v没有任何已有匹配,那么匹配它和u! 或者它有匹配了(与match[v]),但match[v]可以找到另外一个右点和它匹配,那么就可以把点v让给u (这样可以保证已经有的匹配数不减少,可能会变更,但不减少)(贪心)
match[v]=u;
return 1;
}
}
return 0;//扫描全部结束,没有一个匹配成功
}
signed main(){
cin>>n>>m>>E;
for(int i=1;i<=E;i++){
int a,b;
cin>>a>>b;
add(a,b);//我们只通过左边找右边,因此只需要单向边
}
for(int i=1;i<=n;i++){//遍历每个左边的点给它找配对
memset(vis,0,sizeof vis);
if(dfs(i))ans++;//如果找到了配对
}
cout<<ans;
return 0;
}
详细解释以下片段
if(vis[v])continue;//即(在上一层函数中)已经被访问过
vis[v]=1;//不是上一层函数中想要的点,那么这一层就可能可以匹配成功
if(!match[v]||dfs(match[v])){//如果 点v没有任何已有匹配,那么匹配它和u! 或者它有匹配了(与match[v]),但match[v]可以找到另外一个右点和它匹配,那么就可以把点v让给u (这样可以保证已经有的匹配数不减少,可能会变更,但不减少)(贪心)
先忽略vis[],当走到判定处,发现match[v]≠0,那么就要去dfs(match[v]),此时从dfs(u)的函数空间走到了dfs(match[v])的函数空间,这时才会出现vis[v]≠0的情况
如果vis[v]≠1了,就说明在dfs(u)函数空间内这个点已经被预定了,match[v]这个点只能另寻他人
时间复杂度 理论上限O(nm)
题目描述
给定一个二分图,其点的个数为 n n n,边数为 e e e,求其最大匹配的边数。
输入 e e e 条边,保证连成的图为二分图
输出一行一个整数,代表二分图最大匹配的边数。
solu(未验证的)
#include<bits/stdc++.h>
using namespace std;
const int N=5e4+5;
int pleft[N];//0未发现,1为左点,2为右点
int n,ln,E,ans;
//int h[N],e[N];
vector <int> e[N];
int vis[N],match[N];
void add(int a,int b){
e[a].push_back(b);
}
bool dfs(int u){
for(int i=0;i<e[u].size();i++){
int v=e[u][i];
if(vis[v])continue;
vis[v]=1;
if(!match[v]||dfs(match[v])){
match[v]=u;
return 1;
}
}
return 0;
}
signed main(){
cin>>n>>E;
for(int i=1;i<=E;i++){
int a,b;
cin>>a>>b;
if(&&!pletf[b])add(a,b),pletf[b]=2,pletf[a]=1,ln++;
if(pletf[a]==1||pletf[b]==2)
if(!pletf[a])ln++;
add(a,b),pletf[a]=1,pletf[b]=2;
if(pletf[a]==2||pletf[b]==1)
if(!pletf[b])ln++;
add(b,a),pletf[a]=2,pletf[b]=1;
}
// cout<<"OK"<<endl;
for(int i=1;i<=ln;i++){
memset(vis,0,sizeof vis);
if(dfs(i))ans++;
}
cout<<ans;
return 0;
}
费用流
请先复习网络流的内容
EK算法
364 网络流 费用流 EK 算法_哔哩哔哩_bilibili
其中最大流指的是总流量最大,不是单独的一条路径
与EK模板的区别
关于cost的修改
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e4+5;
int mf[N];//最大流量maxflow
int s,t,pre[N],n,m;
int cst,dis[N],vis[N];
struct edge{
int v,c,nxt,w;
}e[20*N];
int h[N],idx=1;//边的id从2开始存,因为边与残留边对应,使用i^1可以迅速在n与n+1之间相互转换(n为偶数),不用特判
void add(int a,int b,int c,int dd){
e[++idx]={b,c,h[a],dd};
h[a]=idx;
}
//***重要
bool spfa(){//找一条可以从s到t的有效路径
memset(dis,0x3f,sizeof dis);//最短路常规操作
dis[s]=0,vis[s]=1;//vis标记其是否在队内
memset(mf,0,sizeof mf);//将每个点能到达的流量上限变成0
queue<int> q;
q.push(s);mf[s]=1e9;//源点的流量上限为无穷大,即源点能为后面提供无限大的流量
while(q.size()){
int u=q.front();q.pop();vis[u]=0;
for(int i=h[u];i;i=e[i].nxt){//扫描出边
int v=e[i].v,w=e[i].w;
if(dis[v]>dis[u]+w&&e[i].c){//如果 目前这条路到v比之前到v的路更短 并且存在这条边/这条边在之前走过但还有空余容量(容量>0)
dis[v]=dis[u]+w;
mf[v]=min(mf[u],e[i].c);//更新流量上限为之前更新过的可以到达u的流量(即点u能提供的最大流量)与u-v见之间的容量的min
pre[v]=i; //存目前路径上点v的前驱边
if(!vis[v])//防止重复入队
q.push(v),vis[v]=1;
//找增广路相当于找到一条新的流量到t,回顾二分图的增光路,之前已经找到的增光路的路径可以调整,但流量不会变化(即不会使之前已经有的流量减小)
}
}
}
if(mf[t])return 1;//如果有流量到达t,说明找到了一条增广路
else return 0;//一定要返回0!没有返回值会当作true处理!
}
//***
int EK(){
int nf=0;//当前总流量nowflow
while(spfa()){//新找到一条增广路,路的流量为mf[t] (流量是从s开始在到达t途中受到限制逐渐减小的,因此到达t的流量才是这条路的流量)
//cout<<'K';
int v=t;
while(v!=s){//从t往回在更新残留网
int i=pre[v];
e[i].c-=mf[t];//主边,空余的容量减少了
e[i^1].c+=mf[t];//残留边(反向边)
//此消彼长
v=e[i^1].v;
}
nf+=mf[t];//汇入一股新的流量
//***重要
cst+=mf[t]*dis[t];
//***
}
return nf;
}
signed main(){
cin>>n>>m>>s>>t;
for(int i=1;i<=m;i++){
int u,v,w,dd;
cin>>u>>v>>w>>dd;
add(u,v,w,dd);add(v,u,0,-dd);
}
cout<<EK()<<' '<<cst<<endl;
}
仅仅以下更改
-
对于cost的统计与相关代码
-
将bfs找增光路变成spfa求最短路
为什么这个判定不能用了?
// use this at spfa最后
if(mf[t])return 1;//如果有流量到达t,说明找到了一条增广路
else return 0;//一定要返回0!没有返回值会当作true处理!
//instead of
if(v==t)return 1;//说明找到了一条增广路
那是因为之前bfs只是要求找到,于是只有一到达汇点就可以判定找到if(v==t)return 1;
,而现在是要求找到最小的。因此只有完整的运行了一次spfa,才能判定有没有到汇点的新增流量
Dinic算法
同EK,将bfs改为spfa即可。
未修改的代码
/*ACACACACACACAC///
Code By Ntsc
/*ACACACACACACAC///
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5;
struct edge{
int v,c,nxt;
}e[N];
int n,m,h[N],ans,idx=1,d[N],s,t,cur[N];
void add(int x,int y,int b){
e[++idx]={y,b,h[x]};
h[x]=idx;
}
bool bfs(){//对每个点进行分层 ,为dfs找增光路做准备
memset(d,0,sizeof d);
queue<int>q;
q.push(s);d[s]=1;//源点是第一层
while(q.size()){
int u=q.front();q.pop();
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].v;
if(!d[v]&&e[i].c){//如果没有访问过v并且这条边有剩余容量
d[v]=d[u]+1;//v点位于u的下一层
q.push(v) ;
if(v==t)return 1;
}
}
}
return 0;
}
int dfs(int u,int mf){//当前点u,(这条路径上)走到u时的剩余流量
//入33 下37 回42 离50
if(u==t)return mf;//如果已经到达汇点,直接返回
int sum=0;//计算u点可以流出的流量之和 (e.g.当u=2时,最后sum=3+3+4+2+3)
for(int i=cur[u];i;i=e[i].nxt){
cur[u]=i;//记录从哪一条边走出去了 ,后面有用
int v=e[i].v;
if(d[v]==d[u]+1&&e[i].c){//如果v在u的下一层 并且有剩余容量
int f=dfs(v,min(mf,e[i].c));//正如EK中的'mf[v]=min(mf[u],e[i].c);'
//回
e[i].c-=f;
e[i^1].c+=f;//更新残留网,对照EK
sum+=f;
mf-=f;//类似八皇后,请思考!
if(!mf)break;//优化.当在u的前几条分支已经流光了u的可用流量时,就不用考虑剩下的分支了
}
}
if(!sum)d[u]=0;//残枝优化.目前这条路没有可行流了
return sum;
}
int dinic(){//累加答案
int ans=0;
while(bfs()){//可以找到增光路
memcpy(cur,h,sizeof h);//请思考!
ans+=dfs(s,1e9);//还是那句话'//源点的流量上限为无穷大,即源点能为后面提供无限大的流量'
}
return ans;
}
signed main(){
cin>>n>>m>>s>>t;
for(int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
add(u,v,w);add(v,u,0);
}
cout<<dinic()<<endl;
return 0;
}
最小费用可行流
最小费用可行流指的是在费用流基础上给每条边的流量设置不仅有上限,还有下限。
见练习 | 这人怎么天天刷题啊’支线剧情