搜索与图论(最短路,网络流等)

一、图的定义
  1. 图由顶点集V(G)和边集E(G)组成,记为G=(V,E)。其中E(G)是边的有限集合,边是顶点的无序对(无向图)或有序对(有向图)。
  2. DAG,即有向无环图,之后的拓扑排序、网络流都会用到。
二、图的存储
  1. 邻接矩阵: O ( ∣ V ∣ 2 ) O(|V|^2) O(V2) (代码过于简单,就不放了)
  2. 邻接表: O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)
    代码:
int hd[N],cnt;
struct node{int to,w,nex;}e[M];
void add(int u,int v,int w)//加边
(e[++cnt] = {to,w,hd[u]};hd[u] = cnt;)
//遍历
for(int i = hd[u];i = e[i].nex)
{
	int v = e[i].to,w = e[i].w;
	...
}

在一般写题的时候都是用的邻接表,邻接表也可以用vector代替,但这样常数会变大 。

三、最短路
1.Dijkstra

概念:Dijkstra算法是一种求解单源最短路的算法,可以在带权有向图中找到每一个点到起点的最短距离。

思路:首先把起点到所有点的距离存下来找个最短的,然后松弛一次再找出最短的,所谓的松弛操作就是,遍历一遍看通过刚刚找到的距离最短的点作为中转站会不会更近,如果更近了就更新距离,这样把所有的点找遍之后就存下了起点到其他所有点的最短距离。

注意事项:Dijkstra不能处理负权边。
时间复杂度:
  1. 朴素Dijkstra: O ( ∣ V ∣ 2 + ∣ E ∣ ) O(|V|^2+|E|) O(V2+E)
  2. 堆优化Dijkstra: O ( ∣ E ∣ l o g ∣ V ∣ ) O(|E|log|V|) O(ElogV)
代码:
void dijkstra()
{
    dis[s] = 0;
    q.push({0, s});
    while(!q.empty())
    {
        node tmp = q.top();
        q.pop();
        int x = tmp.pos d = tmp.dis;
        if(vis[x])continue;
        vis[x] = 1;
        for(int i = head[x];i;i = e[i].next)
        {
            int y = e[i].to;
            if( dis[y] > dis[x] + e[i].dis )
            {
                dis[y] = dis[x] + e[i].dis;
                if(!vis[y])q.push({dis[y], y});
            }
        }
    }
}

模板
2.SPFA
在这里插入图片描述

时间复杂度:

SPFA的时间复杂度非常玄学,平均是 O ( ∣ E ∣ ) O(|E|) O(E),最会情况下是 O ( ∣ V ∣ ∣ E ∣ ) O(|V||E|) O(V∣∣E),所以在要用最短路时,能不用SPFA就不要用。

适用范围:

SPFA的一个重要功能就是用来找负权环,也可以用来处理有负权边的图,比如在求最小费用最大流时会用到。

代码:
bool spfa()
{
	memset(dis,inf,sizeof dis);
	memset(vis,0,sizeof vis);
	q.push(s);vis[s] = 1;dis[s] = 0;minf[s] = inf;
	while(!q.empty())
	{
		int u = q.front();q.pop();vis[u] = 0;
		for(int i = hd[u];i;i = e[i].nex)
		{
			int v = e[i].to;
			if(e[i].f<=0)continue;
			if(dis[v]>dis[u]+e[i].w)
			{
				dis[v] = dis[u]+e[i].w;pre[v] = i;
				minf[v] = min(minf[u],e[i].f);
				if(!vis[v])
				{vis[v] = 1;q.push(v);}
			}
		}
	}
	return dis[t] != inf;
}

总结:在这里插入图片描述

四、二分图
  1. 定义:二分图中的所有顶点能够分成两个相互独立的集合 S , T S,T S,T,并且所有边都在集合之间而集合之内没有边。二分图的一个重要性质是二分图中无奇数环。
  2. 染色判断二分图:利用深度优先搜索,从任意一个顶点开始染色,共有两种颜色,保证每个顶点的颜色与它的父节点和子节点都不相同,时间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)
  3. 匈牙利算法求最大匹配:匈牙利算法本质就是不断寻找增广路来扩大匹配数。但是其正确性证明比较复杂,在此略去。
    时间复杂度: O ( n × e + m ) O(n\times e+m) O(n×e+m),其中 n n n 是左部点个数, e e e 是图的边数, m m m 是右部点个数。
    代码:
