基础图论指南 QwQ

前置知识

在了解图论之前,还需要知道怎么存图。

vector

vector<int> G[MAXN]来存图。 G i G_i Gi 表示从 i i i 出发,能够到达的点的数组。空间开销相较于链式前向星较大。也可以将 vector替换为其他 STL 容器,如 listunordered_setdeque等。list的写法空间更优,常数较小,但是 vector更大众一点。

链式前向星

由于空间小、常数小,深受众多 OIer 的喜爱。本质上是通过跳链的方式,但是不是遍历 ( u → v ) → ( v → o ) (u\to v)\to(v\to o) (uv)(vo),而是 ( u → v i ) → ( u → v i + 1 ) (u\to v_i)\to(u\to v_{i+1}) (uvi)(uvi+1)。这种方式遍历图需要至少两个元素: t o , n x t to,nxt to,nxt t o to to 表示这一条边通向哪里, n x t nxt nxt 表示下一条边是哪里。

这种存图方式还需要一个数组 h e a d head head h e a d i head_i headi 表示 i i i 的上一条边是哪里,如果 n x t nxt nxt 指向在同一个 i i i h e a d i head_i headi,就能够用 n x t nxt nxt 把所有开头为 i i i 的边串联起来了。下面,给出链式前向星的模板:

struct node{
	int /*from,Kruskal 需要初始点*/to,nxt;
//	ll dis;比如 Dijiestra 需要边权 

}edge[MAXM<<1];//MAXM 表示最多存的边,开 2 倍表示双向边 
int cnt,head[MAXN];//MAXN 表示点数
#define rep(from) for(int i=head[from];i;i=edge[i].nxt)//遍历图
inline void addedge(int from,int to/*,ll dis*/){
	edge[++cnt].to=to;
	edge[cnt].nxt=head[from];
//	edge[cnt].dis=dis;
//	edge[cnt].from=from;
	head[from]=cnt;//存上一条边 
} 

邻接矩阵

邻接矩阵用 d p u , v dp_{u,v} dpu,v 表示一条边 ( u → v ) (u\to v) (uv) 的路径。如果无权图的话,用 1 1 1 代表有,用 0 0 0 代表没有。如果是有权图,用 − ∞ -\infty 表示。

由于速度慢,通常用于 Floyed 算法,为 dp 式转移。

最短路

Floyed

Floyed 的复杂度是 O ⁡ ( n 3 ) \operatorname{O}(n^3) O(n3),本质上是 dp 转移式。枚举初始点 u u u、结尾点 v v v、中转点 k k k,用 d p u , v = m i n ( d p u , v , d p u , k + d p k , v ) dp_{u,v}=min(dp_{u,v},dp_{u,k}+dp_{k,v}) dpu,v=min(dpu,v,dpu,k+dpk,v) 来转移。

ll dp[MAXN][MAXN];//转移
inline void Floyed(){
	for(int u=1;u<=n;++u){
		for(int v=1;v<=n;++v){
			for(int k=1;k<=n;++k){
				dp[u][v]=min(dp[u][v],dp[u][k]+dp[k][v]);//转移式 
			}
		}
	}
}

Bellman-Ford 和 SPFA

Bellman-Ford

Bellman-Ford 是基于松弛的思想进行的算法。可以对最短路进行松弛来优化。

对于边 u → v u\to v uv 的边长,设 d i s ( n ) dis(n) dis(n) 为长度,那么则有 d i s ( v ) = min ⁡ ( d i s ( v ) , d i s ( u ) + w ( u , v ) ) dis(v)=\min(dis(v),dis(u)+w(u,v)) dis(v)=min(dis(v),dis(u)+w(u,v))。我们尝试对边上每一条边进行松弛操作。

时间复杂度证明:要遍历 m m m 跳边,每一次松弛最多会减少一个可能错误的点,最多有 n n n 个点,所以时间复杂度为 O ⁡ ( n m ) \operatorname{O}(nm) O(nm)

