参考文章:
1. 博客园:Dinic算法(研究总结,网络流)
2. 洛谷博客:网络最大流-从入门开始,详细讲到实用易懂的Dinic算法
本文主要是用 Dinic算法 解决最大流问题。
最大流问题,Dinic算法,最坏时间复杂度为O(V2*E),然而一般情况下是达不到这么大的,要不这题早超时了。
算法流程
1、根据残量网络计算层次图。
2、在层次图中使用DFS进行增广直到不存在增广路。
3、重复以上步骤直到无法增广。
时间复杂度
因为在Dinic的执行过程中,每次重新分层,汇点所在的层次是严格递增的,而n个点的层次图最多有n层,所以最多重新分层n次。假设总共有m条边,在同一个层次图中,因为每条增广路都有一个瓶颈,而两次增广的瓶颈不可能相同,所以增广路最多m条。搜索每一条增广路时,前进和回溯都最多n次,所以这两者造成的时间复杂度是O(nm);综上所述,Dinic算法时间复杂度的理论上界是O(n*nm)=O(n2*m)。
简单来说,Dinic算法就是先进行BFS,判断是否有增广路(能从源点s达到汇点t)并且记录图中每个点所在的广度优先搜索生成树的层数,形成分层图;如果有增广路,则进行DFS,限制只能从层数小的向层数大的拓展,如果能找到终点并且返回时流量flow大于0,则每条正向边减去增广路中的最小流量,每条反向边加上增广路中的最小流量(这是算法的关键,引入反向边就是为了“反悔”,或者说“纠正之前可能错误的流量路径”)。
以下的代码,是BFS判断是否有增广路,DFS每次只能搜索出一条增广路。BFS分层之后可以多次循环DFS。
#include
using namespace std;
//在普通情况下,DINIC算法时间复杂度为O(V^2*E)
const int N=1e4+10,M=1e5+10,inf=1e9;
int n,m,s,t,cnt,head[N],dep[N];
struct edge
{
int to,w,next;
}e[M<<1];
void add(int x,int y,int z)
{
e[cnt].to=y;
e[cnt].w=z;
e[cnt].next=head[x];
head[x]=cnt++;
}
void add_edge(int x,int y,int z)
{
add(x,y,z);
add(y,x,0);//反向边初始为0
}
bool bfs()
{
queue<int>q;
memset(dep,0,sizeof(dep));
q.push(s);
dep[s]=1;
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(e[i].w>0&&dep[v]==0)
{
dep[v]=dep[u]+1;
q.push(v);
}
}
}
if(dep[t])return 1;
return 0;
}
int dfs(int u,int flow)
{
if(u==t)return flow;//到达汇点,返回这条增广路上的最小流量
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(e[i].w>0&&dep[v]==dep[u]+1)
{
int di=dfs(v,min(flow,e[i].w));//min(flow,e[i].w)表示起点到v的最小流量
if(di>0)//防止dfs结果return 0的情况,如果di=0会死循环
{
e[i].w-=di;
e[i^1].w+=di;//反向边加上di
return di;//di表示整条增广路上的最小流量,回溯的时候一直向上返回,返回的di是不变的
}
}
}
return 0;//找不到增广路,到不了汇点
}
int dinic()
{
int ans=0;
while(bfs())//先bfs进行“探路”并分层,分层后进行多次dfs
{
//每dfs一次,就尝试找到一条增广路并返回路上的最小流量
while(int d=dfs(s,inf))//while循环的意义:只要dfs不为0,就一直dfs,直到找不到增广路
ans+=d;
}
return ans;
}
int main()
{
ios::sync_with_stdio(false);
cin>>n>>m>>s>>t;
int x,y,z;
memset(head,-1,sizeof(head));
for(int i=1;i<=m;i++)
{
cin>>x>>y>>z;
add_edge(x,y,z);
}
int ans=dinic();
printf("%d\n",ans);
return 0;
}
模板题,但是Dinic算法需要优化才能过这题。
(1)加边优化:题目是双向边,所以加反向边的时候加的容量直接写成与正向边相同(而不是0),这样每次就只加了2条边。
这个加边优化必须要写。
void add_edge(int x,int y,int z)
{
add(x,y,z);
add(y,x,z);//而不是add(y,x,0);
}
(2)优化DFS:
for(int &i=cur[u];i!=-1;i=e[i].next)
dep[u]=-2
//只优化DFS 时间:9516ms
int dfs(int u,int flow)
{
if(u==t||flow==0)return flow;//到达汇点或者是没有流量,就返回流量
int res=0;
for(int &i=cur[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(e[i].w>0&&dep[v]==dep[u]+1)
{
int di=dfs(v,min(flow,e[i].w));//min(flow,e[i].w)表示起点到v的最小流量
if(!di)continue;
e[i].w-=di;
e[i^1].w+=di;//反向边加上di
res+=di;//res相当于从v点流出了多少流量(当前的流出流量作为后面点的供给)
flow-=di;//flow相当于还可以流入多少流量到v点(给当前点的供给)
//多路增广,不直接返回一条弧的流量,而是把所有弧都跑完或者没有流量后再返回res
if(flow==0)break;
}
}
if(res==0)dep[u]=-2;//炸点,使没有流量(不能再向后流出)的点不会再被访问
//不写这句话就会超时!
return res;
}
(3)还有一种玄学优化,就是优化BFS:在BFS中找到汇点就直接结束。(这个就相当于找到终点就不更新分层图了,我感觉好像有点问题,但是几个OJ测了都没问题)
//只优化BFS 时间:8486ms
bool bfs()
{
queue<int>q;
memset(dep,0,sizeof(dep));
q.push(s);
dep[s]=1;
while(!q.empty())
{
int u=q.front();q.pop();
if(u==t)return 1;//这句话非常重要!!!
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(e[i].w>0&&dep[v]==0)
{
dep[v]=dep[u]+1;
q.push(v);
}
}
}
if(dep[t])return 1;
return 0;
}
(4) STL中的queue用数组模拟,能快400ms左右,在这题用处不大。
//BFS,DFS同时优化 未优化STL队列 时间:7675ms
#include
using namespace std;
const int N=1e5+10,M=1e5+10,inf=0x7f7f7f7f;
int T,n,m,s,t,cnt,head[N],dep[N],cur[N];
struct edge
{
int to,w,next;
}e[M<<1];
inline void add(int x,int y,int z)
{
e[cnt].to=y;
e[cnt].w=z;
e[cnt].next=head[x];
head[x]=cnt++;
}
inline void add_edge(int x,int y,int z)
{
add(x,y,z);
add(y,x,z);
}
bool bfs()
{
queue<int>q;
memset(dep,0,sizeof(dep));
q.push(s);
dep[s]=1;
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(e[i].w>0&&dep[v]==0)
{
dep[v]=dep[u]+1;
q.push(v);
}
}
}
if(dep[t])return 1;
return 0;
}
int dfs(int u,int flow)
{
if(u==t||flow==0)return flow;//到达汇点或者是没有流量,就返回流量
int res=0;
for(int &i=cur[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(e[i].w>0&&dep[v]==dep[u]+1)
{
int di=dfs(v,min(flow,e[i].w));//min(flow,e[i].w)表示起点到v的最小流量
if(!di)continue;
e[i].w-=di;
e[i^1].w+=di;//反向边加上di
res+=di;//res相当于从v点流出了多少流量(当前的流出流量作为后面点的供给)
flow-=di;//flow相当于还可以流入多少流量到v点(给当前点的供给)
//多路增广,不直接返回一条弧的流量,而是把所有弧都跑完或者没有流量后再返回res
if(flow==0)break;
}
}
if(res==0)dep[u]=-2;//炸点,使没有流量(不能再向后流出)的点不会再被访问
//不写这句话就会超时!
return res;
}
int dinic()
{
int ans=0;
while(bfs())//先bfs进行“探路”并分层,分层后进行多次dfs
{
for(int i=1;i<=n;i++)//比这个memcpy(cur,head,sizeof(head)) 要快一点
cur[i]=head[i];
while(int d=dfs(s,inf))//while循环的意义:只要dfs不为0,就一直dfs,直到找不到增广路
ans+=d;
}
return ans;
}
int main()
{
ios::sync_with_stdio(false);
cin>>T;
while(T--)
{
cnt=0;
memset(head,-1,sizeof(head));
cin>>n>>m;
int x,y;
int mi=inf,mx=-inf;
for(int i=1;i<=n;i++)
{
cin>>x>>y;
if(x<mi)//找横坐标最小的点,是源点
{
mi=x;
s=i;
}
if(x>mx)//找横坐标最大的点,是汇点
{
mx=x;
t=i;
}
}
int a,b,w;
for(int i=1;i<=m;i++)
{
cin>>a>>b>>w;
add_edge(a,b,w);
}
int ans=dinic();
printf("%d\n",ans);
}
return 0;
}
/*
2
5 7
4 5
3 1
3 3
3 0
0 0
1 3 3
2 3 4
2 4 3
1 5 6
4 5 3
1 4 4
3 4 2
ans:9
*/
我觉得这题的难点就是建图,以及把这个看起来像二分图匹配的问题转化为最大流问题(不看题解,真以为是二分图匹配)。
限制条件:每种食物和水都只能被一头牛用一次,求最多能喂饱多少头牛。
把一头牛拆成两头(你没看错,就是这种拆点的神奇操作),然后建图:食物->牛->牛->饮料,最后再加一个超级源点连上每种食物,加一个超级汇点连上每种饮料,图中的任意一条增广路就变成了:超级源点->食物->牛(左)->牛(右)->饮料->超级汇点。
建立网络流模型:
1.对每种食物建立从源点指向它的一条边,流量为1
2.在牛与它所喜爱的食物间建立一条边,流量为1
3.在牛与它所喜欢的饮料间建立一条边,流量为1
4.对每种饮料建立一条指向汇点的边,流量为1
5.在上面的基础上,将牛拆点,在拆开的点间建立一条流量为1的边
在以上条件下,从源点到汇点的最大流即为答案
为什么要这样建图?原因如下:
模型的分析:
条件1使得满足每种食物有且只有一个,条件4类似
条件2使得每头牛只能选择自己喜欢的食物,条件3类似
条件5使得每头牛最多只能选择一种饮料和食物
还注意一个小细节:对牛的编号、食物编号、饮料编号要进行处理,防止它们编号重合。设食物 f 种,饮料 d 种,牛 n 头,则设左边牛的编号为[1,n]不变,拆点得到的右边牛的编号在原有基础上 +n ,食物编号在原有基础上 +2n ,饮料编号在原有基础上 +2n+f 。源点 s 编号为0,汇点 t 编号为2n+f+d+1。
AC代码:
//#include
#include
#include
#include
#include
#include
using namespace std;
const int N=1e3+10,M=1e5+10,inf=1e9;
bool vis1[N],vis2[N];
int n,s,t,cnt,head[N],dep[N];
struct edge
{
int to,w,next;
}e[M<<1];
void add(int x,int y,int z)
{
e[cnt].to=y;
e[cnt].w=z;
e[cnt].next=head[x];
head[x]=cnt++;
}
void add_edge(int x,int y,int z)//加正向边和初始的反向边
{
add(x,y,z);
add(y,x,0);
}
bool bfs()
{
queue<int>q;
memset(dep,0,sizeof(dep));
q.push(s);
dep[s]=1;
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(e[i].w>0&&dep[v]==0)
{
dep[v]=dep[u]+1;
q.push(v);
}
}
}
if(dep[t])return 1;
return 0;
}
int dfs(int u,int flow)
{
if(u==t)return flow;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(e[i].w>0&&dep[v]==dep[u]+1)
{
int di=dfs(v,min(flow,e[i].w));
if(di>0)
{
e[i].w-=di;
e[i^1].w+=di;
return di;
}
}
}
return 0;
}
int dinic()
{
int ans=0;
while(bfs())
{
while(int d=dfs(s,inf))
ans+=d;
}
return ans;
}
int main()
{
ios::sync_with_stdio(false);
memset(head,-1,sizeof(head));
int f,d,q1,q2,x,y;
cin>>n>>f>>d;
s=0,t=2*n+f+d+1;
for(int i=1;i<=n;i++)
{
cin>>q1>>q2;
int cow1=i;//左边牛的编号
int cow2=i+n;//右边牛的编号(拆点)
while(q1--)
{
cin>>x;//食物
x+=2*n;
if(!vis1[x])
{
add_edge(s,x,1);//源点与食物连边
vis1[x]=1;
}
add_edge(x,cow1,1);//食物与牛连边
}
add_edge(cow1,cow2,1);//牛与牛自身连边
while(q2--)
{
cin>>y;//饮料
y+=2*n+f;
add_edge(cow2,y,1);//牛与饮料连边
if(!vis2[y])
{
vis2[y]=1;
add_edge(y,t,1);//饮料与汇点连边
}
}
}
int ans=dinic();
printf("%d\n",ans);
return 0;
}
题意:
有N台组装电脑的机器。电脑的组成部分共有P部分。
每台机器有P个输入输出规格。还有一个容量Q;
其中输入规格有三种情况:0,1,2
0:该部分不能存在
1:该部分必须保留
2:该部分可有可无
输出规格有2种情况:0,1
0:该部分不存在
1:该部分存在
要求的是生产电脑最大的台数,就是求网络中的最大流。
建图:
最后考虑处理输出边数问题,只要考虑退回边和原边的关系,退回边的权值就是原边流过的流量值,那么遍历所有反向边(边的编号为奇数的),考虑其是否>0,如果是,则说明这条边有流流过,那么对应将其记录下来一起输出即可。
这题wa了无数次,最后发现是输出的顺序错了,写成了原料到产出,答案必须是写产出到原料,我以为这个顺序没关系(If several solutions exist, output any of them. 题意应该是说答案的行的顺序可以互换,没说行的内部顺序可以写反)
AC代码:
#include
#include
#include
#include
#include
using namespace std;
const int N=52,M=1e5+10,P=12,inf=2e9;
int s,t,p,n,w,num,cnt,head[N],dep[N];
int a[N][P],b[N][P];//两个下标是行号和列号
struct node
{
int u,v,w;
};
vector<node>path;
struct edge
{
int from,to,w,next;
}e[M<<1];
void add(int x,int y,int z)
{
e[cnt].from=x;
e[cnt].to=y;
e[cnt].w=z;
e[cnt].next=head[x];
head[x]=cnt++;
}
void add_edge(int x,int y,int z)//加正向边和初始的反向边
{
add(x,y,z);
add(y,x,0);
}
bool bfs()
{
queue<int>q;
memset(dep,0,sizeof(dep));
q.push(s);
dep[s]=1;
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(e[i].w>0&&dep[v]==0)
{
dep[v]=dep[u]+1;
q.push(v);
}
}
}
if(dep[t])return 1;
return 0;
}
int dfs(int u,int flow)
{
if(u==t)return flow;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(e[i].w>0&&dep[v]==dep[u]+1)
{
int di=dfs(v,min(flow,e[i].w));
if(di>0)
{
e[i].w-=di;
e[i^1].w+=di;
return di;
}
}
}
return 0;
}
int dinic()
{
int ans=0;
while(bfs())
{
while(int d=dfs(s,inf))
ans+=d;
}
return ans;
}
bool judge(int numa,int numb)//判断从某个机器的产出能否连到其他机器的原料
{
for(int i=1;i<=p;i++)
{
int x=a[numa][i];
int y=b[numb][i];
if((x==0&&y==1)||(x==1&&y==0))return 0;
}
return 1;
}
int main()
{
ios::sync_with_stdio(false);
cin>>p>>n;
memset(head,-1,sizeof(head));
s=0,t=2*n+1;
for(int i=1;i<=n;i++)
{
cin>>w;
int sum1=0;//统计每个原料结点或者产出结点中1的个数
int numa=i;//原料结点编号
int numb=i+n;//产出结点编号
for(int j=1;j<=p;j++)
{
cin>>a[i][j];
if(a[i][j]==1)sum1++;
}
if(sum1==0)add_edge(s,numa,inf);//将源点与不存在1的原料结点连边
add_edge(numa,numb,w);//将同一机器的原料结点和产出结点连边
sum1=0;
for(int j=1;j<=p;j++)
{
cin>>b[i][j];
if(b[i][j]==1)sum1++;
}
if(sum1==p)add_edge(numb,t,inf);//将全是1的产出结点与汇点连边
}
for(int i=1;i<=n;i++)//原料结点行号
{
for(int j=1;j<=n;j++)//产出结点行号
{
if(i==j)continue;
if(judge(i,j))
{
//printf("link edge j=%d i=%d\n",j,i);
add_edge(j+n,i,inf);//将机器j的产出结点连到机器i的原料结点
}
}
}
int ans=dinic();
printf("%d ",ans);
for(int i=1;i<=cnt;i+=2)//遍历反向边
{
int from=e[i].from;
int to=e[i].to;
int w=e[i].w;
if(w>0&&from>=1&&from<=n&&to>=n+1&&to<=2*n&&from!=to-n)
path.push_back({to-n,from,w});
//就这个地方我写成了path.push_back({from,to-n,w}),wa了无数次!
}
printf("%d\n",path.size());
for(int i=0;i<path.size();i++)
printf("%d %d %d\n",path[i].u,path[i].v,path[i].w);
return 0;
}
/*
3 4
15 0 0 0 0 1 0
10 0 0 0 0 1 1
30 0 1 2 1 1 1
3 0 2 1 1 1 1
ans:
25 3
1 3 15(不能写成3 1 15)
2 3 7
2 4 3
*/
题意:
在一个会议室里有n个插座(可能重复);
每个插座只能插一个电器的插头(或者适配器);
有m个电器,每个电器有一个插头需要插在相应一种插座上;
不是所有电器都能在会议室找到相应插座;
有k种适配器,每种适配器有无限多数量;
每种适配器(s1, s2)可以把s1类插座变为s2类插座;
问最后最少有多少个电器无法使用。
建图:
各个电器各自为一个节点,和源点(不存在,自己构造)相连,且源点到电器的容量为1。
将室内已有的插座各自为一个节点,和汇点(不存在,自己构造)相连,且到汇点的容量为1。
将电器到其应该插的插座类型的点做边,且容量为1。
将适配器(a, b)从a点到b点做边,且边的容量为INF。
需要注意,在输入电器以及输入适配器的过程中,如果遇到室内没有的插座类型,则需要在图中添加新的节点。
样例输入:
4
A
B
C
D
5
laptop B
phone C
pager B
clock B
comb X
3
B X
X A
X D
AC代码:
#include
#include
#include
#include
#include
#include
using namespace std;
const int N=1e4,M=1e5+10,inf=0x7f7f7f7f;
int n,m,k,s,t,cnt,head[N+10],dep[N+10];
map<string,int>vis;//存放电器的插头和提供的插座(包括适配器转换得到的插座)
struct edge
{
int to,w,next;
}e[M<<1];
void add(int x,int y,int z)
{
e[cnt].to=y;
e[cnt].w=z;
e[cnt].next=head[x];
head[x]=cnt++;
}
void add_edge(int x,int y,int z)
{
add(x,y,z);
add(y,x,0);
}
bool bfs()
{
queue<int>q;
memset(dep,0,sizeof(dep));
q.push(s);
dep[s]=1;
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(e[i].w>0&&dep[v]==0)
{
dep[v]=dep[u]+1;
q.push(v);
}
}
}
if(dep[t])return 1;
return 0;
}
int dfs(int u,int flow)
{
if(u==t)return flow;//到达汇点,返回这条增广路上的最小流量
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(e[i].w>0&&dep[v]==dep[u]+1)
{
int di=dfs(v,min(flow,e[i].w));//min(flow,e[i].w)表示起点到v的最小流量
if(di>0)//防止dfs结果return 0的情况,如果di=0会死循环
{
e[i].w-=di;
e[i^1].w+=di;//反向边加上di
return di;//di表示整条增广路上的最小流量,回溯的时候一直向上返回,返回的di是不变的
}
}
}
return 0;//找不到增广路,到不了汇点
}
int dinic()
{
int ans=0;
while(bfs())//先bfs进行“探路”并分层,分层后进行多次dfs
{
//每dfs一次,就尝试找到一条增广路并返回路上的最小流量
while(int d=dfs(s,inf))//while循环的意义:只要dfs不为0,就一直dfs,直到找不到增广路
ans+=d;
}
return ans;
}
int main()
{
ios::sync_with_stdio(false);
string s1,s2;
memset(head,-1,sizeof(head));
s=0,t=N;//源点s,汇点t(汇点编号大于所有点的个数就行)
cin>>n;
int num=0;
for(int i=1;i<=n;i++)//遍历插座(可能有重复名字)
{
cin>>s1;//已提供的插座名字s1
if(!vis[s1])vis[s1]=++num;
//合并相同名字的插座,只留插座第一次出现的名字,其对应的编号为vis[s1]
add_edge(vis[s1],t,1);//插座与汇点连边,容量为1
}
cin>>m;
for(int i=1;i<=m;i++)//遍历插头(可能有重复)
{
cin>>s1>>s2;//电器名s1,电器对应的插头名s2
if(!vis[s2])vis[s2]=++num;
add_edge(s,vis[s2],1);//源点与插头连边,容量为1
}
cin>>k;
for(int i=1;i<=k;i++)
{
cin>>s1>>s2;//插座s1可以转换到插座s2
if(!vis[s1])vis[s1]=++num;//已经存在的名字中找不到s1,则将s1插入
if(!vis[s2])vis[s2]=++num;
//printf("link edge %d %d\n",vis[s1],vis[s2]);
add_edge(vis[s1],vis[s2],inf);//将可以转换的两个插座相连,容量为无限
}
int maxflow=dinic();
printf("%d\n",m-maxflow);
return 0;
}
/*
16
A
D
X
A
D
A
D
X
D
A
D
D
X
D
X
D
14
CLOCK B
CLOCK B
CLOCK B
LAPTOP B
LAPTOP B
LAPTOP B
LAPTOP B
LAPTOP B
LAPTOP B
PAGER B
PAGER B
COMB X
CELL C
CELL C
4
C D
X D
B X
B A
ans:0
*/
解决最大流问题有很多算法,诸如EK,Dinic,ISAP,HLPP等等。
今天看到了一篇很好的文章,比对了各个算法的适用场景和复杂度,可以看一下这篇博客:P4722 -【模板】最大流 加强版 / 预流推进
一般来说,Dinic算法是够用的,但是这个加强版的题数据很毒瘤,得用HLPP。
我先挖个坑,看什么时候HLPP算法给填了
看到一个用Dinic优化了一下卡过去的,(不是我写的)代码:
//C++ 17
#include
using namespace std;
const int maxn=1210,maxm=120010;
struct edge
{
int u,v,cap;
}e[maxm];
struct Dinic
{
int tp,s,t,dis[maxn],cur[maxn],que[maxn];
vector<edge>e;vector<int>v[maxn];
void AddEdge(int x,int y,int flw)
{
e.push_back(edge{x,y,flw});
e.push_back(edge{y,x,0});
v[x].push_back(e.size()-2);
}
int bfs()
{
memset(dis,0x3f,sizeof(dis));
int l=1,r=1;que[1]=s;dis[s]=0;
while(l<=r)
{
int p=que[l++],to;
for(int i:v[p])if(e[i].cap&&dis[to=e[i].v]>1e9)
dis[to]=dis[p]+1,que[++r]=to;
}
return dis[t]<1e9;
}
int dfs(int p,int a)
{
if(p==t||!a)return a;
int sf=0,flw;
for(int &i=cur[p],to;i<(int)v[p].size();++i)
{
edge &E=e[v[p][i]];
if(dis[to=E.v]==dis[p]+1&&(flw=dfs(to,min(a,E.cap))))
{
E.cap-=flw;e[v[p][i]^1].cap+=flw;
a-=flw;sf+=flw;
if(!a)break;
}
}
return sf;
}
int dinic(int s,int t,int tp=1)
{
this->s=s;this->t=t;this->tp=tp;
int flw=0;
while(bfs())
{
memset(cur,0,sizeof(cur));
flw+=dfs(s,INT_MAX);
}
return flw;
}
}sol;
int n,m,i,s,t,ans;
int main(){
scanf("%d%d%d%d",&n,&m,&s,&t);
for(i=0;i<m;i++)scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].cap);
sort(e,e+m,[](edge a,edge b){return a.cap>b.cap;});
for(int tp:{0,1})
for(int p=1<<30,i=0;p;p/=2)
{
while(i<m&&e[i].cap>=p)
{
if(tp)sol.v[e[i].v].push_back(i*2+1);
else sol.AddEdge(e[i].u,e[i].v,e[i].cap);
i++;
}
ans+=sol.dinic(s,t,tp);
}
printf("%d\n",ans);
return 0;
}