学习笔记 2.5图的理论

2.5.1 图是什么

图是由顶点(vertex,node)和边(edge)组成。顶点代表对象。在示意图中,我们使用点或圆来表示。边表示的是两个对象的连接关系。
无向图术语
两个顶点之间如果有边连接,那么就视为两个顶点相邻。相邻顶点的序列称为路径。起点和终点重合的路径叫做圈。任何两点之间都有路径链接的图叫做连通图。顶点链接的边数叫做这个顶点的度。
没有圈的连通图叫做树(tree),没有圈的非连通图叫做森林。一棵树的边数恰好是顶点数-1,反之,边数等于顶点-1的连通图就是一棵树。
有向图术语
以有向图顶点v为起点的边的集合叫做v的初度,为终点的边叫做v的入度。没有圈的有向图叫做DAG。

2.5.3 图的搜索

例题 二分图判定

挑战程序设计竞赛 page 97

把相邻顶点染成不同颜色的问题叫做图着色问题。对图进行染色所需要的最小颜色书称为最小着色数。最小着色数是2的图称作二分图。
如果只用2种颜色,那么确定一个顶点的颜色之后,和它相邻的顶点的颜色也就确定了。因此我们可以选择任意一个顶点除法,依次确定相邻顶点的颜色,就可以判断是否可以被2种颜色染色了,这个问题如果用深度优先搜索的话,就能够简单的实现。

vector<int> G[maxn];//用链表的方式保存图
int V;
int color[maxn];//顶点的颜色(1,或者-1)
bool dfs(int v,int c){//把顶点v的颜色染成c
	color[v]=c;
	for(int i=0;i<G[v].size();i++){
		if(color[G[v][i]]==c)retrun false;
		if(color[G[v][i]]==0&&!dfs(G[v][i],-c))return false;//判断是否没有被染过色并且染色失败 则返回false
	}
	return true;
}
void solve(){
	for(int i=0;i<V;i++){//有非联通图的情况
		if(color[i]==0){
			if(!dfs(i,1)){
				cout << "No" << endl;
				return;
			}
		}
	}
	cout << "Yes" << endl;
	return;
}

如果是连通图,那么一次dfs就能访问到所有的顶点。如果题目描述中没有说明,那么有可能图是不连通的,这样就需要依次检查每个顶点是否访问过。由于每个顶点和边只访问过一次,因此复杂度为O(|V|+|E|)

2.5.4 最短路问题

最短路是给定两个顶点,在以这两个点为起点和终点的路径中,边的权值和最小的路径。
1.单源最短路问题1(Bellman-Ford算法)
单源最短路问题是固定一个起点,求它到其他所有点的最短路问题。
记从起点s出发到顶点i的最短距离为d[i]。则下述等式成立。
d[i]=min{d[j]+(从j到i的边的权值)|e=(j,i)∈E}
如果给定的图是一个DAG,就可以按拓扑序顶点编号,并利用这条递推关系式计算出d。但是,如果图中有圈,就无法依赖这样的顺序进行计算。在这种情况下,记当前到顶点i的最短路径为d[i],并设初值d[s]=0,d[i]=INF(足够大的常数),再不断使用这条递推关系式更新d的值,就可以算出新的d。只要图中不存在负圈,这样的更新操作就是有限的。结束之后的d就是所求的最短距离了。

struct edge{int from,to,cos;};
edge es[maxe];//边
int d[maxv];//最短距离
int V,E;//V是顶点数,E是边数
void shortest_path(int s){
	for(int i=0;i<V;i++)d[i]=INF;
	d[s]=0;
	while(true){
		bool update=false;
		for(int i=0;i<E;i++){
		edge e=es[i];
		if(d[e.from]!=INF&&d[e.to]>d[e.from]+e.cost){
				d[e.to]=d[e.from]+e.cost;
				update=true;
			}
		}
		if(!update)break;
	}
}

这个算法叫做Bellman-Ford。时间复杂度为O(|V|+|E|)。如果没有负圈那么外层循环最多执行V-1次,如果有负圈那么在第V次也会更新d的值,所有可以用于判断是否有负圈。如果一开始把d[i]初始化为0,那么就能检查出所有的负圈。

//true 代表有负环
bool find_negative_loop(){
	memset(d,0,sizeof(d));
	for(int i=0;i<V;i++){
		for(int j=0;j<E;j++){
			edge e=es[j];
			if(d[e.to]>d[e.from]+e.cost){
				d[e.to]=d[e.from]+e.cost;
				if(i==V-1)return true;
			}
		}
	}
	return false;
}

单源最短路径问题2(Dijkstra算法)
对Bellman-Ford算法进行修改(必须是非负图的情况下才能修改)
1.找到最短距离已经确定的顶点,从它出发更新相邻顶点的最短距离。
2.此后不需要再关心1中的“最短距离已经确定的顶点”。