struct node{
	int to,nxt;
	ll dis;
}edge[MAXM<<1]; 
int n,m,cnt,head[MAXN];
ll dis[MAXN];
inline void addedge(int from,int to,ll dis){
	edge[++cnt].to=to;
	edge[cnt].dis=dis;
	edge[cnt].nxt=head[from];
	head[from]=cnnt;
}
inline void Bellman_Ford(int s){
	memset(dis,0x7f,sizeof(dis));
	dis[s]=0;
	for(int i=1;i<=n;++i){
		bool f=false;//可不可以松弛 
		for(int j=head[i];j;j=edge[j].nxt){//遍历图 
			int to=edge[j].to;
			if(dis[to]>dis[i]+edge[j].dis){
				dis[to]=dis[i]+edge[j].dis;//三角形不等式
				f=true; 
			}
		}
		if(!f){
			break;//没有可以松弛的了 
		}
	} 
}
SPFA

关于 SPFA,它死了

SPFA 是基于 Bellman-Ford 的优化。很明显,每一次可以进行松弛操作的点一定是上一个松弛操作过的点能够到达的点。所以,用队列进行 bfs 即可。(不知道为什么不可以用 A-star 优化)。

SPFA 的最大时间复杂度仍然是 O ⁡ ( n m ) \operatorname{O}(nm) O(nm),所以在没有负环的时候尽量使用下面要讲的 Dijiestra。

struct node{
	int to,nxt;
	ll dis;
}edge[MAXM<<1]; 
int n,m,cnt,head[MAXN];
ll dis[MAXN];
bool vis[MAXN];
inline void addedge(int from,int to,ll dis){
	edge[++cnt].to=to;
	edge[cnt].dis=dis;
	edge[cnt].nxt=head[from];
	head[from]=cnnt;
}
inline void SPFA(int s){
	memset(dis,0x7f,sizeof(dis));
	memset(vis,0,sizeof(vis));
	dis[s]=0;
	vis[s]=true;
	queue<int> q;
	q.push(s);
	while(!q.empty()){
		int from=q.front();
		q.pop();//bfs 经典队列 
		vis[from]=false;
		for(int i=head[from];i;i=edge[i].nxt){//遍历边 
			int to=edge[i].to;
			if(dis[to]>dis[from]+edge[i].dis){//三角形不等式 
				dis[to]=dis[from]+edge[i].dis;
				if(!vis[to]){//继续扩展 
					q.push(to);
					vis[to]=true;
				}
			}
		}
	} 
}

Dijiestra

Dijiestra 本质上是一种贪心算法,只能够处理无负边权的最短路。

首先,把最短的路径出队,对相应的边进行松弛操作。然后,将相应的边假如队列,继续后续的松弛。

Dijiestra 还有很多种写法,一一介绍:

  • 暴力:直接找最小的节点,时间复杂度为 O ⁡ ( n 2 + m ) \operatorname{O}(n^2+m) O(n2+m)
  • 堆优化:用二叉堆来维护最小值,可以做到 m m m 次入队, n n n 次出队,时间复杂度 O ⁡ ( ( n + m ) log ⁡ m ) \operatorname{O}((n+m)\log m) O((n+m)logm)
  • Fibonacci 堆优化:插入是 O ⁡ ( 1 ) \operatorname{O}(1) O(1) 的,所以达到了 O ⁡ ( m + n log ⁡ n ) \operatorname{O}(m+n\log n) O(m+nlogn),但是由于实现难度较高,并且题目基本不用 Fibonacci 堆卡 log ⁡ \log log,所以不常使用。
  • 线段树优化:将插入堆变成单点修改,查询最小值改成线段树全局查找最小值,时间复杂度 O ⁡ ( ( n + m ) log ⁡ n ) \operatorname{O}((n+m)\log n) O((n+m)logn)

下面给出经典的堆优化和线段树优化代码:

