最大流(Dinic算法)
最大流:给定一张带权有向图,有源点和汇点。源点可以流出无穷多的水,汇点可以接受无穷多的水,但是每条边有最大可承载流量,要求输出汇点接受到的最大流量。
/*
Dinic算法,理论上界O(n^2*m)
步骤:
1.在残量网络上bfs求出节点的层次,构造分层图
2.在分层图上dfs寻找增广路,回溯时更新流量
优化:
1.当前弧优化:当dfs增广的时候,从有效的边开始增广,不再增广以前增广过的
2.炸点优化:dfs时当前点没有流量了,就不再使用了
3.分层优化:bfs时找到汇点就退出
*/
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
typedef long long ll;
const int maxn = 1205;
struct edges{
int to,next;
ll val;
}edge[300005];
int head[maxn],cnt;
int dep[maxn]; //depth[i]表示第i个点的层次,即从源点到它的距离
int cur[maxn]; //当前弧优化,每一个点当前增广到的边
int s,e;
int tot; //总共有多少点
void add(int u,int v,ll val) {
edge[cnt].to = v;
edge[cnt].val = val;
edge[cnt].next = head[u];
head[u] = cnt++;
}
bool bfs() { //bfs分层
queue<int> q;
memset(dep,0,sizeof(dep));
dep[s] = 1;
q.push(s);
while( !q.empty() ) {
int x = q.front();
q.pop();
for (int i = head[x]; i != -1; i = edge[i].next) {
int v = edge[i].to;
if( edge[i].val > 0 && dep[v] == 0 ) { //若该残量不为0,且V[i]还未分配深度,则给其分配深度并放入队列
dep[v] = dep[x] + 1;
q.push(v);
if( v == e ) return true; //分层优化,找到汇点就退出
}
}
}
if( dep[e] == 0 ) return false; //当汇点的深度不存在时,说明不存在分层图,同时也说明不存在增广路
return true;
}
ll dfs(int x,ll flow) { //当前节点,当前状态这个点能流的最大流
if( x == e ) return flow; //到终点直接返回
ll used = 0; //记录当前点流出去的最大流量
for (int i = cur[x]; i != -1; i = edge[i].next) { //当前弧优化
cur[x] = i; //x节点增广到第i条边了
if( dep[edge[i].to] == dep[x] + 1 && edge[i].val > 0 ) {
ll d = dfs(edge[i].to,min(flow-used,edge[i].val)); //向下增广
edge[i].val -= d;
edge[i^1].val += d;
used += d;
if( used == flow ) break;
}
}
if( used == 0 ) dep[x] = -1; //炸点优化,这个点已经没有流量了,说明没有用了,置为-1在循环中不会用到了
return used; //否则说明没有增广路,返回0
}
ll Dinic() {
ll ans = 0;
while( bfs() ) {
for (int i = 0; i <= tot; i++) //每次分层后更新cur,当前增广的边肯定是第一条边,这里必须覆盖了全部的点
cur[i] = head[i];
ans += dfs(s,1e18);
}
return ans;
}
int main()
{
int n,m;
cin >> n >> m >> s >> e;
tot = n;
memset(head,-1,sizeof(head));
for (int i = 1; i <= m; i++)
{
int x,y;
ll v;
cin >> x >> y >> v;
add(x,y,v);
add(y,x,0);
}
cout << Dinic() << '\n';
return 0;
}
最小割
给定一个网络,若一个边集被删去后,起点无法抵达终点,则成该边集为这个网络的割。
最小权值的割称为最小割。
最小割=最大流
费用流
在最大流的基础上,给每条边权加上了单位流量的花费p,f的流量会对答案产生p*f的贡献。
最小费用最大流:在最大化流量的前提下最小化总费用。
/*
最小费用最大流
加上单位花费后,只要在EK算法的基础上加以改进即可。
将EK算法中bfs找一条从s到t的路径变成spfa找一条从s到t的路径单位花费和最小的路径即可。
复杂度上界为O(V^2*E)
*/
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
typedef long long ll;
const ll INF = 1e18;
const int maxn = 5e3+5;
struct Edge{
int to,next;
ll val,cost;
}edge[100010];
int head[maxn],cnt = 0;
void add(int u,int v,ll val,ll cost)
{
edge[cnt].to = v;
edge[cnt].val = val;
edge[cnt].cost = cost;
edge[cnt].next = head[u];
head[u] = cnt++;
}
int s,e,tot;
int exist[maxn],preid[maxn],tag[100010];
ll dist[maxn];
//exist[i]表示是否在队列中
//preid[i]表示i的前一个节点编号
//tag[i]表示preid到i的边的下标编号
bool spfa() //找一条从起点到终点单位花费和最少的路径
{
for (int i = 0; i <= tot; i++) //一定要包括所有的点
{
dist[i] = INF;
}
memset(preid,0,sizeof(preid));
memset(tag,0,sizeof(tag));
memset(exist,0,sizeof(exist));
queue<int> q;
q.push(s); //把起点入队
exist[s] = 1;
dist[s] = 0;
while( !q.empty() )
{
int x = q.front();
exist[x] = 0;
q.pop();
for (int i = head[x]; i != -1; i = edge[i].next) //遍历与x相邻的节点
{
Edge t = edge[i];
if( t.val && dist[t.to] > dist[x] + t.cost ) //如果这条边的权值大于0,表明可以走
{
dist[t.to] = dist[x] + t.cost;
preid[t.to] = x; //preid置为x
tag[t.to] = i; //tag置为i
if( !exist[t.to] )
{
q.push(t.to);
exist[t.to] = 1;
}
}
}
}
if( dist[e] == INF ) return false; //找不到这条路径
return true;
}
void mcmf(ll &flow,ll &cost)
{
while( spfa() )
{
ll mi = 1e18;
for (int t = e; t != s; t = preid[t])
{
mi = min(mi,edge[tag[t]].val); //回溯找到最小流量
}
for (int t = e; t != s; t = preid[t])
{
edge[tag[t]].val -= mi; //正边减去最小流量
edge[tag[t]^1].val += mi; //负边加上最小流量
}
flow += mi; //答案加上流量
cost += (ll)mi * dist[e]; //花费加上流量乘单位花费和
}
}
int main()
{
int n,m;
cin >> n >> m >> s >> e;
tot = n;
memset(head,-1,sizeof(head));
for (int i = 1; i <= m; i++)
{
int x,y;
ll v,w;
cin >> x >> y >> v >> w;
add(x,y,v,w);
add(y,x,0,-w);
}
ll flow = 0,cost = 0;
mcmf(flow,cost);
cout << flow << ' ' << cost << '\n';
return 0;
}
Dijkstra优化费用流
由于我们需要跑多次的spfa,相当不优秀,所以我们可以通过引入势函数去修正边权使得边权为负。
给每个点赋一个势函数h[i],如果我们保证h[u]-h[v]+w[u][v]>=0,那么我们在松弛时就把边变为这个东西,这样就都是正权边了。那么这个基于这个跑出来的单源最短路dis’[x],实际上的dis[x]=dis’[x]-f[s]+f[x],s为起点。
那么如何保证这个不等式呢,其实移项一下就变成为h[u]+w[u][v]>=h[v],这个就是典型的三角形不等式,所以h[i]的值就是为原图下的最短路径的值。
那么我们考虑利用以上的算法来优化费用流。
在费用流里面,初始的费用如果都是非负的,那么初始的势函数为0。如果图为有向无环图,则可以用dp来转移O(m)算出最短路,否则需要跑一遍spfa来确定初始的f[i]值。
确定了f的初值后,我们考虑每次跑一遍最短路之后,残量网络的变化。其实就是有反边的加入,所以需要修正这个势函数。
实际上对于h[i],新的势函数就是原来的加上这次跑的最短路。可以证明这样操作后不影响正边,同时反边的加入也能得到解决。
证明:
首先费用流跑一次后只会在最短路径上加入反边,那么对于最短路径上的任意两点u->v,均有dis’[u]+w’[u][v]=dis’[v]。即dis’[u]+h[u]-h[v]+w[u][v]=dis’[v]。移项后(dis’[u]+h[u])-(dis’[v]+h[v])+w[u][v]=0。而括号里的就是新的势函数,满足三角形不等式,所以这样更新后对原来的正边没有影响。等式两边同时取负号,(dis’[v]+h[v])-(dis’[u]+h[u])+w[v][u]=0,也能看出也满足反边的要求。
/*
最小费用最大流
dij优化费用流
利用势函数将负权变成正权,dij来替换spfa
复杂度(n*mlogm)
*/
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
typedef long long ll;
const ll INF = 1e18;
const int maxn = 5e3+5;
struct Edge{
int to,next;
ll val,cost;
}edge[100010];
int head[maxn],cnt = 0;
void add(int u,int v,ll val,ll cost)
{
edge[cnt].to = v;
edge[cnt].val = val;
edge[cnt].cost = cost;
edge[cnt].next = head[u];
head[u] = cnt++;
}
struct node{ //dij需要堆优化里放的节点
int id;
ll val;
node(int a,ll b)
{
id = a;
val = b;
}
bool operator<(const node&n)const
{
return val > n.val;
}
};
int s,e,tot;
int preid[maxn],vis[maxn],tag[100010];
ll dist[maxn];
ll h[maxn]; //势函数
//preid[i]表示i的前一个节点编号
//tag[i]表示preid到i的边的下标编号
bool dij() //找一条从起点到终点单位花费和最少的路径
{
for (int i = 0; i <= tot; i++) //一定要包括所有的点
dist[i] = INF;
memset(vis,0,sizeof(vis));
memset(preid,0,sizeof(preid));
memset(tag,0,sizeof(tag));
priority_queue<node> q;
q.push(node(s,0)); //把起点入队
dist[s] = 0;
while( !q.empty() )
{
int x = q.top().id;
q.pop();
if( vis[x] ) continue;
vis[x] = 1;
for (int i = head[x]; i != -1; i = edge[i].next) //遍历与x相邻的节点
{
Edge t = edge[i];
if( t.val && dist[t.to] > dist[x] + t.cost + h[x] - h[t.to] ) //如果这条边的权值大于0,表明可以走
{
//注意一定要加势函数
dist[t.to] = dist[x] + t.cost + h[x] - h[t.to];
preid[t.to] = x; //preid置为x
tag[t.to] = i; //tag置为i
q.push(node(t.to,dist[t.to]));
}
}
}
if( dist[e] == INF ) return false; //找不到这条路径
return true;
}
void mcmf(ll &flow,ll &cost)
{
memset(h,0,sizeof(h)); //本题中费用均为正数,所以势函数初值为0,否则需要用dp或spfa求出初始的势函数
while( dij() )
{
ll mi = 1e18;
for (int t = e; t != s; t = preid[t])
{
mi = min(mi,edge[tag[t]].val); //回溯找到最小流量
}
for (int t = e; t != s; t = preid[t])
{
edge[tag[t]].val -= mi; //正边减去最小流量
edge[tag[t]^1].val += mi; //负边加上最小流量
}
flow += mi; //答案加上流量
ll dis = dist[e] - h[s] + h[e]; //求出实际的最短路
cost += mi * dis; //花费加上流量乘单位花费和
for (int i = 0; i <= tot; i++) h[i] += dist[i]; //更新势函数
}
}
int main()
{
int n,m;
cin >> n >> m >> s >> e;
tot = n;
memset(head,-1,sizeof(head));
for (int i = 1; i <= m; i++)
{
int x,y;
ll v,w;
cin >> x >> y >> v >> w;
add(x,y,v,w);
add(y,x,0,-w);
}
ll flow = 0,cost = 0;
mcmf(flow,cost);
cout << flow << ' ' << cost << '\n';
return 0;
}