图论算法部分总结

以下内容参考《算法竞赛入门》

拓扑排序

Ⅰ基于BFS:
算法步骤简述:
1.找到所有入度为0的点,放入队列。
2.将队列中的元素出对一个,令其相邻节点中除了入度为0的节点,剩余节点入度减一,如果减为0,则加入队列。
3.重复直到结束。
无解的判断:如果队列已经为空,还有节点的入度不为0,则不是有向无环图DAG,不存在拓扑排序。
Ⅱ基于DFS:
算法步骤简述:
1.遍历所有节点,对没有走过的且入度为0的点进行dfs。
2.dfs过程中,假设某一相邻入度为不为0的节点编号为i,先令其入度减一,再对其进行dfs。
3.在当前节点的dfs中,上述2过程结束后判断自己的入度是否为0,如果为0则加入队列或输出。
无解判断同上。

2.欧拉路

定义:
欧拉路:从图中某个点出发遍历整个图,对于每条边通过且只通过一次。
欧拉回路:起点和终点相同的欧拉路。
无向连通图判断条件:如果图中所有点都是偶点(度数为偶数的点,奇点同理),则存在欧拉回路且任意一点都可做起点和终点。如果图中除了两个奇点剩下全为偶点,则存在欧拉路,且其中一个奇点为起点,另一个为终点。别的情况下不存在欧拉路与欧拉回路。
有向连通图判断条件:一个点的出度记为1,入度即为-1。如果所有点的出度与入度和为0,则存在欧拉回路且任意一点都可做起点和终点。如果一个节点度数为1一个节点度数为-1,其他节点度数全为0,则存在欧拉路,且度数为1的为起点,-1的为终点。
输出欧拉回路的模板:

void euler(int u){
	int v;
	for(v=1;v<=n;v++){//遍历所有邻居
		if(G[u][v]!=0){//如果有边,也就是邻居(可能会有重边)
			G[u][v]--;
			G[v][u]--;
			euler(v);
			System.out.println(v+" "+u);
		}
	}
}

需要先判断是否存在欧拉回路。
对于上面输出语句,如果放在递归前面,则可能输出一个环一个环,是不正确的。
可以证明输出语句放在最后是正确的:
先直接递归到最后,又一定回到了开始节点。现在可能一些环接在了某一些节点上,此时开始节点一定不存在其他可以走的边,因为如果还存在其他的未走过的边,则可以继续递归,我们把最后一条边当作最后欧拉回路的第一条边,顺着之前的环回退,如果当前节点还有边未走过,则一定可以将其当作起点,找到一个剩下没走过边的欧拉回路,假设现在已经确定了最后欧拉回路的前i条边,则令当前节点的欧拉回路作为第i+1,i+2…i+k条边,此节点找完,继续沿着最开始的环回退,依次递归,最后就是欧拉回路的环。

无向图的连通性

定义:
连通分量:所有能互通的点组成一个连通分量。
割点:删除连通分量的一个点,则连通分量将会分裂成两个或更多。(也就是删除后存在两点不再互相连通)
割边:删除连通分量中的一个边,则连通分量分成了两个,则称为割边。
要计算一个图中的割点,我们需要先通过dfs生成一个深度优先生成树,也就是将图转化成树。
要用到两个定理
定理1:如果根节点是割点,那么它一定存在两个及两个以上的子节点。解释:如果根节点删掉,则多个子节点不连通,则转化为多个连通分量,所以是割点。(注意,此生成树是dfs深度 生成树,如果两个节点有除了根节点的别的路,则一定在一颗树上而不是两颗子树)
定理2:生成树上的非根节点u是一个割点,则当且仅当存在子节点v,v和v的子孙节点都没有与u的祖先节点相连的边。解释:如果v或v的子孙节点不存在一条与u的祖先节点相连的边,则把u删除后,v和v的子孙无法与u的祖先节点相连,将会生成更多的连通分量,所以u是一个割点。
我们现在需要使用两个辅助数组num与low。num[i]代表当前节点是dfs中第几个访问的,low[i]代表当前节点及子孙节点中除了树边,相连的节点中的num最小值。
现在应用定理2:一个节点u为一个割点,当且仅当存在一个子节点v,low[v]>=num[u];一个节点(u,v)为一条割边,当且仅当存在一个子节点v,low[v]>num[u];
证明:如果一个节点u的子节点为v,low[v]的值大于等于u,则可知一定存在v及v的子节点没有连向u祖先节点的边,因为如果存在这样的边,则low[v]的值一定大于num[u],根据定理2,u是一个割点。
模板:

void dfs(int u,int par){//u是当前节点的编号 par是父节点的编号
	low[u]=num[u]=p++;//p的值从0开始 表示是第几个访问的节点 初始令low与num值相同
	int child=0;//孩子数目 用以判断根节点
	for(int v:G[u]){//G为一个ArrayList数组 存放边
		if(num[v]!=0){//没走过 遍历
			child++;
			dfs(v,u);
			low[u]=Math.min(low[u],low[v]);
			if(u!=1&&low[v]>=num[u]){
				isGD[u]=true;//isGD表示是不是割点
			}
		}else{
			low[u]=Math.min(low[u],num[v]);//判断是不是更早的节点
		}
	}
	if(u==1&&child>1){//判断根节点
		isGD[1]=true;
	}
}

双连通分量