堆优化
struct node{
	int to,nxt;
	ll dis;
}edge[MAXM<<1];
int cnt,head[MAXN];
bool vis[MAXN];
ll dis[MAXN];
inline void addedge(int from,int to,ll dis){
	edge[++cnt].to=to;
	edge[cnt].dis=dis;
	edge[cnt].nxt=head[from];
	head[from]=cnt;
} 
inline void Dijiestra(int s){
	priority_queue<pair<ll,int> > q;
	memset(vis,0,sizeof(vis));
	memset(dis,0x3f,sizeof(dis));
	q.push(make_pair(0ll,s));
	dis[s]=0;
	while(!q.empty()){
		int front=q.top().second;//每次出队的是最小值 
		q.pop();
		if(vis[front]){
			continue;//剪枝 
		}
		vis[front]=true;
		for(int i=head[front];i;i=edge[i].nxt){
			int to=edge[i].to;
			if(dis[to]>dis[front]+edge[i].dis){
				dis[to]=dis[front]+edge[i].dis;
				q.push(make_pair(dis[to],to)));//松弛操作 
			}
		}
	}
}
线段树优化
struct node{
	int to,nxt;
	ll dis;
}edge[MAXM<<1];
int cnt,head[MAXN],pos[MAXN<<2];
bool vis[MAXN];
ll dis[MAXN],ans[MAXN],tree[MAXN<<2];
inline void addedge(int from,int to,ll dis){
	edge[++cnt].to=to;
	edge[cnt].dis=dis;
	edge[cnt].nxt=head[from];
	head[from]=cnt;
} 
inline void push_up(int root){
	if(tree[root<<1]<tree[root<<1|1]){
		tree[root]=tree[root<<1];
		pos[root]=pos[root<<1];
	}else{
		tree[root]=tree[root<<1|1];
		pos[root]=pos[root<<1|1];
	}
}
void build(int root,int l,int r){
	if(l==r){
		tree[root]=INT_MAX;
		pos[root]=l;
		return;
	}
	int mid=(l+r)>>1;
	build(root<<1,l,mid);
	build(root<<1|1,mid+1,r);
	push_up(root); 
}
void change(int root,int pos,ll k,int l,int r){
	if(l==pos&&r==pos){
		tree[root]=k;
		return;
	}
	int mid=(l+r)>>1;
	if(pos<=mid){
		change(root<<1,pos,k,l,mid); 
	}else{
		change(root<<1|1,pos,k,mid+1,r);
	}
	push_up(root);
}
inline void Dijiestra(int s){
	fill(dis+1,dis+n+1,INT_MAX);
	build(1,1,n);//建树 
	dis[s]=0ll;
	for(int i=1;i<=n;++i){
		ans[s]=dis[s];
		ll disu=dis[s];
		change(1,s,ll(INT_MAX)+1ll,1,n);
		for(int j=head[s];j;j=edge[j].nxt){
			int to=edge[j].to;
			if(dis[to]<=INT_MAX&&dis[to]>dis[s]+edge[j].dis){
				dis[to]=dis[s]+edge[j].dis;
				change(1,to,dis[to],1,n);//修改 
			}
		}
		s=pos[1];
	}
}

Johnson

Johnson 的本质是重新赋权。先新建一个虚拟节点 0 0 0,向其他节点延伸出 n n n 条边权为 0 0 0 的点,用 SPFA 或者 Floyed 跑 0 0 0 到其他节点的最短路。重新赋权,比如有边 u → v u\to v uv,长度为 w w w,则重新赋为 w + d i s u − d i s v w+dis_u-dis_v w+disudisv,然后再跑 Dijiestra。

由于本人不会 Jonhson 全员最短路算法,可以参照洛谷题解或者 OI wiki,都很优质。

例题1

这一道题目要求的就是中转距离,根据时间多加的点进行 O ⁡ ( n ) \operatorname{O}(n) O(n) 的转移即可。

#include<bits/stdc++.h>
#define MAXN 202
#define INF 1e9
using namespace std;
int f[MAXN][MAXN],a[MAXN];
int main(){
	int n,m,q,p=0;
	scanf("%d %d",&n,&m);
	for(int i=0;i<n;++i){
		scanf("%d",&a[i]);
		for(int j=0;j<n;++j){
			f[i][j]=INF;
		}
		f[i][i]=0;
	}
	while(m--){
		int x,y,len;
		scanf("%d %d %d",&x,&y,&len);
		f[x][y]=f[y][x]=len;
	}
	scanf("%d",&q);
	while(q--){
		int x,y,len;
		scanf("%d %d %d",&x,&y,&len);
		while(a[p]<=len&&p<n){
			for(int i=0;i<n;++i){
				for(int j=0;j<n;++j){
					f[i][j]=f[j][i]=min(f[i][j],f[i][p]+f[p][j]);
				}
			}
			++p;
		}
		if(a[x]>len||a[y]>len||f[x][y]==INF){
			puts("-1");
		}else{
			printf("%d\n",f[x][y]);
		}
	}
	return 0;
}

