咕了两个月的我(如果不算Luogu的题解)终于回来写博了qwq,因为我的数据结构知识一直很薄弱,每次考试老是失分,所以我决定写一写关于图论的博客,最近一段时间也正好在复习这部分,这篇博客的内容会涉及到:树与图的遍历,树的深度,图的联通块,拓扑排序,树的重心,最短路,最小生成树,并查集,Tarjan与图的连通性,树的直径,LCA,树链剖分,负环。文章内容与lyd的《算法竞赛进阶指南》重合度比较高(因为我就是按照他的书来复习的),同时文章以讲解为辅,代码为主,适合各位同学复习而并非初学者接触。
所有模板都是我手打的,也许有错误,欢迎批评指正弱弱的我qwq。
声明
博客中如果没有特殊说明,则默认是n个点和无向图,文中会使用vector,邻接表,邻接矩阵这三种方式来存储。
树与图的遍历
树与图的存储方式是相同的,遍历可以采用dfs和bfs这两种方式。
深度优先遍历与深度
众所周知写这个只是为了知识的完整性,大家随便看看就行。
dep是深度数组。
void dfs(int u) { vis[u]=true; for(int i=h[u];i!=-1;i=e[i].nxt) { int v=e[i].v; if(vis[v]) continue; dep[v]=dep[u]+1; dfs(v); } }
广度优先遍历
bfs并没有dfs常用,用队列q来存储,每次遇到一个节点u,就入队,然后依次入队它的子节点,如果子节点也全部入队,就把u出队,又入队它第一个子节点的所有子节点,以此类推...
eg:
入队标蓝,出队标红,这张图的bfs模拟过程如下:
1 234
234 5
345
45 6
56 7
67 8
78 9
89
代码的实现很简单。
void bfs() { memset(d,0,sizeof(d)); q.push(1); d[1]=1; while(!q.empty()) { int u=q.front(),q.pop(); for(int i=h[u];i!=-1;i=e[i].nxt) { int v=e[i].v; if(d[v]) continue; d[v]=d[u]+1; q.push(v); } } }
我们可以看到图中有一个d数组,d[u]的作用是存储从1遍历到节点u所需经过的最少点数,qwq但是请大家不要妄想能用这个来求最短路,stO青君大佬。
bfs的遍历有两个性质:
1.在访问完节点i的所有节点后,才会开始访问节点i+1。
2.队列中的元素至多有两个层次的节点,第一部分属于i层,第二部分属于i+1层,所有i的节点排在i+1的节点之前,也就是说,bfs遍历满足两段性和单调性,这也是它的基本性质。
树的dfs序
一般地,我们在dfs时,在刚进入递归前和即将回溯之前各记录一次该点的编号,最后产生的2n长节点的序列就是dfs序,如果用a[N>>1]存储dfs序,l[ ]和r[ ]分别存递归前和回溯前的cnt,a[ l [ u ] ]和a[ r [ u ] ]这一段记录的就是u节点的子树,由此可以很好地把树上操作转化为区间问题~!
代码简短也好理解。
void dfs(int u) { vis[u]=true; l[u]=++cnt; // a[cnt]=u; for(int h[u];i!=-1;i=e[i].nxt) { int v=e[i].v; if(vis[v]) continue; dfs(v); } r[u]=++cnt; // a[cnt]=u; }
树的重心和子树大小
如果一棵树有v1~vk个节点,并且以v1~vk为根的子树大小是siz[v1]~siz[vk],那么以u为根的子树大小是siz[u]=siz[v1]+...+siz[vk],也就是说siz数组用来存储节点的子树大小(包括节点本身)。
树的重心指的是,如果我们把一个节点u从树中删除,那么原来的一棵树可能会分成若干个不同的部分,如果在删除一个节点后,剩下的所有子树中最大的一棵是最小的,那么这个被删去的节点称为树的重心,对于无权图,大小一般指的是节点个数。
代码简短好理解,如下。
void dfs(int u) { max_point=0; siz[u]=1; vis[u]=true; for(int i=h[u];i!=-1;i=e[i].nxt) { int v=e[i].v; if(vis[v]) continue; dfs(v);//要先遍历子节点才能找到子节点的siz值 qwq为什么总是忘记dfs 我有罪 siz[u]+=siz[v]; max_point=max(max_point,siz[v]); } max_point=max(max_point,n-siz[u]); if(max_point<ans) { ans=max_point; pos=u; } }
图的连通块划分
经过多次dfs,可以找出一张图的每一个连通块,不会连通块的同学可以去看看知识点再做两道题,这个很简单,不赘述了。
void dfs(int u) { scc[u]=cnt; for(int i=h[u];i!=-1;i=e[i].nxt) { int v=e[i].v; if(scc[v]) continue; scc[v]=cnt; dfs(v); } } int main() { for(int i=1;i<=n;i++) if(!scc[i]) dfs(i); }
拓扑排序
给定一张有向无环图,若一个由图中所有点构成的序列A满足:对于图中边(x,y),x在A中都出现在y之前,则称A是这张图的一个拓扑序,求解这个序列的过程我们称其为拓扑排序。
拓扑排序的实现过程很简单:
1.建立空的序列A
2.预处理所有的入度d[i],入队初始入度为0的点。
3.取出队头节点x,把x加入A的末尾。
4.对于从A出发的每条边(x,y),如果被减为0,就把y入队。
5.重复3,4步直到队列为空,这个时候我们就求出了拓扑序。
void add(int u,int v) { e[+cnt].v=v; e[cnt].nxt=h[u]; h[u]=cnt; d[v]++;//d数组表示v的入度,入度指的是以某一个点为终点的边数 } void topsort() { queue<int>q; for(int i=1;i<=n;i++) if(!d[i]) q.push(i);//入度为0则入队 while(!q.empty()) { int u=q.front(),q.pop(); a[++cnt]=u; for(int i=h[u];i!=-1;i=e[i].nxt) { int v=e[i].v; if(--d[v]) q.push(v);//--d[v]表示的是,u此刻为v的出度,如果减去u之后v是0个节点的入度,那么可以入队 } } } int main() { for(int i=1;i<=n;i++) printf("%d ",a[i]); }
最短路径
如题,这里求的是图论当中的最短路,最短路有多源和单源最短路。
Floyd算法
Floyd属于多源最短路算法,非常简单,但时间复杂度为o(n^3),太高了qwq,优点是简单好理解,思想是dp。
dp[k][i][j]=min(dp[k-1][i][j],dp[k-1][i][k]+dp[k-1][k][j);
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; #define N 1005 int d[N][N]; int n,m; int main() { scanf("%d%d",&n,&m); memset(d,0x3f,sizeof(d)); for(int i=1;i<=n;i++) d[i][i]=0; for(int i=1;i<=m;i++) { int u,v,w; scanf("%d%d%d",&u,&v,&w); d[u][v]=min(d[u][v],w); } for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) d[i][j]=min(d[i][j],d[i][k]+d[k][j]); for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++) printf("%d ",d[i][j]); printf("\n"); } return 0; }
Dijkstra算法
qwq这是基于贪心思想的算法,一个伪代码就能体现
d[v]=min(d[v],d[u]+w)(u为当前到起点路最短的点)
邻接矩阵实现
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; #define N 3005 int d[N],a[N][N],n,m,v[N]; void dijkstra() { memset(d,0x3f,sizeof(d)); d[1]=0; for(int i=1;i<n;i++) { int x=0; for(int j=1;j<=n;j++) if(!v[j]&&(x==0||d[j]<d[x])) x=j; v[x]=1; for(int j=1;j<=n;j++) d[j]=min(d[j],d[x]+a[x][j]); } } int main() { scanf("%d%d",&n,&m); memset(a,0x3f,sizeof(a)); for(int i=1;i<=n;i++) a[i][i]=0; for(int i=1;i<=m;i++) { int u,v,w; scanf("%d%d%d",&u,&v,&w); a[u][v]=min(a[u][v],w); } dijkstra(); for(int i=1;i<=n;i++) printf("%d ",d[i]); return 0; }
邻接链表+堆优化
#include<cstdio> #include<cstring> #include<queue> #include<algorithm> using namespace std; #define N 100005 struct node { int v,nxt,w; }e[N]; int d[N],h[N],v[N]; int n,m,cnt; priority_queue< pair <int,int> >q; void add(int u,int v,int w) { e[++cnt].v=v; e[cnt].w=w; e[cnt].nxt=h[u]; h[u]=cnt; } void dijkstra() { q.push(make_pair(0,1)); d[1]=0; while(q.size()) { int u=q.top().second; q.pop(); if(v[u]) continue; v[u]=1; for(int i=h[u];i!=-1;i=e[i].nxt) { int v=e[i].v; int w=e[i].w; d[v]=min(d[v],d[u]+w); q.push(make_pair(-d[v],v));//运用相反数思想实现一个小根堆 每次找到权值最小的u访问一个G(U,V) 满足贪心思想 } } } int main() { memset(h,-1,sizeof(h)); memset(d,0x3f,sizeof(d)); scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { int u,v,w; scanf("%d%d%d",&u,&v,&w); add(u,v,w); } dijkstra(); for(int i=1;i<=n;i++) printf("%d ",d[i]); return 0; }
SPFA算法
qwq俗话说得好:SPFA死了,所以对于它不做过多介绍,只提一句:
边(u,v,w)满足三角形不等式d[v]<=d[u]+w;
#include<cstdio> #include<queue> #include<cstring> #include<algorithm> using namespace std; #define N 100005 struct node { int nxt,v,w; }e[N]; int n,m,d[N],h[N],cnt,v[N]; void add(int u,int v,int w) { e[++cnt].v=v; e[cnt].w=w; e[cnt].nxt=h[u]; h[u]=cnt; } queue<int>q; void spfa() { memset(d,0x3f,sizeof(d)); d[1]=0,v[1]=1; q.push(1); while(q.size()) { int u=q.front();q.pop(); v[u]=0; for(int i=h[u];i!=-1;i=e[i].nxt) { int y=e[i].v; int w=e[i].w; d[y]=min(d[y],d[u]+w); if(!v[y]) q.push(y),v[y]=1; } } } int main() { scanf("%d%d",&n,&m); memset(h,-1,sizeof(h)); for(int i=1;i<=m;i++) { int u,v,w; scanf("%d%d%d",&u,&v,&w); add(u,v,w); } spfa(); for(int i=1;i<=n;i++) printf("%d ",d[i]); return 0; }