定义:
点双连通分量:去掉任意一个点,其他点仍是连通的。
边双连通分量:去掉任意一条边,图仍然是连通的。
算法:
求点连通分量及个数:
已知一个割点至少是两个点双连通分量的公共点。
Tarjan算法:用栈存储走过的边,和求割点一样进行dfs,把遍历过的点存储起来,dfs之后,每次遇到num[u]>=low[v],就将栈里弹出到子节点,弹出来的节点则为一个点双连通分量。
求边双连通分量个数:还是用dfs,其中所有的点生成一个low值,low值相同的点在同一个边双连通分量中,dfs结束后,有多少low值就有多少个边双连通分量。(边双连通分量可缩为一点)

有向图的连通性

定义:
强连通:在有向图中,任意两点互相可达,则称之为强连通图。
强连通分量:如果一个有向图不是强连通图,则可以将其分为多个子图,每个子图内部是强连通的,且子图已经扩展到最大,不能与子图外的任意点强连通,称这样的一个极大强连通子图是该有向图的强连通分量SCC。
算法1:Kosaraju算法(时间复杂度O(V+E))
原理1:一个有向图G,把G所有的边反向,建立反图rG,反图rG不会改变原图G的强连通性。图G的SCC数量等于图rG的SCC数量。
原理2:对原图和反图各做一次DFS,可以确定SCC数量。
算法步骤:
1.对图进行dfs,按照dfs完成顺序得到编号
2.按照编号从大到小对反图进行dfs,能到达的节点即为一个强连通分量。
3.重复2,直到所有点都遍历过。
说明:因为是从大到小进行遍历,所以大的节点u可以到达连通的小的节点v,如果在反图中u还可以到达v,证明v也可以到达u,即存在一条路径从u到v且存在一条路径从v到u,所以其为强连通的。
算法2:Tarjan算法(时间复杂度也为O(V+E))
与前面求割点的算法一样,先求其low数组与num数组,所有的low值相同的节点的集合即为一个强连通分量。
说明:我们可知,如果某一节点v的low值为u,则u能到达v,且v可以到达u,所以u,v是可以互相到达的;所有相同low值的均可以相互到达,所以为一强连通分量。

2-SAT问题

关于什么是2-SAT问题,可以上网自己查一下,这里不再详细描述问题。
解决方法:
假设A,B不能同时出现,则可以得到推论A出现!B必然出现,B出现!A必然出现,我们用箭头表示前者出现后者必然出现的关系,例如A->!B,B->!A。将所有矛盾关系转化为有向图。
求解算法:
1.先计算出所有强连通分量SCC,如果所有的SCC内部均没有矛盾,则存在可行解,否则,则不存在可行解。
2.将一个SCC看作一个点,求出其有向无环图DAG,求出反图的拓扑排序,按从先结束的点开始选,每次选择的时候排除掉矛盾的点,最后选择的即为合法的组合。
说明:对于其中第1点,如果存在矛盾A与!A在同一个SCC中,我们不论选择A或者!A都将陷入这个SCC,也就是必将矛盾,所以所有的SCC内部均要求无矛盾。
2-SAT问题我也有点懵逼,网上有许多大神讲解,可以看一下。

单源最短路径

1.Bellman-Ford
时间复杂度:O(nm)可以检查负环。
思路:每个节点问其他节点到所有相邻节点的距离,看看是否可以更新。最多进行n次询问,就可知道到达其他任意n-1个节点的距离。在n轮之后,如果还存在d[j]>d[i]+dis[i][j],则存在负环。
核心代码:

for(int k=0;k<n;k++)
	for(int i=0;i<n;i++)
		for(int j:G[i]){//G存储i的相邻边
			if(d[j]>d[i]+dis[i][j])//dis存储边
				d[j]=d[i]+dis[i][j];
		}

2.SPFA
Bellman-Ford的升级版。
思路:调整一个节点的最短距离后,紧接着调整其相邻节点。
算法步骤:
1.将起点s入队,计算它到所有邻居的距离,将其入队。
2.队列出队一个,更新其相邻节点,如果可以更新且不在队列中,入队。
3.直到队列为空。
PS:其中的节点可能多次入队出队,是一种不稳定的算法。
代码:

while(!que.isEmpty()){
	int u=que.poll();
	inq[u]=false;//inq表示是否在队列中
	for(int v:G[u]){//遍历子节点
		if(d[v]>d[u]+dis[u][v]){
			d[v]=d[u]+dis[u][v];
			if(!inq[v]){
				que.add(v);
				Neg[v]++;//判断负圈
				if(Neg[v]>n){
					return -1;
				}
			}
		}
	}
}

3.dijkstra
dijkstra是一种高效且稳定的算法,每一次都可以收敛,其算法复杂度为O(mlgn)。但是无法判断负圈。
算法思路,每次选择一个当前距离开始节点s最短的点,由于之前最短的点都已经选过了,此时最短的点不可能通过走其他没有走过的点再拐回来得到更短的距离,所以此时的距离一定是最短的。
算法步骤(其中的队列使用优先队列):
1.将开始节点与相邻节点的边加到队列中
2.每次出队一个距离最近的边,如果没有走过,设置其走过,将其相邻的可以更新距离的节点的边加到队列中。
3.直到队列为空。
核心代码:

while(!que.isEmpty()){
	Edge e=que.poll();
	int val=e.val;
	int to=e.to;
	if(d[to]<val)//这个边过时了,不进行更新
		continue;
	for(int j:G[to]){
		if(d[j]>d[to]+dis[to][j]){
			d[j]=d[to]+dis[to][j];
			que.add(new Edge(j,d[j]));
		}
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值