例题2

很让人坠机的样子,因为建边都建不完。发现题目给的异或并不是“在 C++ 中用 ^表示”,而是放了链接,说明这一道题目跟异或的性质有关。

我们发现有一些边可以被其他的边替代,而且按位来算正好是 n log ⁡ n n\log n nlogn 的!再推一下,我们发现形如 5 5 5 的二进制是 101 101 101,那么可以用 4 ⊕ 1 = 100 ⊕ 001 = 101 = 5 4\oplus 1=100\oplus001=101=5 41=100001=101=5 替代。那么,只需要处理位上最高位是 1 1 1 的,其他的都可以组合而成。

之后,跑 Dijiestra 就可以了。堆优化比较呛,要吸氧。这里放上线段树优化的代码。

#include<bits/stdc++.h>
#define MAXN 1000001
#define MAXM MAXN*22
using namespace std;
typedef long long ll;
struct node{
	int from,nxt,to;
	ll dis;
}edge[MAXM];
int n,m,c,s,t,cnt,head[MAXN],pos[MAXN<<2];
ll dis[MAXN],ans[MAXN],tree[MAXN<<2];
inline void addedge(int x,int y,ll dis){
	edge[++cnt].to=y;
	edge[cnt].from=x;
	edge[cnt].nxt=head[x];
	edge[cnt].dis=dis;
	head[x]=cnt;
}
inline void push_up(int root){
	if(tree[root<<1]<tree[root<<1|1]){
		tree[root]=tree[root<<1];
		pos[root]=pos[root<<1];
	}else{
		tree[root]=tree[root<<1|1];
		pos[root]=pos[root<<1|1];
	}
}
void build(int root,int l,int r){
	if(l==r){
		tree[root]=INT_MAX;
		pos[root]=l;
		return;
	}
	int mid=(l+r)>>1;
	build(root<<1,l,mid);
	build(root<<1|1,mid+1,r);
	push_up(root); 
}
void change(int root,int pos,ll k,int l,int r){
	if(l==pos&&r==pos){
		tree[root]=k;
		return;
	}
	int mid=(l+r)>>1;
	if(pos<=mid){
		change(root<<1,pos,k,l,mid); 
	}else{
		change(root<<1|1,pos,k,mid+1,r);
	}
	push_up(root);
}
inline void Dijiestra(int s){
	fill(dis+1,dis+n+1,INT_MAX);
	build(1,0,n);
	dis[s]=0ll;
	for(int i=1;i<=n;++i){
		ans[s]=dis[s];
		ll disu=dis[s];
		change(1,s,ll(INT_MAX)+1ll,1,n);
		for(int j=head[s];j;j=edge[j].nxt){
			int to=edge[j].to;
			if(dis[to]<=INT_MAX&&dis[to]>dis[s]+edge[j].dis){
				dis[to]=dis[s]+edge[j].dis;
				change(1,to,dis[to],1,n);
			}
		}
		s=pos[1];
	}
}
int main(){
	scanf("%d %d %d",&n,&m,&c);
	while(m--){
		int x,y;
		ll dis;
		scanf("%d %d %lld",&x,&y,&dis);
		addedge(x,y,dis);
	}
	for(int i=0;i<=n;++i){
		for(int j=1;j<=n;j<<=1){
			if((i^j)<=n){
				addedge(i,i^j,1ll*j*c);
			}
		}
	}
	scanf("%d %d",&s,&t);
	Dijiestra(s);
	printf("%d\n",ans[t]);
	return 0;
}

堆优化的也放上:

#include<bits/stdc++.h>
#define MAXN 1000001
#define MAXM MAXN*22
using namespace std;
typedef long long ll;
struct node{
	int nxt,to;
	ll dis;
}edge[MAXM];
int cnt,head[MAXN],dis[MAXN];
inline void addedge(int x,int y,ll dis){
	edge[++cnt].to=y;
	edge[cnt].nxt=head[x];
	edge[cnt].dis=dis;
	head[x]=cnt;
}
inline void Dijiestra(int s){
	memset(dis,0x3f,sizeof(dis));
	priority_queue<pair<ll,int>,vector<pair<ll,int> >,greater<pair<ll,int> > > q;
	dis[s]=0ll;
	q.push(make_pair(0ll,s));
	while(!q.empty()){
		int front=q.top().second;
		q.pop();
		for(int i=head[front];i;i=edge[i].nxt){
			int to=edge[i].to;
			if(dis[to]>dis[front]+edge[i].dis){
				dis[to]=dis[front]+edge[i].dis;
				q.push(make_pair(dis[to],to));
			}
		}
	}
}
int main(){
	int n,m,c;
	scanf("%d %d %d",&n,&m,&c);
	while(m--){
		int x,y;
		ll dis;
		scanf("%d %d %lld",&x,&y,&dis);
		addedge(x,y,dis);
	}
	for(int i=0;i<=n;++i){
		for(int j=1;j<=n;j<<=1){
			if((i^j)<=n){
				addedge(i,i^j,1ll*j*c);
			}
		}
	}
	int s,t;
	scanf("%d %d",&s,&t);
	Dijiestra(s);
	printf("%lld",dis[t]);
	return 0;
}

树上问题

最小生成树

最小生成树的定义是将整个图删去若干边成为一棵树,在保持所有点连通的情况下保证边权和最小。

Kruskal

Kruskal 的本质是不断地加边,直到整个图连通。

考虑遍历每一条边,贪心的选择最小的边权。如果这条边可以由之前的若干边连起来,那么就可以不加。这个连通性的问题可以考虑用并查集维护。所以,设并查集的时间复杂度为 O ⁡ ( α ) \operatorname{O}(\alpha) O(α),那么则 Kruskal 的时间复杂度为 O ⁡ ( ( n − 1 ) α ) \operatorname{O}((n-1)\alpha) O((n1)α)

struct node{
	int from,to;
	ll dis;
}edge[MAXM<<1];//由于不需要遍历点的下一条边,所以可以不需要 nxt 和 head 
int cnt,fa[MAXN];
inline void addedge(int from,int to,ll dis){
	edge[++cnt].from=from;
	edge[cnt].to=to;
	edge[cnt].dis=dis;
}
inline bool cmp(node x,node y){
	return x.dis<y.dis;
}
inline void prework(){
	for(int i=1;i<MAXN;++i){
		fa[i]=i;
	}
}
int get(int x){
	if(fa[x]==x){
		return x;
	}
	return fa[x]=get(fa[x]);
}
inline void merge(int x,int y){
	fa[get(x)]=get(y);
}
inline ll Kruskal(){
	ll ans=0;
	prework();//初始化 
	sort(edge+1,edge+1+cnt,cmp);//从小到大 
	for(int i=1;i<=cnt;++i){//遍历每一条边 
		int u=edge[i].from,v=edge[i].to;
		if(get(u)!=get(v)){//两个节点需要加边 
			merge(u,v);
			ans+=edge[i].dis;
		}
	}
	return ans;
}
Prim

Prim 的本质是加点。

如果一个点可以用前面的点的边加起来,那么就可以考虑不加。并且,每一次需要放一个点最小的边权进去拓展进行遍历。具体实现可以使用 A-star 的算法实现(本人倒是觉得很像 Dijiestra),可以使用堆优化和线段树优化。时间复杂度 O ⁡ ( ( n + m ) log ⁡ m ) \operatorname{O}((n+m)\log m) O((n+m)logm)