bool dfs(int u)
{
    for(int i = 1;i <= m;i++)
       if(a[u][i]&&!vis[i])
       {
            vis[i] = 1;
            if(!f[i]||dfs(f[i]))return f[i] = u,1;
       }
    return 0;
}
void solve()
{
   for(int i = 1;i <= n;i++)
   {
       memset(vis,0,sizeof vis);
       ans += dfs(i);
   }
   printf("%d",ans);
}

P3386 【模板】二分图最大匹配

五、网络流
  1. 定义:带权的有向图 G = ( V , E ) G=(V,E) G=(V,E),满足以下条件,则称为网络流图(flow network):
    仅有一个入度为 0 0 0 的顶点 s s s,称 s s s 为源点
    仅有一个出度为 0 0 0 的顶点 t t t,称 t t t 为汇点
    每条边的权值都为非负数,称为该边的容量,记作 c ( i , j ) c(i,j) c(i,j)
    弧的流量:通过容量网络 G G G 中每条弧 ( u , v ) (u,v) (u,v),上的实际流量(简称流量),记为 f ( u , v ) ; f(u,v); f(u,v);

  2. 可行流:对于任意一个时刻,设 f ( u , v ) f(u,v) f(u,v) 为实际流量,整个图 G G G 的流网络满足以下 3 3 3 个性质:

  • 容量限制:对任意 u , v ∈ V u,v\in V u,vV f ( u , v ) ≤ c ( u , v ) f(u,v)\le c(u,v) f(u,v)c(u,v)
  • 反对称性:对任意 u , v ∈ V u,v\in V u,vV f ( u , v ) = − f ( v , u ) f(u,v) = -f(v,u) f(u,v)=f(v,u)。从 u u u v v v 的流量一定是从 v v v u u u 的流量的相反值。
  • 流守恒性:对任意 u u u,若 u u u 不为 S S S T T T,一定有 ∑ f ( u , v ) = 0 \sum f(u,v)=0 f(u,v)=0 ( u , v ) ∈ E (u,v)\in E (u,v)E。即u到相邻节点的流量之和为 0 0 0,因为流入 u u u 的流量和 u u u 点流出的流量相等, u u u 点本身不会”制造”和”消耗”流量。
  1. 最大流:在容量网络中,满足弧流量限制条件,且满足平衡条件并且具有最大流量的可行流,称为网络最大流,简称最大流。
  2. 弧的类型:
    · 饱和弧:即 f ( u , v ) = c ( u , v ) f(u,v)=c(u,v) f(u,v)=c(u,v);
    · 非饱和弧:即 f ( u , v ) < c ( u , v ) f(u,v) < c(u,v) f(u,v)<c(u,v)
    · 零流弧:即 f ( u , v ) = 0 f(u,v)=0 f(u,v)=0;
    · 非零流弧:即 f ( u , v ) > 0 f(u,v)>0 f(u,v)>0.
  3. EK算法:求最大流的过程,就是不断找到一条源到汇的路径,若有,找出增广路径上每一段[容量-流量]的最小值delta,然后构建残余网络,再在残余网络上寻找新的路径,使总流量增加。然后形成新的残余网络,再寻找新路径……直到某个残余网络上找不到从源到汇的路径为止,最大流就算出来了。
时间复杂度:

上限为 O ( ∣ V ∣ ∣ E ∣ 2 ) O(|V||E|^2) O(V∣∣E2),一般可以处理 1 0 3 10^3 103~ 1 0 4 10^4 104 的数据规模。

代码:
//codevs 1993
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int INF=0x7ffffff;

queue <int> q;
int n,m,x,y,s,t,g[201][201],pre[201],flow[201],maxflow; 
//g邻接矩阵存图,pre增广路径中每个点的前驱,flow源点到这个点的流量 

