一、图的定义
- 图由顶点集V(G)和边集E(G)组成,记为G=(V,E)。其中E(G)是边的有限集合,边是顶点的无序对(无向图)或有序对(有向图)。
- DAG,即有向无环图,之后的拓扑排序、网络流都会用到。
二、图的存储
- 邻接矩阵:
O
(
∣
V
∣
2
)
O(|V|^2)
O(∣V∣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不能处理负权边。
时间复杂度:
- 朴素Dijkstra: O ( ∣ V ∣ 2 + ∣ E ∣ ) O(|V|^2+|E|) O(∣V∣2+∣E∣)
- 堆优化Dijkstra: O ( ∣ E ∣ l o g ∣ V ∣ ) O(|E|log|V|) O(∣E∣log∣V∣)
代码:
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;
}
总结:
四、二分图
- 定义:二分图中的所有顶点能够分成两个相互独立的集合 S , T S,T S,T,并且所有边都在集合之间而集合之内没有边。二分图的一个重要性质是二分图中无奇数环。
- 染色判断二分图:利用深度优先搜索,从任意一个顶点开始染色,共有两种颜色,保证每个顶点的颜色与它的父节点和子节点都不相同,时间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣)。
- 匈牙利算法求最大匹配:匈牙利算法本质就是不断寻找增广路来扩大匹配数。但是其正确性证明比较复杂,在此略去。
时间复杂度: 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);
}
五、网络流
-
定义:带权的有向图 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); -
可行流:对于任意一个时刻,设 f ( u , v ) f(u,v) f(u,v) 为实际流量,整个图 G G G 的流网络满足以下 3 3 3 个性质:
- 容量限制:对任意 u , v ∈ V u,v\in V u,v∈V, 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,v∈V, 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 点本身不会”制造”和”消耗”流量。
- 最大流:在容量网络中,满足弧流量限制条件,且满足平衡条件并且具有最大流量的可行流,称为网络最大流,简称最大流。
- 弧的类型:
· 饱和弧:即 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. - EK算法:求最大流的过程,就是不断找到一条源到汇的路径,若有,找出增广路径上每一段[容量-流量]的最小值delta,然后构建残余网络,再在残余网络上寻找新的路径,使总流量增加。然后形成新的残余网络,再寻找新路径……直到某个残余网络上找不到从源到汇的路径为止,最大流就算出来了。
时间复杂度:
上限为 O ( ∣ V ∣ ∣ E ∣ 2 ) O(|V||E|^2) O(∣V∣∣E∣2),一般可以处理 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;
}
- 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,P2…Pk,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=deeppi−1+1
算法流程
- 先利用BFS对残余网络分层。一个节点的深度,就是源点到它最少要经过的边数。
- 分完层后,从源点开始,用DFS从前一层向后一层反复寻找增广路(即要求DFS的每一步都必须要走到下一层的节点)。
- DFS过程中,要是碰到了汇点,则说明找到了一条增广路径。此时要增加总流量的值,消减路径上各边的容量,并添加反向边,即所谓的进行增广。
- DFS找到一条增广路径后,并不立即结束,而是回溯后继续DFS寻找下一个增广路径。
- DFS结束后,对残余网络再次进行分层,然后再进行DFS。当残余网络的分层操作无法算出汇点的层次(即BFS到达不了汇点)时,算法结束,最大流求出。
时间复杂度:
在普通情况下, DINIC算法时间复杂度为
O
(
∣
V
∣
2
∣
E
∣
)
O(|V|^2|E|)
O(∣V∣2∣E∣);
在二分图中, DINIC算法时间复杂度为
O
(
∣
E
∣
∣
V
∣
)
O(|E|\sqrt{|V|})
O(∣E∣∣V∣).
一般情况下可处理
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;
}
- 割:
通俗的理解一下: 割集好比是一个恐怖分子,把你家和自来水厂之间的水管网络砍断了一些,
然后自来水厂无论怎么放水,水都只能从水管断口哗哗流走了,你家就停水了。
割的大小应该是恐怖分子应该关心的事,毕竟细管子好割一些,而最小割花的力气最小。
最小割最大流定理:网络流的最大流量等于最小割的容量。
- 费用流:现在我们想象假如我们有一个流量网络,现在每个边除了流量,现在还有一个单位费用,这条边的费用相当于它的单位费用乘上它的流量,我们要保持最大流的同时,还要保持边权最小,这就是最小费用最大流问题。因为在一个网络流图中,最大流量只有一个,但是“流法”有很多种,每种不同的流法所经过的边不同因此费用也就不同,所以需要用到最短路算法。总增广的费用就是最短路*总流量。
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;
}
参考文章:https://blog.csdn.net/A_Comme_Amour/article/details/79356220
https://blog.csdn.net/weixin_44548214/article/details/115571542