struct node{
	int from,to,nxt;
	ll dis;
}edge[MAXM<<1];
int n,cnt,head[MAXN];
ll dis[MAXN];
bool vis[MAXN];
inline void addedge(int from,int to,ll dis){
	edge[++cnt].from=from;
	edge[cnt].to=to;
	edge[cnt].dis=dis;
	edge[cnt].nxt=head[from];
	head[from]=cnt;
}
inline ll Prim(){
	memset(dis,0x3f,sizeof(dis));
	memset(vis,0,sizeof(vis));
	priority_queue<pair<ll,int> > q;
	q.push(make_pair(0ll,1));//通常从 1 开始 
	dis[1]=0ll;
	ll ans=0;
	for(int i=1;i<n&&!q.empty();++i){
		int front=q.top().second;
		q.pop();
		if(vis[front]){
			--i;
			continue;
		}
		vis[front]=true;
		ans+=dis[front];//当前的 dis
		for(int i=head[front];i;i=edge[i].nxt){
			int to=edge[i].to;
			if(dis[to]>edge[i].dis){//从三角形不等式变成了介个样子 
				dis[to]=edge[i].dis;
				q.push(make_pair(dis[to],to));
			}
		} 
	}
	return ans;
}

通常来讲,Prim 应用于稠密图,Kruskal 应用于稀疏图。但是 Prim 在稠密图上只是理论比 Kruskal 更优,因为 Kruskal 有玄学复杂度的并查集,时快时慢,真正也没见得比 Kruskal 优。所以,通常我们使用 Kruskal。

LCA

LCA 是一个经典的例题,可以用于树论,也可以用于图论。通常有两种算法:倍增算法和 Tarjan。

倍增

倍增的方式是预处理出 f a i , j fa_{i,j} fai,j 表示 i i i 向上跳 2 j 2^j 2j 步所到达的节点。这个可以使用 dfs 预处理,时间复杂度 n log ⁡ n n\log n nlogn。然后单次询问让更深的节点通过倍增能够组合成任何数的性质跳到同一层,然后不断往上跳,跳到相同为止。时间复杂度 log ⁡ n \log n logn,总时间复杂度 ( n + m ) log ⁡ n (n+m)\log n (n+m)logn

struct node{
	int to,nxt;
}edge[MAXM<<1];
int n,cnt,head[MAXN]; 
int dep[MAXN],fa[MAXN][MAXK];
inline void addedge(int x,int y){
	edge[++cnt].to=y;
	edge[cnt].nxt=head[x];
	head[x]=cnt;
}
void dfs(int now,int pa){
	fa[now][0]=fa;
	dep[now]=dep[fa]+1;
	for(int i=1;i<MAXK;++i){
		fa[now][i]=fa[fa[now][i-1]][i-1];//更新向上跳 
	}
	for(int i=now;i;i=edge[i].nxt){
		int to=edge[i].to;
		if(to!=pa){
			dfs(to,now);//dfs 下一个节点 
		}
	}
}
inline int lca(int x,int y){
	if(dep[x]>dep[y]){
		swap(x,y);//深的在下面 
	}
	for(int i=0;i<MAXK;++i){
		if(dep[fa[y][i]]>=dep[x]){
			y=fa[y][i];
		} 
	} 
	if(x==y){
		return x;
	}
	for(int i=0;i<MAXK;++i){
		if(fa[x][i]!=fa[y][i]){
			x=fa[x][i];
			y=fa[y][i];//一起向上跳 
		}
	}
	return x;
}
Tarjan

Tarjan 是一种离线的算法,要使用并查集来维护。

首先,要接受输入边 InputEdge和查询边 QueryEdge,并区分开来。QueryEdge需要反向加入。

用 dfs 遍历。如果遍历到一条节点,使用 v i s vis vis 记录每个节点有没有访问过, f a fa fa 记录祖先节点。

当一个节点还在遍历根节点的时候,就设置父节点为自己,否则设置为不断往上的节点。

如果发现该点访问完了,而且另一个节点也访问完了,那么父节点就是答案。