inline int bfs(int s,int t)
{
	while (!q.empty()) q.pop();
    for (int i=1; i<=n; i++) pre[i]=-1;
    pre[s]=0;
    q.push(s);
    flow[s]=INF;
    while (!q.empty())
	{
        int x=q.front();
        q.pop();
		if (x==t) break;
		for (int i=1; i<=n; i++)
          //EK一次只找一个增广路 
		  if (g[x][i]>0 && pre[i]==-1)
		  {
			pre[i]=x;
			flow[i]=min(flow[x],g[x][i]);
			q.push(i);
          }
	}
	if (pre[t]==-1) return -1;
	else return flow[t];
}

//increase为增广的流量 
void EK(int s,int t)
{
	int increase=0;
	while ((increase=bfs(s,t))!=-1)//这里的括号加错了!Tle 
	{//迭代 
		int k=t;
		while (k!=s)
		{
			int last=pre[k];//从后往前找路径
			g[last][k]-=increase;
			g[k][last]+=increase;
			k=last;
		}
		maxflow+=increase;
	}
}

int main()
{
	scanf("%d%d",&m,&n);
	for (int i=1; i<=m; i++)
	{
		int z;
		scanf("%d%d%d",&x,&y,&z);
		g[x][y]+=z;//此处不可直接输入,要+= 
	}
	EK(1,n);
	printf("%d",maxflow);
	return 0;
}
  1. dinic算法:

前面的网络流算法,每进行一次增广,都要做 一遍BFS,十分浪费。能否少做几次BFS?
这就是Dinic算法要解决的问题。

原理

dinic算法在EK算法的基础上增加了分层图的概念,根据从 s s s 到各个点的最短距离的不同,把整个图分层。寻找的增广路要求满足所有的点分别属于不同的层,且若增广路为 s , P 1 , P 2 … P k , t s,P_1,P_2…P_k,t s,P1,P2Pk,t,点 v v v 在分层图中的所属的层记为 d e e p v deep_v deepv,那么应满足 d e e p p i = d e e p p i − 1 + 1 deep_{p_i}=deep_{p_{i−1}}+1 deeppi=deeppi1+1

算法流程
  • 先利用BFS对残余网络分层。一个节点的深度,就是源点到它最少要经过的边数。
  • 分完层后,从源点开始,用DFS从前一层向后一层反复寻找增广路(即要求DFS的每一步都必须要走到下一层的节点)。
  • DFS过程中,要是碰到了汇点,则说明找到了一条增广路径。此时要增加总流量的值,消减路径上各边的容量,并添加反向边,即所谓的进行增广。
  • DFS找到一条增广路径后,并不立即结束,而是回溯后继续DFS寻找下一个增广路径。
  • DFS结束后,对残余网络再次进行分层,然后再进行DFS。当残余网络的分层操作无法算出汇点的层次(即BFS到达不了汇点)时,算法结束,最大流求出。
时间复杂度:

在普通情况下, DINIC算法时间复杂度为 O ( ∣ V ∣ 2 ∣ E ∣ ) O(|V|^2|E|) O(V2E);
在二分图中, DINIC算法时间复杂度为 O ( ∣ E ∣ ∣ V ∣ ) O(|E|\sqrt{|V|}) O(EV ).
一般情况下可处理 1 0 4 10^4 104~ 1 0 5 10^5 105 的数据规模。

代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#define ll long long
using namespace std;
const int N = 205,M = 5005;
int d[N],rad[N],n,m,s,t;
ll ans;
int hd[N],cnt = 1;
struct node{int to,nex;ll w;}e[M << 1];
void add(int u,int v,ll w)
{e[++cnt] = {v,hd[u],w};hd[u] = cnt;}
queue<int> q;
bool bfs()
{
	memset(d,0,sizeof d);q.push(s);d[s] = 1;
	while(!q.empty())
	{
		int u = q.front();q.pop();
		rad[u] = hd[u];
		for(int i = hd[u];i;i = e[i].nex)
		{
			int v = e[i].to;
			if(!d[v]&&e[i].w)
			{d[v] = d[u]+1;q.push(v);}
		}
	}
	return d[t];
}
ll dfs(int u,ll cl)
{
	if(u==t)return cl;
	ll rem = cl;
	for(int i = rad[u];i;i = e[i].nex)
	{
		int v = e[i].to;rad[u] = i;
		if(d[v]!=d[u]+1||!e[i].w)continue;
		ll now = dfs(v,min(e[i].w,rem));
		e[i].w -= now;e[i^1].w += now;
		rem -= now;
	}
	return cl-rem;
}
inline int rd()
{
	char c;int f = 1;
	while((c = getchar())<'0'||c>'9')if(c=='-')f = -1;
	int x = c-'0';
	while('0' <= (c = getchar())&&c <= '9')x = x*10+(c^48);
	return x*f;
}
int main()
{
	//freopen(".in","r",stdin);
	//freopen(".out","w",stdout);
	n = rd();m = rd();s = rd();t = rd();
	for(int i = 1;i <= m;i++)
	{
		int u = rd(),v = rd();
		add(u,v,rd());add(v,u,0);
	}
	while(bfs())ans += dfs(s,1ll<<32);
	cout << ans;
	return 0;
}

