NOIP 2016[图论复习]

19 篇文章 0 订阅

--------by NK SuperGate

主要复习的几个算法:tajan ,kruscal,spfa和dijkstra,floyd,以及比较经典的图论模型和思考方式

最短路

最短路在许多图论题中都会出现,主要作为预处理的一部分或者主要算法的核心,有的时候最短路的时间效率如何直接或者间接地影响了整个代码的用时,因此对题目进行分析找到最适合的最短路算法是非常重要的

一般来说,图中边数较少的时候(稀疏图)用spfa比较适合,时间复杂度为O(km)其中K为每个点进队次数(不超过n,一般为2)

m为边的总数

对于边数较多的图(稠密图),spfa会被卡到O(n^2),这个时候,我们就应该运用dijkstra+堆优化,dijkstra+堆的复杂度为O(nlogn),边的数量对其复杂度几乎毫无影响,因此dijkstra+堆相比spfa稳定性是最大的优点

要注意的是,以上两种方法仅仅适用于求单源最短路,当然如果要求图中每一个点到给定点的最短路存个反图跑最短路就可以了,如果要求多源最短路,我们就要用到floyd算法,时间复杂度为O(n^3)

由于floyd比较简单,就不放出代码了,这里只写出了spfa和dijkstra+堆优化的模板代码,存边方式是用的结构体+vector

int n,m,last[maxn],dis[maxn],Start,End;
bool vis[maxn];
struct node{
	int a,b,c,Next;
	node(int a,int b,int c,int Next):a(a),b(b),c(c),Next(Next){}
};
vector<node>s;
void insert(int a,int b,int c){
	s.push_back(node(a,b,c,last[a]));
	last[a]=s.size()-1;
}
struct Point{
	int dist,x;
	Point(int dist,int x):dist(dist),x(x){}
	bool operator<(const Point& h)const{
		return dist<h.dist;
	}
};
void dijkstra(){dijkstra+堆
	memset(dis,inf,sizeof(dis));
	priority_queue<Point>q;
	dis[Start]=0;
	q.push(Point(0,Start));
	int i,x,v;
	while(q.size()){
		Point temp=q.top();q.pop();
		if(temp.dist!=dis[temp.x])continue;
		for(i=last[temp.x];i>=0;i=s[i].Next){
			v=s[i].b;
			if(dis[v]>dis[temp.x]+s[i].c){
				dis[v]=dis[temp.x]+s[i].c;
				q.push(Point(dis[v],v));
			}
		}
	}
}
void spfa(){///spfa
	queue<int>q;
	memset(dis,inf,sizeof(dis)); 
	int i,x,v;
	dis[Start]=0,vis[Start]=1;
	q.push(Start);
	while(q.size()){
		x=q.front();q.pop();
		vis[x]=0;
		for(i=last[x];i>=0;i=s[i].Next){
			v=s[i].b;
			if(dis[v]>dis[x]+s[i].c){
				dis[v]=dis[x]+s[i].c;
				if(!vis[v]){
					vis[v]=1;
					q.push(v);
				}
			}
		}
	}
}

最小生成树

最短路也经常作为预处理的一部分出现在图论题中,其延伸算法最小生成森林(其实本质上基本一样)也十分常见

kruscal应该是所有最小生成树中最容易理解且应用范围最广的一种方法,具体操作是:将所有的边从小到大排序,然后枚举每一条边,判断这条边的两个点是否已经在同一个集合中,如果不在就合并,由于选的边肯定是较小的边,合并出来的树的边数和一定是最小的

这里的判断是否在一个集合和合并操作用到了一个非常重要的数据结构-----并查集,并查集及其延伸用法在后面再详细阐述

最后注意一下,kruscal时间复杂度为O(mlogm)

int n,m,fa[maxn],ans;
int getfa(int x){return fa[x]==x?x:fa[x]=getfa(fa[x]);}
struct node{
	int a,b,c;
	bool operator<(const node& h)const{
		return c<h.c;
	}
}edge[maxn<<2];
void kruscal(){
	int i,x,y,fx,fy;
	for(i=1;i<=m;i++){
		x=edge[i].a,y=edge[i].b;
		fx=getfa(x),fy=getfa(y);
		if(fx!=fy){
			ans+=edge[i].c;
			fa[fx]=fy;
		}
	}
}
int main(){
	_read(n);_read(m);
	int i,j,x,y,z,last=0;
	for(i=1;i<=n;i++)fa[i]=i;
	for(i=1;i<=m;i++){
		_read(edge[i].a);
		_read(edge[i].b);
		_read(edge[i].c);
	}
	sort(edge+1,edge+1+m);
	kruscal();
}