struct node{
	int from,to,nxt;
}edge[MAXM<<1];
struct query_node{
	int from,to,nxt,lca;
}QueryEdge[MAXQ<<1];
int head[MAXN],QueryHead[MAXN];
int cnt,QueryCnt;
int fa[MAXN];
bool vis[MAXN];
inline void prework(){
	for(int i=1;i<MAXN;++i){
		fa[i]=i;
		vis[i]=false;
	}
}
int get(int x){
	if(fa[x]==x){
		return x;
	}
	return fa[x]=get(fa[x]);
}
void Tarjan(int u){
	fa[u]=u;//设置父节点 
	vis[u]=true;
	for(int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(!vis[v]){
			Tarjan(v);//递归 
			fa[to]=u;//设置父节点 
		}
	}
	for(int i=QueryHead[u];i;i=QueryEdge[i].nxt){
		int v=QueryEdge[i].to;//遍历查询 
		if(vis[v]){
			QueryEdge[i-1].lca=QueryEdge[i].lca=get(edge[i].to);
		}
	}
}

注意,本 Tarjan 代码复杂度其实还需要套上一个并查集的复杂度,时间复杂度 O ⁡ ( ( n + m ) α ) \operatorname{O}((n+m)\alpha) O((n+m)α),其实有 O ⁡ ( n + m ) \operatorname{O}(n+m) O(n+m) 的实现,于此(全英文警告)。

由于 Tarjan 算法实现较为难以理解,并且考场上倍增 LCA 可能能够卡过,也不会特意正解 Tarjan 卡倍增 LCA,所以通常我们会选择使用 ( n + m ) log ⁡ n (n+m)\log n (n+m)logn 的倍增 LCA 算法。

例题

有一些较小边权的边不一定会走过去,考虑用 Kruskal 换种做法,跑最大生成树。

求出这个后,题目转化成求两个点之间路径的最小边权,由于是唯一的,但是直接跑会超时,所以考虑再处理一个 m i n v minv minv 表示最小值,跑 LCA。

#include<bits/stdc++.h>
#define MAXN 10001
#define MAXM 50005
#define MAXK 21
#define INF 2147483647
using namespace std;
struct node{
	int from,to,next,dis;
}a[MAXM],edge[MAXM<<1];
int n,m,q,cnt,head[MAXN],deep[MAXN],fa[MAXN][MAXK],dis[MAXN][MAXK],fa[MAXN];
bool vis[MAXN];
inline void addedge(int from,int to,int dis){
	edge[++cnt].next=head[from];
	edge[cnt].from=from;
	edge[cnt].to=to;
	edge[cnt].dis=dis;
	head[from]=cnt;
}
inline bool cmp(const node &x,const node &y){
	return x.dis>y.dis;
}
inline int get(int x){
	if(fa[x]==x){
		return x;
	}
	return fa[x]=get(fa[x]);
}
inline void merge(int x,int y){
	fa[get(x)]=get(y);
}
inline void Kruskal(){
	sort(a+1,a+m+1,cmp);
	for(int i=1;i<=n;++i){
		fa[i]=i;
	}
	for(int i=1;i<=m;++i){
		if(get(a[i].from)!=get(a[i].to)){
			merge(a[i].from,a[i].to);
			addedge(a[i].from,a[i].to,a[i].dis);
			addedge(a[i].to,a[i].from,a[i].dis);
		}
	}
}
inline void dfs(int now){
	vis[now]=true;
	for(int i=head[now];i;i=edge[i].next){
		int to=edge[i].to;
		if(vis[to]){
			continue;
		}
		deep[to]=deep[now]+1;
		fa[to][0]=now;
		dis[to][0]=edge[i].dis;
		dfs(to);
	}
}
inline int lca(int x,int y){
	if(get(x)!=get(y)){
		return -1;
	}
	if(deep[x]>deep[y]){
		swap(x,y);
	}
	int ans=INF;
	for(int i=MAXK-1;i>=0;--i){
		if(deep[fa[y][i]]>=deep[x]){
			ans=min(ans,dis[y][i]);
			y=fa[y][i];
		}
	}
	if(x==y){
		return ans;
	}
	for(int i=MAXK-1;i>=0;--i){
		if(fa[x][i]!=fa[y][i]){
			ans=min(ans,min(dis[x][i],dis[y][i]));
			x=fa[x][i];
			y=fa[y][i];
		}
	}
	return min(ans,min(dis[x][0],dis[y][0]));
}
int main(){
	scanf("%d %d",&n,&m);
	for(int i=1;i<=m;++i){
		scanf("%d %d %d",&a[i].from,&a[i].to,&a[i].dis);
	}
	Kruskal();
	for(int i=1;i<=n;++i){
		if(!vis[i]){
			deep[i]=0;
			dfs(i);
			fa[i][0]=i;
			dis[i][0]=INF;
		}
	}
	for(int i=1;i<MAXK;++i){
		for(int j=1;j<=n;++j){
			fa[j][i]=fa[fa[j][i-1]][i-1];
			dis[j][i]=min(dis[j][i-1],dis[fa[j][i-1]][i-1]);
		}
	}
	scanf("%d",&q);
	while(q--){
		int x,y;
		scanf("%d %d",&x,&y);
		printf("%d\n",lca(x,y));
	}
	return 0;
}