P3376 【模板】网络最大流

  1. 割:
    在这里插入图片描述
通俗的理解一下: 割集好比是一个恐怖分子,把你家和自来水厂之间的水管网络砍断了一些,
然后自来水厂无论怎么放水,水都只能从水管断口哗哗流走了,你家就停水了。
割的大小应该是恐怖分子应该关心的事,毕竟细管子好割一些,而最小割花的力气最小。

最小割最大流定理:网络流的最大流量等于最小割的容量。

  1. 费用流:现在我们想象假如我们有一个流量网络,现在每个边除了流量,现在还有一个单位费用,这条边的费用相当于它的单位费用乘上它的流量,我们要保持最大流的同时,还要保持边权最小,这就是最小费用最大流问题。因为在一个网络流图中,最大流量只有一个,但是“流法”有很多种,每种不同的流法所经过的边不同因此费用也就不同,所以需要用到最短路算法。总增广的费用就是最短路*总流量。
SPFA

就是把Dinic中的bfs改成spfa,再求最大流的过程中最小费用流也就求出来了。

代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int N = 5005,M = 5e4+5,inf = 0x7f7f7f7f;
int dis[N],minf[N],pre[N],n,m,s,t;
bool vis[N];
int hd[N],cnt = 1,maxf,ans;
struct node{int to,f,w,nex;}e[M << 1];
void add(int u,int v,int f,int w)
{e[++cnt] = {v,f,w,hd[u]};hd[u] = cnt;}
queue<int> q;
bool spfa()
{
	memset(dis,inf,sizeof dis);
	memset(vis,0,sizeof vis);
	q.push(s);vis[s] = 1;dis[s] = 0;minf[s] = inf;
	while(!q.empty())
	{
		int u = q.front();q.pop();vis[u] = 0;
		for(int i = hd[u];i;i = e[i].nex)
		{
			int v = e[i].to;
			if(e[i].f<=0)continue;
			if(dis[v]>dis[u]+e[i].w)
			{
				dis[v] = dis[u]+e[i].w;pre[v] = i;
				minf[v] = min(minf[u],e[i].f);
				if(!vis[v])
				{vis[v] = 1;q.push(v);}
			}
		}
	}
	return dis[t] != inf;
}
inline int rd()
{
	char c;int f = 1;
	while((c = getchar())<'0'||c>'9')if(c=='-')f = -1;
	int x = c-'0';
	while('0' <= (c = getchar())&&c <= '9')x = x*10+(c^48);
	return x*f;
}
int main()
{
	n = rd();m = rd();s = rd();t = rd();
	for(int i = 1;i <= m;i++)
	{
		int u = rd(),v = rd(),f = rd(),w = rd();
		add(u,v,f,w);add(v,u,0,-w);
	}
	while(spfa())
	{
		maxf += minf[t];ans += minf[t]*dis[t];
		int now = t;
		while(now!=s)
		{
			e[pre[now]].f -= minf[t];
			e[pre[now]^1].f += minf[t];
			now = e[pre[now]^1].to;
		}
	}
	printf("%d %d",maxf,ans);
	return 0;
}

P3381 【模板】最小费用最大流


参考文章:https://blog.csdn.net/A_Comme_Amour/article/details/79356220
https://blog.csdn.net/weixin_44548214/article/details/115571542

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值