图论总结(未完结)
图论总结
5.13 开始讲图论的,我咕咕咕到了 5.20。。。
图论相对于 DP 和数学期望那块,算是简单的了。感觉就是要多总结多找题感吧。
这里从基础概念开始,总结一些非常非常基础的东西。由于我太菜似乎也总结不出什么深奥的东西/kk。
概念
注:这里是总结了一部分我认为比较重要或者比较难的概念,可能并不全面,全面的图的概念见图论相关概念 - OI Wiki。
图
图(Gragh)是一个二元组 \(G=(V(G),E(G))\)。其中 \(V(G)\) 是点集,\(E(G)\) 是边集。
根据不同的分类标准可以把图分为不同的种类:
按照边是否有边权把图分为无权图和有权图;按照图是否连通分为连通图和非连通图;按照边是否有向分为有向图和无向图……
度数
与一个顶点 \(v\) 相连的边的条数称作该顶点的度数(degree),记作 \(d(v)\)。
对于有向图,一个点的度数又可以分为入度和出度。对于一个点 \(v\),以该点为终点的边的条数叫点 \(v\) 的入度,以该点为起点的边的条数叫点 \(v\) 的出度。
重边
若 \(E\) 中存在两个完全相同的元素(边)\(e_1,e_2\),则它们被称作(一组)重边。
生成子图
在图 \(G=(V(G),E(G))\) 中,选一些点和边,若这些点和边构成了一棵树,则这是这些点和边构成了一张生成子图。
连通图/强联通图
在一个无向图上的任意两个点可以互相到达,那么这张图叫做连通图。
在一个有向图上的任意两个点可以互相到达,那么这张图叫做强连通图。
稀疏图/稠密图
若一张图的边数远小于其点数的平方,那么它是一张稀疏图。
若一张图的边数接近其点数的平方,那么它是一张稠密图。
图的存储
图有三种存储方式。
邻接矩阵
方法
定义一个二维数组 \(a_{u,v}\) 表示节点 \(u\) 到 \(v\) 之间是否有边;有边,则 \(a_{u,v}=1\),反之,则 \(a_{u,v}=0\)。
对于有权图,\(a_{u,v}\) 可以存储 \(u\) 到 \(v\) 的边权。
时间复杂度
查询两点间是否有边:\(O(1)\)。
遍历一个点所有出边:\(O(n)\)。
遍历整张图:\(O(n^2)\)。
空间复杂度:\(O(n^2)\)。
优点
可以在 \(O(1)\) 的时间里查询两点间是否有边。
缺点
-
只适用于图无重边的情况;
-
对于点数较多的图,空间复杂度太大,无法接受;
-
遍历一个点的所有出边和遍历整张图时间复杂度较大,难以接受。
代码实现
int a[maxn][maxn];
for(int i=1;i<=m;i++)//输入 m 条边
{
int u,v,w;
u=read();v=read();w=read();//无权图不需要输入 w
a[u][v]=w;//无权图:a[u][v]=1;
//无向图:a[v][u]=w;
}
邻接表
使用一个可以作为动态数组的数据结构(vector 或者 basic_string)来存边。
方法
定义 basic_string(或 vector),edge[u] 就表示点 \(u\) 所有出边信息。每次遇到一个 \(u,v,w\),就连边,具体见下方实现代码。
时间复杂度
查询两点 \(u,v\) 之间是否有边:\(O(d(u))\)。
遍历一个点所有出边:\(O(d(u))\)。
遍历整张图:\(O(n+m)\)(\(n\) 是点数,\(m\) 是边数)。
空间复杂度:\(O(m)\)(注意无向图需要开 2 倍空间)。
优点
-
遍历整张图和遍历一个所有出边的时间复杂度均较小;
-
空间复杂度较小;
-
尤其适用于需要对一个点的所有出边进行排序的场合。
-
适用于稠密图。
缺点
判断两点间是否有边的时间复杂度较大。
代码实现(这里以有向无环图为例)
struct Node{int v,w;}//v 另一个端点,w 表示边权。
basic_stringedge[maxn];
//存边
for(int i=1;i<=m;i++)
{
int u,v,w;
u=read();v=read();w=read();
edge[u]+=Node{v,w};
}
//遍历一个点所有出边
for(Node y:edge[x])
{
//y.v 即为终点
//y.w 即为边权
}
链式前向星
由于这玩意写起来太麻烦我实在懒得用。不过为了以后复(重)习(开)还是总结一下吧。
方法
将邻接表换成类链表的形式即可。
对于一个点 \(u\),定义 \(head_u\) 表示以 \(u\) 为起点的第一条边编号,\(to_u\) 表示当前边的终点,\(nxt_i\) 表示 \(u\) 的第 \(i\) 条边的下一条边的编号,\(cnt\) 表示当前图中总共有多少边。
逆序存边。每次连一条边 \(u,v\) 时:
-
边数 \(cnt++\);
-
将该边的 \(nxt\) 设为原 \(head_u\);
-
新的 \(head_u\) 设为当前边数 \(cnt\);
-
\(to_u\) 设为 \(v\)。
(这里说的比较简略,若想要更加深刻的了解链式前向星或没看懂的,可以移步链式前向星(详解)_Stephencurry‘s csdn的博客-CSDN博客_链式前向星)
时间复杂度:同邻接表时间复杂度。
空间复杂度:同邻接表空间复杂度。
优点
前两条同邻接表。
然而其实我也不知道它有什么其它的优点(至少现在未体会到)。先引(chao)用(xi) OIWiki 的一句话,等日后有所体会再回来补充吧:
优点是边是带编号的,有时会非常有用,而且如果
cnt的初始值为奇数,存双向边时i ^ 1即是i的反边(常用于 网络流)。
缺点
-
判断两点间是否有边的时间复杂度较大;
-
不能方便地对一个点的出边进行排序;
-
写起来显然比前两种麻烦的多(
所以用这玩意干啥)。
代码实现
int head[maxn],to[maxn],nxt[maxn];
int cnt;
void add(int u,int v)//连一条从 u 到 v 的边
{
nxt[++cnt]=head[u];
head[u] = cnt;
to[cnt] = v;
}
// 遍历 u 的出边
for(int i=head[u];i;i=nxt[i])
{
int v = to[i];
}
最小生成树
定义
生成树:对于一个无向连通图 \(G=(V,E),n=|V|,m=|E|\),由 \(V\) 中的全部 \(n\) 个节点和 \(E\) 中的 \(n-1\) 条边构成的无向联通子图叫做图 \(G\) 的一棵生成树。
最小生成树:对于一个无向连通图,其边权和最小的生成树就叫做这个无向连通图的最小生成树(Minimum Spanning Tree),简称 MST。
注:最小生成树存在的前提是无向连通图,非无向连通图没有生成树。(非连通图只有最小生成森林)
Kruskal 算法
基本思想
利用贪心思想。
在任意时刻,都从剩余的边中选出一条权值最小,且该边两个端点不属于同一棵树(不连通),把该边加入 MST 中。
步骤
-
对于每一个节点单独建立一个并查集;
-
将图上所有的边从小到大排序;
-
遍历每一条边。
-
判断连接这条边的两个节点是否在同一个集合内。
若不在,则将它们连边,同时加入到一个集合里。
若在,则
continue。 -
直到加入 \(n-1\) 条边,即形成了一棵树,结束遍历。
时间复杂度
排序+并查集:\(O(m \log m)\)。
证明
咕咕咕
懒得写(之后补上吧,最近要总结的东西多了点)
代码实现
struct Node{int u,v,w;}e[maxm];//结构体存边
int fa[maxn];//并查集
int n,m,ans;
int find(int x)
{
if(x!=fa[x])fa[x]=find(fa[x]);
return fa[x];
}
void add(int x,int y)
{
x=find(x);y=find(y);
fa[x]=y;
}
int cmp(Node a,Node b){return a.w
例题
P3366【模板】最小生成树 - 洛谷
P1396 营救 - 洛谷:
P1967 [NOIP2013 提高组] 货车运输 - 洛谷:经典 LCA+Kruskal 练手题。
P2323 [HNOI2006]公路修建问题 - 洛谷:分别考虑两种公路,两边 Kruskal。0
P4047 [JSOI2010]部落划分 - 洛谷:可以加深对 Kruskal 本质的理解。
P2245 星际导航 - 洛谷(这题和货车运输本质上一样)
Prim 算法
基本思想
依然是贪心思想。
随便选择一个点作为起始点(加入到连通块中),然后每次从剩下的点中选择与当前连通块(最小生成树)距离最短的点加入到连通块中,直到所有的点都被加到这个连通块为止。那么这个连通块就是最小生成树。
步骤
-
定义一个数组 \(dis_i\) 表示节点 \(i\) 到当前联通块的距离;
-
随便选择一个点,加入到连通块(最小生成树)中;
-
更新所有剩下的节点到 \(i\) 的距离;
-
选择一个距离当前连通块距离最近的点 \(t\) 加入到连通块中;
-
对于所有剩下的节点 \(i\),判断 \(dis_i\) 是否大于 \(t\) 与 \(i\) 的距离;若大于,则 \(dis_i\) 更新为 \(t\) 到 \(i\) 的距离。
-
重复步骤 2-5,直至所有的点加入到连通块中为止,则连通块即为最小生成树。
时间复杂度
\(O(n^2)\),优先队列优化:\(O(m \log n)\)。
证明
适用范围
相对于 Kruskal:在稠密图尤其是完全图上,暴力 Prim 的复杂度比 Kruskal 优,但 不一定 实际跑得更快。
实现代码
int calc(int a,int b){return (x[a]-x[b])*(x[a]-x[b])+(y[a]-y[b])*(y[a]-y[b]);}
//计算两点间距离
void add(int x)//将一个点加入连通块。
{
for(int i=1;i<=n;i++)
{
if(vis[i])continue;
if(i==x)continue;
dis[i]=min(dis[i],calc(i,x));
}
vis[x]++;
}
void Prim()
{
dis[1]=0;
add(1);
int T=n-1;
while(T--)
{
int now=0;
for(int i=1;i<=n;i++)
{
if(!vis[i]&&dis[i]
例题
P1265 公路修建 - 洛谷:典型的 Prim 例题,用 Kruskal 会 MLE。
次小生成树
注:由于本人语文太差,所以以下内容借鉴了 OIWiki 上次小生成树讲解。
非严格次小生成树
定义
在一张无向图中,边权和最小的满足边权和 \(\ge\) 最小生成树边权和的生成树。
求解步骤
-
求出无向图的最小生成树,设其边权和为 \(val\);
-
遍历每条未被选中的边 \(e=(u,v,w)\);
-
找到最小生成树上 \(u\) 到 \(v\) 边权最大的一条边 \(e'=(u',v',w')\):用在 货车运输 那道题里的思路,在倍增求 LCA 的过程中维护每个节点到其 \(2^i\) 级祖先的最大边权;
-
用 \(e\) 替换 \(e'\),可以得到一条边权和 \(val'=val-e'+e\) 的生成树;
-
由于求的是次小,所以只需要在上述所有求得的 \(val'\) 取最小值即可。
严格次小生成树
定义
在一张无向图中,边权和最小的满足边权和严格 \(>\) 最小生成树边权和的生成树。
求解步骤
考虑在求严格次小生成树的过程中稍作改动。
在刚刚的求解过程中,之所以是非严格大于,是因为最小生成树保证生成树中 \(u\) 到 \(v\) 路径上的边权最大值一定不大于其它从 \(u\) 到 \(v\) 路径的边权最大值。即:我们在用 \(e\) 替换 \(e'\) 时,两者边权可能是相等的。
解决方法:在倍增求 LCA 的过程中维护每个节点到其 \(2^i\) 级祖先的最大边权的同时维护严格次大边权,当最大边权与原最小生成树上最大边权相等,用严格次大值替换;
时间复杂度
\(O(m \log m)\)。
代码实现
//摘抄我的 P4180 严格次小生成树
struct P{int u,v,w;}e[maxn<<1];
int f[maxn],vis[maxn],fa[maxn][25],tt[5];
int maxx[maxn][25],minn[maxn][25],dep[maxn];
//maxx 表示的是
int n,m,ans,ss;
struct Node{int v,w;};
basic_stringedge[maxn<<1];
int cmp(P a,P b){return a.w=0&&tt[t]==tt[3])t--;
if(t<0)minn[x][i]=-INF;
else minn[x][i]=tt[t];
}
for(Node y:edge[x])
{
if(y.v==fath)continue;
maxx[y.v][0]=y.w;
dfs(y.v,x);
}
}
int lca(int u,int v)
{
if(dep[u]=0;i--)
{
if(fa[u][i]!=fa[v][i]){u=fa[u][i];v=fa[v][i];}
}
return fa[u][0];
}
int query(int x,int y,int val)
{
int ret=-INF;
for(int i=20;i>=0;i--)
{
if(dep[fa[x][i]]>=dep[y])
{
if(val!=maxx[x][i])ret=max(ret,maxx[x][i]);
else ret=max(ret,minn[x][i]);
x=fa[x][i];
}
}
return ret;
}
void work()
{
ans=INFLL;
for(int i=1;i<=m;i++)
{
if(!vis[i])
{
int LCA=lca(e[i].u,e[i].v);
int x=query(e[i].u,LCA,e[i].w);
int y=query(e[i].v,LCA,e[i].w);
int kk=max(x,y);
if(kk!=-INF)ans=min(ans,ss-kk+e[i].w);
}
}
if(ans==INFLL)cout<<-1<
Kruskal 重构树
定义/步骤
-
前三步同 Kruskal 算法;
-
判断连接这条边的两个节点是否在同一个集合内。
若不在,则:
-
将他们连边;
-
新建一个点,点权为加入边的边权;
-
将两个集合的根节点分别设为新建点的左儿子和右儿子
-
将两个集合和新建点合并成一个集合。将新建点设为根。
-
-
直到加入 \(n-1\) 条边,即形成了一棵树,结束遍历。
那么形成的这个有 \(n\) 个叶子节点的二叉树,我们就叫做 Kruskal 重构树。
性质
-
是一棵有根二叉树,根节点是最后新建节点;
-
若原图联通,则 Kruskal 重构树会比原图多 \(n-1\) 个节点(连了 \(n-1\) 条边嘛);
-
上述定义下(即:边权从小到大排序),节点 \(u\) 到 \(v\) 路径上最大边权的最小值 = Kruskal 重构树上 \(lca(u,v)\);
-
边权从大到小排序,节点 \(u\) 到 \(v\) 路径上最小边权的最大值 = Kruskal 重构树上 \(lca(u,v)\)。
适用范围
求图上两点路径上最大边权最小值/最小边权最大值。
代码实现(以最小边权最大为例)
//复制粘贴我 货车运输 的代码。码风可能和现在有所区别。但~~懒得改了~~
int find(int x)
{
if(x!=f[x]){f[x]=find(f[x]);}
return f[x];
}
void dfs(int x,int fath,int v)
{
dep[x]=dep[fath]+1;
sum[x][0]=v;
fa[x][0]=fath;
for(int i=1;i<=20;i++)
{
fa[x][i]=fa[fa[x][i-1]][i-1];
sum[x][i]=min(sum[x][i-1],sum[fa[x][i-1]][i-1]);
}
for(Node y:edge[x])
{
if(y.v==fath)continue;
dfs(y.v,x,y.w);
}
}
int work(int x,int y)
{
if(dep[x]=0;i--)
{
if(fa[x][i]!=fa[y][i])
{
ans=min(ans,min(sum[x][i],sum[y][i]));
x=fa[x][i];
y=fa[y][i];
}
}
int t=sum[x][0];
if(fa[x][0]!=y) t=min(t,sum[y][0]);
return min(t,ans);
}
int main()
{
memset(sum,0x3f3f3f,sizeof(sum));
n=read();m=read();
for(int i=1;i<=m;i++)
{
e[i].x=read();e[i].y=read();e[i].z=read();
}
sort(e+1,e+m+1,cmp);
for(int i=1;i<=n;i++)f[i]=i;
for(int i=1;i<=m;i++)
{
int a=find(e[i].x),b=find(e[i].y);
if(f[a]==b)continue;
f[a]=b;
edge[e[i].x]+=((Node){e[i].y,e[i].z});
edge[e[i].y]+=((Node){e[i].x,e[i].z});
}
for(int i=1;i<=n;i++)
{
if(f[i]==i)dfs(i,i,100000);
}
int q=read();
while(q--)
{
int x,y;// q 组询问,每次求 x 和 y 路径上最小边权最大值。
x=read();y=read();
int X=find(x),Y=find(y);
if(X!=Y)cout<<-1<<'\n';
else cout<
例题
P1967 [NOIP2013 提高组] 货车运输 - 洛谷:可以看做是生成树,也可以看做是 Kruskal 重构树求最小边权最大。
P2245 星际导航 - 洛谷:几乎和 货车运输 一样,只是求的是最大边权最小值。
P7834 [ONTAK2010] Peaks 加强版 - 洛谷:离散化+Kruskal 重构树+树上倍增+主席树。
拓扑排序
定义
感觉自己完全叙述不出来于是抄了度娘
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological
Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。 ——百度百科
可以看出,拓扑排序的目标是将所有的节点排序,使得排在前面的点不依赖排在后面的点。
其实这也就是 DP 求解的本质。
我们在 DP 中学到,一个 DP 问题,求解大的状态依赖于小的状态。
只有当小状态求解完成之后,才能获取大状态的解。
这些依赖关系形成了 DAG,即:
-
自顶向下 + 记忆化的求解,对应自顶向下的拓扑排序。
-
自底向上的 DP 求解,对应自底向上的拓扑排序。
(这一点会在我之后复(重)习(开)DP 后提到,如果不咕的话)
求解步骤
-
建立一个空队列 \(q\);
-
在图上找到所有入度为 0 的点,将它们入队;
-
对于所有在队列中的点:
-
出队;
-
遍历所有与它们连边的点 \(i\),将它们出度减一;
若此时 \(i\) 的入度为 0,则将其入队。
-
-
重复步骤 2-3;
优化/拓展
对于求字典序最大/最小的拓扑排序:可将队列换成优先队列实现。
时间复杂度
设 DAG 有 \(n\) 个点 \(m\) 条边。
普通队列下:\(O(n+m)\)。
优先队列求字典序最大/最小:\(O(n \log n+m)\)。
代码实现
for(int i=1;i<=m;i++)
{
int u,v;
u=read();v=read();
edge[u]+=v;
in[v]++;//in[v] 表示 v 的入度。
}
for(int i=1;i<=n;i++)//将第一轮所有入度为 0 的点入队。
{
if(!in[i])q.push(i);
}
while(!q.empty())
{
int now=q.front();
q.pop();
for(int y:edge[now])
{
in[y]--;
if(!in[y])q.push(y);
}
}
例题
P1347 排序 - 洛谷:去年 9 月份写的,已经差不多忘了题意了,但似乎挺板的(?)
P7113 [NOIP2020] 排水系统 - 洛谷:比较板的题,注意最后一个点会爆 ull,所以请使用 int128 或者把一个数压成两个数的方法存储。
P3243 [HNOI2015]菜肴制作 - 洛谷:反向 topsort+优先队列求字典序。
P1983 [NOIP2013 普及组] 车站分级 - 洛谷
最短路
一些说明/概念
单源最短路径:图上一个点到其它所有点的最短路径;
多源最短路径:图上每个点分别作为起点和终点的最短路径;
下面要说明的几种算法都是针对有权有环图而言的。
在 DAG 上可以直接用 topsort 来求最短路径;
在无权图上可以直接 BFS。
Floyd 算法
适用范围
Floyd 算法用于求解多源最短路。
求解过程
定义 \(dp_{k,x,y}\) 表示从 \(x\) 到 \(y\) 只经过编号 \(\le k\) 的节点的最短路径。
初始化:\(dp_{0,x,y}=e(x,y)\)。
特殊地,所有的 \(dp_{0,x,x}=0(x=y)\);若 \(x,y\) 不连边,则 \(dp_{0,x,y}=\infty\)(设成 \(\infty\) 是因为后面转移的时候要取最小值)。
考虑转移。有两种情况:
-
经过编号为 \(k\) 的点:\(dp_{k,x,y}=dp_{k-1,x,k}+dp_{k-1,k,y}\)。
-
不经过编号为 \(k\) 的点:\(dp_{k,x,y}=dp_{k-1,x,y}\)。
上述两种情况取最小值即可。
那么对于所有的 \(x,y\),\(dp_{n,x,y}\) 即为答案。
空间复杂度 \(O(n^3)\),\(n\) 稍大就会 MLE,考虑优化。
可以发现第一维的 \(k\) 只与上一层的 \(k-1\) 有关,所以可以省略。
那么有:
\[dp_{x,y}=\min(dp_{x,k}+dp_{k,y},dp_{x,y}) \]复杂度
时间复杂度:\(O(n^3)\)。
空间复杂度:\(O(n^2)\)。
代码实现
//注意 k 是最外层循环,i 是次外层,j 是内层。
//不能是以 i,j,k 的顺序循环。
memset(dp,0x3f,sizeof(dp));
for(int i=1;i<=m;i++)
{
int u,v,w;
u=read();v=read();w=read();
dp[u][v]=min(dp[u][v],w);
}
for(int k=1;k<=n;k++)
{
for(int x=1;x<=n;x++)
{
for(int y=1;y<=n;y++)
{
dp[x][y]=min(dp[x][y],dp[x][k]+dp[k][y]);
}
dp[x][x]=0;
}
}
dijkstra 算法
适用范围
适用于求单源最短路径。
其实在一些问题中,也可以
Bellman-ford & SPFA
参考资料
图论相关概念 - OI Wiki
最小生成树 - OI Wiki
拓扑排序 - OI Wiki
拓扑排序_百度百科
算法竞赛进阶指南 - 0x60 图论(纸质资料)
(以上参考资料按总结顺序排序。)
To be continued……