其他

拓扑排序

拓扑排序是基于拓扑序进行的排序。比如 u → v u\to v uv 有一条边,那么则称 u u u 的拓扑序大于 v v v

拓扑排序的思想是找出入度为 0 0 0 的点,然后将这个点删除,再去找入度为 0 0 0 的点,直到没有入度为 0 0 0 的点(有环)或者图空了(没环)。

struct node{
	int to,nxt;
}edge[MAXM<<1];
int cnt,head[MAXN],indeg[MAXN];
bool vis[MAXN];
inline void addedge(int from,int to,int dis){
	edge[cnt].to=to;
	edge[++cnt].nxt=head[from];
	head[from]=cnt;
}
inline bool Topusort(){
	queue<int> q;
	for(int i=1;i<MAXN;++i){
		if(!indeg[i]){
			q.push(i);//加入入度为 0 的点 
		}
	}
	while(!q.empty()){
		int front=q.front();
		q.pop();
		for(int i=head[front];i;i=edge[i].nxt){//遍历相关的点 
			int to=edge[i].to;
			--indeg[to];
			if(!indeg[to]){
				q.push(to);//加入入度为 0 的点 
			}
		}
	}
	for(int i=1;i<MAXN;++i){
		if(indeg[i]){
			return true;//如果还有,那就有环 
		}
	}
	return false;//没有环 
}
例题

发现停靠了的节点的等级一定大于其他没有停靠的节点,可以把这个等级看作是拓扑序,较优先的节点向靠后的节点连边,然后跑 Topu 即可。

#include<iostream>
#include<vector>
#include<queue>
#define MAXN 1001
using namespace std;
int n,m,a[MAXN],indeg[MAXN];
vector<int> G[MAXN];
bool uni[MAXN],vis[MAXN][MAXN];
inline void addedge(int from,int to){
	G[from].push_back(to);
	++indeg[to];
	vis[from][to]=true;
}
inline int Topusort(){
	queue<pair<int,int> > q;
	int ans=1;
	for(int i=1;i<=n;++i){
		if(!indeg[i]){
			q.push(make_pair(i,1));
		}
	}
	while(!q.empty()){
		int from=q.front().first;
		int step=q.front().second;
		q.pop();
		for(int i=0;i<G[from].size();++i){
			int to=G[from][i];
			--indeg[to];
//			ans=max(ans,step+1);
			if(!indeg[to]){
				q.push(make_pair(to,step+1));
				ans=max(ans,step+1);
			}
//			cout<<to<<" "<<step+1<<endl;
		}
	}
	return ans;
}
int main(){
	scanf("%d %d",&n,&m);
	while(m--){
		int k;
		scanf("%d",&k);
		for(int i=1;i<=k;++i){
			scanf("%d",&a[i]);
			uni[a[i]]=true;
		}
		for(int i=a[1];i<=a[k];++i){
			if(!uni[i]){
				for(int j=1;j<=k;++j){
					if(!vis[a[j]][i]){
						addedge(a[j],i);
					}
				}
			}
		}
		for(int i=1;i<=k;++i){
			uni[a[i]]=false;
		}
	}
	printf("%d",Topusort());
	return 0;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值