图论

最小生成树

一.Kruskal

1.定理:

任意一棵最小生成树一定包含无向图中权值最小的边。

2.步骤:

(1)建立并查集,每个点单独构成集合。
(2)边从小到大排序,依次扫描每条边。
(3)\(x\)\(y\) 属于同一集合,忽略这条边。
(4)否则,合并\(x\)\(y\)所在的集合,累加边权。
(5)所有边扫完后,即为答案。

3.时间复杂度:

\(O(mlogm)\)

4.代码:

点击查看代码
int n,m,f[100005],ans;
struct edge{
	int u;
	int v;
	int w;
}e[200005];
bool cmp(edge a,edge b){
	return a.w<b.w;
}
int get(int x){
	if(f[x]==x) return x;
	return f[x]=get(f[x]);
}
void Kruskal(){
	sort(e+1,e+1+m,cmp);
	for(int i=1;i<=n;i++) f[i]=i; 
	for(int i=1;i<=m;i++){
		int u=get(e[i].u);
		int v=get(e[i].v);
		if(u==v) continue;
		f[u]=v;
		ans+=e[i].w;
	}
	cout<<ans;
}

二.Prim

1.区分:

与 Kruskal 算法(由若干小集合#去合成更大的集合,过程中有若干个集合)不同的是,Prim 算法总是维护最小生成树的一部分(由一个总集合
去拓展点,使该集合更大,过程中始终有一个集合)。

2.步骤:

(1)最初仅确定1号节点属于最小生成树。
(2)类似于蓝白点思想,每次只拓展一个离总集合最近的一个元素。
(3)拓展 \(n-1\) 次求出答案。

3.时间复杂度:

\(O(n^2)\)
二叉堆优化:\(O(mlogn)\)

4.适用范围:

多用于稠密图,尤其是完全图的最小生成树求解。

5.代码:

点击查看代码
int a[3005][3005],d[3005],n,m,ans;//a用来存边 
bool v[3005];
void Prim(){
	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++)
		    if(!v[j]) d[j]=min(d[j],a[x][j]); 
	}
	for(int i=2;i<=n;i++) ans+=d[i];
	cout<<ans;
}

最短路

一.Dijkstra

1.步骤:

(1)初始化:\(dist_1=0\),其余节点的 \(dist\) 置为正无穷。
(2)找出一个未被标记的,\(dist\) 值最小的节点 \(x\),然后标记节点 \(x\)
(3)扫描 \(x\) 的所有出边,松弛其他节点的 \(dist\)
(4)重复(2)(3),直到所有节点都被标记。

2.时间复杂度:

\(O(n^2)\)
二叉堆优化:\(O((n+m)logn)\)

3.使用限制:

不能有负边。

4.代码:

点击查看代码
int a[3005][3005],dist[3005],n,m;
bool v[3005];
void Dijkstra(){
	memset(d,0x3f,sizeof(d));
	memset(v,0,sizeof(v));
	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]);
 	}
} 

二.Bellman-Ford

1.原理:

对于图中的某一条边 \((x,y,z)\),有 \(dist[y]<=dist[x]+z\) 成立,则称该边满足三角形不等式。若所有边都满足三角形不等式,则 \(dist\) 数组就是所求最短路。

2.步骤:

基于迭代思想。
(1)扫描所有边 \((x,y,z)\),若 \(dist[y]>dist[x]+z\),则用 \(dist[x]+z\) 更新 \(dist[y]\)
(2)重复上述步骤,直到没有更新操作发生。

3.时间复杂度:

\(O(nm)\)

4.适用范围:

可处理有负边/负环的最短路,有限路线最短路。

5.代码:

点击查看代码
int a[3005][3005],dist[3005],backup[3005];
struct edge{
	int u;
	int v;
	int w;
}e[200005];
void Bellman_Ford(){
	memset(dist,0x3f,sizeof(dist));
	dist[1]=0;
	for(int i=1;i<=k;i++){
		memcpy(backup,dist,sizeof(dist));
		for(int j=1;j<=m;j++){
			int u=e[i].u;
			int v=e[i].v;
			int w=e[i].w;
			dis[v]=min(dist[v],backup[a]+w);
		} 
	}
}

三.SPFA

1.步骤:

队列优化的 Bellman-Ford 算法(避免了 Bellman-Ford 对不需要扩展的节点的冗余扫描)。
(1)建立一个队列,最初只有起点。
(2)取出队头节点 \(x\),扫描它的所有出边 \((x,y,z)\),若 \(dist[y]>dist[x]+z\),则使用 \(dist[x]+z\) 更新 \(dist[y]\)。同时,若 \(y\) 不在队列中,则把 \(y\) 入队。
(3)重复上述步骤,直至队列为空。

2.时间复杂度:

随机图:\(O(km)\)\(k\) 是一个较小的常数)。
特殊构造图:\(O(nm)\)

3.适用范围:

可处理负边。

4.代码:

点击查看代码
const int N=100010,M=1000010;
int head[N],ver[N],edge[M],Next[M],d[N];
int n,m,tot;
queue<int> q;
bool v[N];
void add(int x,int y,int z){
	ver[++tot]=y;
	edge[tot]=z;
	Next[tot]=head[x];
	head[x]=tot;
}
void Spfa(){
	memset(d,0x3f,sizeof(d));
	d[1]=0;v[1]=1;
	q.push(1);
	while(!q.empty()){
		int x=q.front();
		q.pop();
		for(int i=head[x];i;i=Next[i]){
			int y=ver[i],z=edge[i];
			if(d[y]>d[x]+z){
				d[y]=d[x]+z;
				if(!v[y]) q.push(y),v[y]=1;
			}
		}
	}
}