int cost[maxv][maxv];//用邻接矩阵法保存图
int d[maxv];//最小值
bool used[maxv];//已经使用过的图
int V;
void dijkstra(int s){
	for(int i=0;i<V;i++)d[i]=INF;
	d[s]=0;
	for(int i=1;i<=V-1;i++){
		int k,min_s=INF;
		for(int u=0;u<V;u++){
			if(!used[u]&&min_s>d[u]){
				min_s=d[u];
				k=u;
			}
			if(min_s==INF)return;//图不连通
			used[k]=true;
			for(int u=0;u<V;u++){
				if(!used[u]&&d[u]>d[k]+cost[k][u]){
					d[u]=d[k]+cost[k][u];
				}
			}
		}
	}
}

使用邻接矩阵实现的Dijkstra算法复杂度为O(V2)。使用邻接表的话,更新最短距离只需要访问每条边一次即可,因此这部分的复杂度是O(|E|)。但是每次要枚举所有的顶点来查找下一个使用的顶点,因此最终的时间复杂度还是O(|V2|)。在E比较小的时候,大部分时间花在了查找下一个使用的顶点上,因此需要使用合适的数据结构对其进行优化。
需要优化的是数值的插入(更新)和取出最小值两个操作,因此使用堆就可以了。把每个顶点当前最短距离用堆来维护,在更新最短距离时,把对应的元素往根的方向移动以满足堆的性质。而每次从堆中取出的最小值就是下一次要使用的顶点。这样堆中元素共有O(|V|)个,更新和取出数值的操作有O(|E|)次,因此整个算法的时间杂度时O(|E|log|V|)。
下面使用STL的priority_queue的实现。在每次更新时往堆里插入当前最短距离和顶点的键值对。插入的次数时O(|E|)次,因此y元素的个数是O(|E|)个。当取出的最小值不是最短距离的话就丢弃这个值。

struce edge{int to,cost;};
typedef pair<int,int> p;//first是最短距离,second是顶点的编号
int V;
vector<edge> G[maxv];
int d[maxv];
void dijkstra(int s){
	//通过指定greater<P>参数,堆按照first从小到大的属性呢取出值
	priority_queue<p,vector<p>,greater<p>>que;
	fill(d,d+V,INF);
	d[s]=0;
	que.push(P(0,s));
	while(!que.empty()){
		P p=que.top();
		que.pop();
		int v=p.second;
		if(d[v]<p.first)continue;
		for(int i=0;i<G[v].size();i++){
			edge e=G[v][i];
			if(d[e.to]>d[v]+e.cost){
				d[e.to]=d[v]+e.cost;
				que.push(P(d[e.to],e.to));
			}
		}
	}
}

2.5.5 最小生成树

给定一个无向图,如果它的某个子图任意两个顶点都连通并且是一棵树,那么这棵树就叫做生成树(Spanning Tree)。如果边上有全职,那么使得边权和最小的生成树叫做最小生成树。
1.最小生成树问题1(prim算法)

int cost[maxv][maxv];
int V;
int mincost[maxv];
bool used[maxv];
int prim(){
	fill(mincost,mincost+V,INF);
	mincost[0]=0;
	int res=0;
	for(int i=0;i<V;i++){
		int min_s=INF;
		int k;
		for(int u=0;u<V;u++){
			if(!used[u]&&min_s>mincost[u]){
				k=u;
				min_s=d[u];
			}
		}
		if(min_s==INF)return -1;//buliantong
		used[k]=1;
		for(int u=0;u<V;u++){
			mincost[u]=min(mincost[u],mincost[k]);
		}
		res+=mincost[k];
	}
	return res;
}

2.最小生成树问题2(Kruskal)
Kruskal算法按照边的权值的顺序从小到大查看一遍,如果不产生圈(重编等也算在内),就把这条边加入到生成树中。至于这个算法为什么是正确的,其实是和Prim算法证明的思路基本相同。
接下来我们介绍如何判断是否产生圈。假设现在要把链接顶点u和顶点v的边加入生成树中。如果加入之前u和v不再同一个连通分量里那么加入e也不会产生圈。反之,如果u和v在同一个连通分量里,那么一定会产生圈。可以使用并查集高效地判断是否属于同一个连通分量。
Kruskal算法在排序上最费时,算法的复杂度是O(|E|log|V|)。

struct node{int u,v,cost};
bool cmp(const edge&e1,const edge& e2){
	return e1.cost<e2.cost;
}
edge es[maxe];
int V,E;
int Kruskal(){
	int res=0;
	sort(es,es+E,comp);
	init_union_find(V);//并查集的初始化。
	for(int i=0;i<E;i++){
		edge e=es[i];
		if(!same(e.u,e.v)){
			unite(e.u,e.v);
			res+=e.cost;
		}
	}
	return res;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值