图的连通性

对于图的连通性一般会有两种类型:有向图的强连通分量无向图的双连通分量,这两种题目都可以用tajan算法及其变形求解,这里着重分析有向图的强连通分量的tajan算法

tajan算法的优势在于,它可以在求出每一个强连通分量同时求出每一个点所属的编号和强连通分量的数量,时间复杂度控制在O(n)范围内

tajan的精妙之处就在于dfn数组和low数组的运用,虽然比较难懂(现在我还只会用不知道为什么,菜的抠脚)

实际上tajan还有许多变形的算法,比如求无向图的割点割边,双连通分量等,这些留到之后有时间再写

void tajan(int u){
	dfn[u]=low[u]=++vistime;
	S.push(u);ins[u]=1;
	int i,v;
	for(i=last[u];i>=0;i=s[i].Next){
		v=s[i].b;
		if(dfn[v]==0){
			tajan(v);
			low[u]=min(low[u],low[v]);
		}
		else if(dfn[v]!=0&&ins[v])
			low[u]=min(low[u],dfn[v]);
	}
	if(dfn[u]==low[u]){
		scc++;
		do{
			v=S.top();S.pop();
			id[v]=scc;
			ins[v]=0;
		}while(u!=v);
	}
}

拓扑排序

对于一个图,这个图中任意一个边<vx,vy>(有向边)都满足vx在vy前的由点构成的序列叫做拓扑序列,将图中的点排成拓扑序列的过程叫做拓扑排序

拓扑排序的方法其实很简单,看了代码就很容易理解,但是其用法都在许多经典题目中得以体现,有的时候与其用其他的算法,拓扑排序能更好的解决特定问题

for(i=1;i<=n;i++)
	if(!d[i])s.push(i);
while(s.size()){
	int t=s.top();s.pop();
	cout<<t<<endl;输出拓扑序列
	for(i=last[t];i>=0;i=s[i].Next){
		int v=s[i].b;
		if(d[v]>0){
			d[v]--;
			if(d[v]==0)s.push(v);
		} 
	}
}


树上倍增(LCA)

LCA是树中比较常用的求最近公共祖先的算法,其复杂度为:预处理:O(nlogn),单次查询:O(logn)

LCA其实并没有太多难的地方,常用的地方就是求两点的距离以及维护子树的最大权值(这里要注意maxv和fa数组的更新前后顺序!),其他的枚举类的题有的时候也会涉及到倍增的思想,可见倍增并不局限于图论中

之前做过升级版的LCA仙人掌题,还好省赛不考,不然难的哭

void dfs(int u,int f){
	int v,i,k;
	dep[u]=dep[fa[u][0]]+1;
	k=ceil(log(dep[u])/log(2));
	for(i=1;i<=k;i++)
		fa[u][i]=fa[fa[u][i-1]][i-1];
	for(i=last[u];i>=0;i++){
		v=s[i].b;
		if(v==fa)continue;
		fa[v][0]=u;
		dfs(v,u);
	}
}
int LCA(int x,int y){
	int k,S,i;
	if(dep[x]<dep[y])swap(x,y);
	S=ceil(log(n)/log(2));
	k=dep[x]-dep[y];
	for(i=0;i<=S;i++)
		if((k>>i)&1)x=fa[x][i];
	if(x==y)return x;
	S=ceil(log(dep[x])/log(2));
	for(i=S;i>=0;i++)
		if(fa[x][i]!=fa[y][i])x=fa[x][i],y=fa[y][i];
	return fa[x][0];
}

总结

图论是本人比较喜欢的一类题目,因为图论题的思路一般很清晰,主要考验代码能力和细节处理能力,同时也对思维有一定锻炼效果。

要注意的是,图论不一定是我们一眼就能看出来的,有可能在尝试多种其算法都不行的情况下才会发现这是一道图论题,有的时候对一个序列进行交换,转移等操作时,就可以将特定位置连边然后求解

图论是一种思考方式,当多个事件有某种特定的联系(例如转化关系),图论也许是最简单的一种解题选择方案

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值