四.Floyd

负环

负环判定

一.Bellman-Ford 判负环

若经过 \(n\) 次迭代,算法仍未结束(仍有能产生更新的边),则图中存在负环。

\(n-1\) 轮迭代以内,算法结束(所有边满足三角形不等式),则图中无负环。

二.SPFA 判负环

1.设 \(cnt[x]\) 表示从1到 \(x\) 的最短路径包含的边数,\(cnt[1]=0\)。当执行更新 \(dist[y]=dist[x]+z\) 时,同样更新 \(cnt[y]=cnt[x]+1\)。此时发现 \(cnt[y]>=n\),则图中有负环。若算法正常结束,则图中没有负环。
2.记录每个点入队的次数,次数达到 \(n\) 时说明有负环。

差分约束

1.原理:

将约束条件 \(X_i-X_j<=c_k\) 变形为 \(X_i<=X_j+c_k\)。这与单源最短路径问题中的三角形不等式 \(dist_y<=dist_x+z\) 非常相似。因此,可以把变量 \(X_i\) 看作有向图的一个节点 \(i\) 连一条长度为 \(c_k\) 的有向边。

2.步骤:

(1)先求一组负数解,之后增加0节点,令 \(X_0=0\),这样就多了 \(N\) 个形如 \(X_i-X_0<=0\) 的约束条件,应该从节点0向每个节点 \(i\) 连一条长度为0的有向边。
(2)\(dist[0]=0\),以0为起点求单源最短路。若图中存在负环,则给定的差分约束系统无解。否则,\(X_i=dist[i]\)就是差分约束系统的一组解。

Tarjan 与无向图连通性

零.前置知识

1.\(dfn_x\)->时间戳(访问的先后顺序)。
2.\(low_x\)->追溯值
(1)初始 \(low_x=dfn_x\)考虑从 \(x\) 出发的每条边 \((x,y)\)
(2)若在搜索树上 \(x\)\(y\) 的父节点,则令 \(low_x=min(low_x,low_y)\)
若无向边 \((x,y)\) 不是搜索树上的边,则令 \(low_x=min(low_x,dfn_y)\)

一.桥(割边)

1.定义:

从图中删去边 \(e\) 之后,\(G\) 分裂成两个不相连的子图,则称 \(e\)\(G\) 的桥或割边。

2.判定法则

无向边 \((x,y)\) 是桥,当且仅当搜索树上存在 \(x\) 的一个子节点 \(y\),满足:
\(dfn[x]<low[y]\)

3.代码:

点击查看代码
const int SIZE=100010;
int head[SIZE],ver[SIZE*2],Next[SIZE*2];
int dfn[SIZE],low[SIZE],n,m,tot,sum;
bool bridge[SIZE*2];
void add(int x,int y){
	ver[++tot]=y;
	Next[tot]=head[x];
	head[x]=tot;
} 
void tarjan(int x,int in_edge){
	dfn[x]=low[x]=++dfc;
	for(int i=head[x];i;i=Next[i]){
		int y=ver[i];
		if(!dfn[y]){
			tarjan(y,i);
			low[x]=min(low[x],low[y]);
			if(low[y]>dfn[x])
			    bridge[i]=bridge[i^1]=1; 
		}else if(i!=(in_edge^1))
		    low[x]=min(low[x],dfn[y]);
	}
}
for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0);

二.割点

1.定义:

从图中删去节点 \(x\),以及所有与 \(x\) 关联的边之后,\(G\) 分裂成两个或两个以上不相连的子图,则称 \(x\)\(G\) 的割点。

2.判定法则:

(1)\(x\) 不是根节点,则 \(x\) 是个点当且仅当搜索树上存在 \(x\) 的一个子节点 \(y\) 满足 \(dfn[x]<=low[y]\)
(2)\(x\) 是根节点,则 \(x\) 是割点当且仅当搜索树上存在至少两个子节点 \(y_1,y_2\) 满足上述条件。

3.代码:

点击查看代码
const int SIZE=100010;
int head[SIZE],ver[SIZE*2],Next[SIZE*2];
int dfn[SIZE],low[SIZE],stack[SIZE];
int n,m,tot,num,root;
bool cut[SIZE];
void add(int x,int y){
	ver[++tot]=y;
	Next[tot]=head[x];
	head[x]=tot;
}
void tarjan(int x){
	dfn[x]=low[x]=++num;
    int flag=0;
    for(int i=head[x];i;i=Next[i]){
    	int y=ver[i];
		if(!dfn[y]){
			tarjan(y);
			low[x]=min(low[x],low[y]);
			if(low[v]>=dfn[x]){
				flag++;
				if(x!=root||flag>1) cnt[x]=true;
			}
		}else low[x]=min(low[x],dfn[y]); 
	}  
}

三.无向图的双连通分量

1.定理

一张无向连通图是“点双连通图”,当且仅当满足下面两个条件之一:
(1)图中顶点数不超过2。
(2)图中任意两点都同时包含在至少一个简单环中。

posted @ 2022-12-03 08:33  Travller  阅读(46)  评论(0)    收藏  举报