图论总结
图论
一、图的定义
- 图由顶点集 \(V(G)\) 和边集 \(E(G)\) 组成,记为 \(G=(V,E)\)。其中 \(E(G)\) 是边的有限集合,边是顶点的无序对(无向图)或有序对(有向图)。
- DAG,即有向无环图,之后的拓扑排序、网络流都会用到。
- 其他基本定义可以参见 oi-wiki。
- 下文中,一般用 \(n\) 表示顶点数 \(|V|\),用 \(m\) 表示边数 \(|E|\)。
二、图的存储
- 邻接矩阵:\(O(n^2)\)
(代码过于简单,就不放了) - 邻接表:\(O(n+m)\)
代码:
int hd[N],cnt;
struct node{int to,nex,w;}e[M];
void add(int u,int v,int w)//加边
{e[++cnt] = {to,hd[u],w};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(n^2+m)\)
- 堆优化Dijkstra:\(O(m\log n)\)
代码
int dijkstra(int s,int t)
{
memset(dis,0x3f,sizeof dis);
q.push(make_pair(dis[s] = 0,s));
while(!q.empty())
{
int u = q.top().second;q.pop();
if(vis[u])continue;vis[u] = 1;
for(int i = hd[u];i;i = e[i].nex)
{
int v = e[i].to,w = e[i].w;
if(dis[v] > dis[u]+w)
{
dis[v] = dis[u]+w;
if(!vis[v])q.push(make_pair(-dis[v],v));
}
}
}
return dis[t];
}
2.SPFA
时间复杂度:SPFA 的时间复杂度非常玄学,平均是 \(O(km)\),其中 \(k\) 是一个较小的常数,但可能被特殊的图卡成 \(O(nm)\),所以在要用最短路时,能不用 SP FA就不要用。
适用范围: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\),并且所有边都在集合之间而集合之内没有边。二分图的一个重要性质是二分图中无奇数环。
- 染色判断二分图:利用深度优先搜索,从任意一个顶点开始染色,共有两种颜色,保证每个顶点的颜色与它的父节点和子节点都不相同,时间复杂度:\(O(|V|+|E|)\)。
- 匈牙利算法求最大匹配:匈牙利算法本质就是不断寻找增广路来扩大匹配数。但是其正确性证明比较复杂,在此略去。
时间复杂度:\(O(n\times e+m)\),其中 \(n\) 是左部点个数,\(e\) 是图的边数,\(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)\),满足以下条件,则称为网络流图(flow network):
仅有一个入度为 \(0\) 的顶点 \(s\),称 \(s\) 为源点
仅有一个出度为 \(0\) 的顶点 \(t\),称 \(t\) 为汇点
每条边的权值都为非负数,称为该边的容量,记作 \(c(i,j)\)。
弧的流量:通过容量网络 \(G\) 中每条弧 \((u,v)\),上的实际流量(简称流量),记为 \(f(u,v);\) -
可行流:对于任意一个时刻,设 \(f(u,v)\) 为实际流量,整个图 \(G\) 的流网络满足以下 \(3\) 个性质:
- 容量限制:对任意 \(u,v\in V\),\(f(u,v)\le c(u,v)\)。
- 反对称性:对任意 \(u,v\in V\),\(f(u,v) = -f(v,u)\)。从 \(u\) 到 \(v\) 的流量一定是从 \(v\)到 \(u\) 的流量的相反值。
- 流守恒性:对任意 \(u\),若 \(u\) 不为 \(S\) 或 \(T\),一定有 \(\sum f(u,v)=0\),\((u,v)\in E\)。即u到相邻节点的流量之和为 \(0\),因为流入 \(u\) 的流量和 \(u\) 点流出的流量相等, \(u\) 点本身不会”制造”和”消耗”流量。
- 最大流:在容量网络中,满足弧流量限制条件,且满足平衡条件并且具有最大流量的可行流,称为网络最大流,简称最大流。
- 弧的类型:
- 饱和弧:即 \(f(u,v)=c(u,v)\);
- 非饱和弧:即 \(f(u,v) < c(u,v)\);
- 零流弧:即 \(f(u,v)=0\);
- 非零流弧:即 \(f(u,v)>0\).
EK算法
求最大流的过程,就是不断找到一条源到汇的路径,若有,找出增广路径上每一段[容量-流量]的最小值delta,然后构建残余网络,再在残余网络上寻找新的路径,使总流量增加。然后形成新的残余网络,再寻找新路径,直到某个残余网络上找不到从源到汇的路径为止,最大流就算出来了。
时间复杂度:上限为 \(O(|V||E|^2)\),一般可以处理 \(10^3\)~\(10^4\) 的数据规模。
代码
#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,P_1,P_2…P_k,t\),点 \(v\) 在分层图中的所属的层记为 \(d_v\),那么应满足 \(d_{p_i}=d_{p_{i−1}}+1\)
算法流程
- 先利用BFS对残余网络分层。一个节点的深度,就是源点到它最少要经过的边数。
- 分完层后,从源点开始,用DFS从前一层向后一层反复寻找增广路(即要求DFS的每一步都必须要走到下一层的节点)。
- DFS过程中,要是碰到了汇点,则说明找到了一条增广路径。此时要增加总流量的值,消减路径上各边的容量,并添加反向边,即所谓的进行增广。
- DFS找到一条增广路径后,并不立即结束,而是回溯后继续DFS寻找下一个增广路径。
- DFS结束后,对残余网络再次进行分层,然后再进行DFS。当残余网络的分层操作无法算出汇点的层次(即BFS到达不了汇点)时,算法结束,最大流求出。
时间复杂度:
- 在普通情况下, DINIC算法时间复杂度为 \(O(|V|^2|E|)\);
- 在二分图中, DINIC算法时间复杂度为 \(O(|E|\sqrt{|V|})\).
- 一般情况下可处理 \(10^4\)~\(10^5\) 的数据规模。
代码
#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
最小斯坦纳树
给定一个 \(n\) 个点 \(m\) 条边的图,有 \(k\) 个点为关键点,你需要选出一些边,使得这 \(k\) 个点联通,且边权和最小。
\(n\le 100,m\le 500,k\le 10\)
首先这些边肯定构成一棵树,考虑状压 dp,设 \(f(i,S)\) 表示以 \(i\) 为根的一棵树,包含集合 \(S\) 中所有点的最小边权和。
那么有以下两种转移方式:
- 用 \(S\) 的某一个子集转移过来:\(f(i,S) \larr f(i,T)+f(i,S-T)\qquad T\subseteq S\)
- 用一条边 \((j,i,w)\) 进行转移:\(f(i,S)\larr f(j,S)+w(j,i)\),这是一个三角形不等式,相当于对整张图进行一次松弛操作。
我们考虑从小到大枚举所有 \(S\),先用 \(S\) 的所有子集更新 \(S\),然后进行一次 SPFA,时间复杂度为 \(\mathcal{O}(n\times 3^k+nm\times 2^k)\),虽然 SPFA 可能会比较假,但实际上 SPFA 跑得比 dijkstra 快得多。
代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#define ll long long
using namespace std;
const int N = 1005,K = 15,inf = 0x3f3f3f3f;
int f[N][1<<K],hd[N],cnt,n,m,k,ans = inf;
bool vis[N];
struct node{int to,nex,w;}e[N << 1];
void add(int u,int v,int w)
{e[++cnt] = {v,hd[u],w};hd[u] = cnt;}
queue<int> q;
void spfa(int s)
{
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,w = e[i].w;
if(f[v][s] > f[u][s]+w)
{
f[v][s] = f[u][s]+w;
if(!vis[v])vis[v] = 1,q.push(v);
}
}
}
}
inline int rd()
{
char c;int f = 1;
while(!isdigit(c = getchar()))if(c=='-')f = -1;
int x = c-'0';
while(isdigit(c = getchar()))x = x*10+(c^48);
return x*f;
}
int main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
n = rd();m = rd();k = rd();
for(int i = 1;i <= m;i++)
{
int u = rd(),v = rd(),w = rd();
add(u,v,w);add(v,u,w);
}
memset(f,inf,sizeof(f));
for(int i = 1;i <= k;i++)f[rd()][1<<i-1] = 0;
for(int s = 1;s < (1<<k);s++)
{
for(int i = 1;i <= n;i++)
{
for(int t = s;t;t = s&(t-1))
f[i][s] = min(f[i][s],f[i][t]+f[i][s^t]);
if(f[i][s] != inf)q.push(i),vis[i] = 1;
}
spfa(s);
}
for(int i = 1;i <= n;i++)ans = min(ans,f[i][(1<<k)-1]);
cout << ans << endl;
return 0;
}