【PAT】第十章 图算法专题

本文详细介绍了图算法中的核心概念,包括图的定义、存储方式(邻接矩阵和邻接表)、深度优先搜索和广度优先搜索。深入探讨了Dijkstra算法、其变种及Bellman-Ford算法、SPFA算法,还有Prim算法用于最小生成树。此外,还涉及拓扑排序和关键路径的识别。适合学习图论和算法设计的学生和开发者。
摘要由CSDN通过智能技术生成

第十章 图算法专题

10.1 图的定义和相关术语

有向图-无向图
出度-入度


10.2 图的存储

10.2.1邻接矩阵

g[][]

【注】邻接矩阵只适用于顶点数目不超过1000的题目。

10.2.2 邻接表

vector<int> adj[n]
或者

struct edge{
	int v;
	int w;
};
vector<edge> adj[n];

10.3 图的遍历

10.3.1 深度优先搜索DFS

DFS以深度为第一关键词,每次沿着路径访问没有被访问过的结点直到不能再前进才退回到最近的分岔口。

【注】DFS不一定会遍历整个图的所有边!

邻接矩阵版本

const int maxn=1000;
const int inf=1e9+10;

int n,g[maxn][maxn];
bool vis[maxn]={false};

voud dfs(int u,int depth) 	//访问深度 
{
	vis[u]=true;
	//对u的操作
	for(int v=0;v<n;v++)
	{	//v未被访问过且是u的邻接点 
		if(vis[v]==false&&g[u][v]!=inf)
			dfs(v,depth+1);
	} 
}
//遍历整个图g 
void dfsTrave()
{
	for(int u=0;u<n;u++)
	{
		if(vis[u]==false)
			dfs(u,1); 	//访问u所在的连通块
	}
 } 

邻接表版本

const int maxn=1000;
const int inf=1e9+10;

int n;
vector<int> adj[maxn];
bool vis[maxn]={false};

voud dfs(int u,int depth) 	//访问深度 
{
	vis[u]=true;
	//对u的操作
	for(int i=0;i<adj[u].size();i++)
	{	//adj[u][i]未被访问
		if(vis[adj[u][i]]==false)
			dfs(adj[u][i],depth+1);
	} 
}
//遍历整个图g 
void dfsTrave()
{
	for(int u=0;u<n;u++)
	{
		if(vis[u]==false)
			dfs(u,1);
	}
 } 

10.3.2 广度优先搜索BFS

BFS以广度为第一关键词,每次以扩散的方式向外访问顶点。

建立一个队列,把初始顶点加入队列,此后每次取出队首顶点进行访问,并把从该顶点出发可以到达的 未曾加入过队列(而非未曾访问过的) 的顶点全部加入队列,直到队列为空。

邻接矩阵版

int n,g[maxn][maxn];
bool inq[maxn]={flase};

void bfs(int u)
{
	queue<int> q;
	q.push(u);
	inq[u]=true;
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		for(int v=0;v<n;v++)
		{
			if(inq[v]==false&&g[u][v]!=inf)
			{
				q.push(v);
				inq[v]=true;
			}
		}
	}
}
void bfsTrave()
{
	for(int u=0;u<n;u++)
	{
		if(inq[u]==false)
			bfs(u);
	}
}

邻接表版

int n;
vector<int> adj[maxn];
bool inq[maxn]={flase};

void bfs(int u)
{
	queue<int> q;
	q.push(u);
	inq[u]=true;
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		for(int i=0;i<adj[u].size();i++)
		{
			int v=adj[u][i];
			if(inq[v]==false&&g[u][v]!=inf)
			{
				q.push(v);
				inq[v]=true;
			}
		}
	}
}
void bfsTrave()
{
	for(int u=0;u<n;u++)
	{
		if(inq[u]==false)
			bfs(u);
	}
}

如果要知道连接块中每个顶点的层号,声明一个带变量layer的结构体。


10.4 最短路径

对于给定的图G、起点S和终点T,求出ST的最短路径

10.4.1 Dijkstra算法

单源最短路问题:给定图G和起点s,得到s到其他每个顶点的最短距离。非负权

注意顶点是0 ~ n-1还是1 ~ n。

思路

集合S存放已被访问过的顶点,执行n以下步骤:

  • 从集合V-S中选择与起点s距离最短的顶点u,访问u并加入集合S中。
  • u中介点,优化起点s到所有u能到达的顶点v未被访问过的)之间的最短距离。

1. 具体实现
  • 常量:const int maxn=最大顶点数量; const int INF=1e9;
  • Gint G[maxn][maxn]初始化为全INF,在输入边权后得到完整的图。
  • 集合Sbool vis[maxn]初始化为false,表示未被访问过。
  • 起点s到其他顶点的最短距离:int d[maxn]初始化时,除d[s]=0外,其余初始化为INF
1.1 邻接矩阵实现

顶点数较少(<=500)时,可以直接使用邻接矩阵。

#include <cstdio>
#include <string.h> 	//memset头文件
#include <algorithm> 	//fill头文件 
using namespace std; 	//使用algorithm头文件的前提 

const int maxn=1000; 	//最大顶点数
const int INF=1e9; 		//表示两顶点不可达

int n,g[maxn][maxn];
int d[maxn];
bool vis[maxn]={false}; 	//初值为false,表示全都未被访问过

void Dijkstra(int s)
{
	//ini
	fill(d,d+maxn,INF); 	//非-1或0不要用memset
	d[s]=0;
	//update n times
	for(int i=0;i<n;i++) 	//循环n次 
	{
		//获得集合V-S中距离集合S最近的点
		int u=-1,MIN=INF; 	//MIN表示起点到V-S中顶点u的最小距离 
		for(int j=0;j<n;j++)
		{
			if(vis[j]==false&&d[j]<MIN)
			{
				u=j;
				MIN=d[j];
			}
		}
		
		//如果没有这样的点,说明此时集合V-S里的点与集合S不可达!
		if(u==-1)
			return ;
			
		//根据中介点u更新最短路
		vis[u]=true; 	//访问顶点u
		for(int v=0;v<n;v++)
		{	//v未被访问过,u能到达v,d[v]能被优化 
			if(vis[v]==false&&g[u][v]!=INF&&d[u]+g[u][v]<d[v])
			{
				d[v]=d[u]+g[u][v];
			}
		} 
	}
} 

复杂度:O(V2)。n次循环,每次循环寻找最近的点以及更新邻接点。

1.2 邻接链表实现
#include <cstdio>
#include <string.h> 	//memset头文件
#include <algorithm> 	//fill头文件 
#include <vector>
using namespace std; 	//使用algorithm头文件的前提 , stl

const int maxn=1000; 	//最大顶点数
const int INF=1e9; 		//表示两顶点不可达
struct node{
	int v,dis; 	//目标顶点,边权 
};
vector<node> adj[maxn]; 	//图,adj[u]存放顶点u的所有邻接点
int n; 	//顶点数
int d[maxn]; 	//最短路长
bool vis[maxn]={false};

void Dijsktra(int s)
{
	fill(d,d+maxn,INF);
	d[s]=0;
	
	for(int i=0;i<n;i++)
	{
		int u=-1,min=INF;
		for(int j=0;j<n;j++)
		{
			if(vis[j]==false&&d[j]<MIN)
			{
				u=j;
				MIN=d[j];
			}
		}
		//当前集合V-S与集合S不可达!
		if(u==-1)
			return ;
		
		vis[u]=true;
		for(int j=0;j<adj[u].size();j++)
		{
			int v=adj[u][j].v;
			if(vis[v]==false&&d[v]>d[u]+adj[u][j].dis)
			{
				d[v]=d[u]+adj[u][j].dis;
			}
		}
	}
} 

复杂度:O(V2+E) 。n次循环,每次循环寻找最近的点以及更新邻接点。
PS.寻找最小d[u]的过程可以通过堆优化,从而使复杂度降为O(VlogV+E)。


2. 变种
2.1 无向边

将每条无向边看做两条方向相反的有向边。

2.2 打印最短路径

前驱结点数组pre[]pre[v]表示从起点s到顶点v的最短路径上的v的前一个顶点。初始化时,pre[i]=i

for(int v=0;i<n;v++)
{	//v未被访问过,u能到达v,d[v]能被优化 
	if(vis[v]==false&&G[u][v]!=INF&&d[u]+G[u][v]<d[v])
	{
		d[v]=d[u]+G[u][v];
		pre[v]=u; 	//记录前驱结点
	}
} 

//输出最短路径
void printPath(int s,int v)
{
	if(v==s) 	//当前结点是起点时,直接输出,并返回。
	{
		printf("%d",s);
		return ;
	}
	printPath(s,pre[v]);
	printf("%d",v);	
}
2.3 最短路径不止一条

第二标尺。增加一个数组来存放新增的边权或点权或最短路径条数

2.3.1 每条边再加一个权
增加一个数组cost[maxn][maxn](边权)和一个数组c[maxn]。初始化c[s]=0,其余为INF-1(如果求的是最小边权和,则初始化为INF,反之初始化为0)。

if(vis[v]==false&&g[u][v]!=INF)
{
	if(d[v]>d[u]+g[u][v])
	{
		d[v]=d[u]+g[u][v];
		c[v]=c[u]+cost[u][v];
	}
	else if(d[v]==d[u]+g[u][v]&&c[v]>c[u]+cost[u][v]) 	//最短路径长度相同
	{
		c[v]=c[u]+cost[u][v]; 	//看第二标尺是否更小(或更大) 
	} 		
}

2.3.1 每个点增加一个点权
增加一个数组weight[maxn](点权)和一个数组w[maxn]。初始化w[s]=weight[s],其余为0INF(如果求的是最小点权和,则初始化为INF,反之初始化为0)。

if(vis[v]==false&&g[u][v]!=INF)
{
   if(d[v]<d[u]+g[u][v])
   {
   	d[v]=d[u]+g[u][v];
   	w[v]=w[u]+weight[v];
   }
   else if(d[v]==d[u]+g[u][v]&&w[v]<w[u]+weight[v]) 	//最短路径相同
   {
   	w[v]=w[u]+weight[v]; 	//看第二标尺是否更优 
   } 		
}

2.3.3 问有多少条最短路径
增加一个数组num[maxn](最短路径条数)。初始化num[s]=0(假设一定存在最短路径),其余为0

if(vis[v]==false&&g[u][v]!=INF)
{
   if(d[v]<d[u]+g[u][v])
   {
   	d[v]=d[u]+g[u][v];
   	num[v]=num[u]; 	//继承
   }
   else if(d[v]==d[u]+g[u][v]) 	//最短路径相同
   {
   	num[v]+=num[u]; 	//累积
   } 		
}
3. Dijsktra+DFS

【注】若只有一个第二标尺,则直接使用Dijsktra算法。否则,使用DIJ+DFS。

先在Dijsktra算法中记录下所有的最短路径(只考虑距离),再从所有的最短路径中选出第二标尺最优的路径。

具体思路
  • 使用Dijsktra算法记录所有的最短路径。
    pre数组设置为 vector<int> pre[MAXN] 。对于每个结点,pre[v]就是一个变长数组vector,且最多存在maxn个前驱结点。pre[v]中存放结点v所有能产生最短路径的前驱结点pre[maxn]数组不需要赋初值。
if(d[v]>d[u]+g[u][v]) 	//有更短的最短路径,清空pre[v],再push_back
{
	d[v]=d[u]+g[u][v];
	pre[v].clear(); 	//清空 
	pre[v].push_back(u); 	//压入 
}
else if(d[v]==d[u]+g[u][v]) 	//不止一条最短路径,加入到pre[v]中
{
	pre[v].push_back(u); 	//压入 
}
  • 遍历所有最短路径。找出第二标尺最优的最短路径。
    所有的前驱结点形成一棵递归树。对这棵树进行深度遍历每当到达叶子结点(即起点),就得到了一条最短路径。对于每条最短路径计算第二标尺,得到最优第二标尺的那条最短路径。
  • 具体实现
    第二标尺最优值optValue,全局变量。
    记录最优路径的数组pathvector
    临时记录DFS遍历到叶子结点时的路径tempPathvector
    递归边界:叶子结点,即起点。
    计算最短路径的数量:全局变量,递归边界处++。

ATTENTION

  • tempPath中的路径是逆序的。
  • 对于每次递归,先将当前结点pushtempPath中,再在退出前pop当前结点,实现回溯。统一将递归边界单独pushpop
int optValue=0,cnt=0;
vector<int> pre[MAXN];
vector<int> path,tempPath;

void dfs(int v) 	//v为当前访问结点 
{
	if(v==st) 	//st为起点
	{
		cnt++; 	//计算最短路径数量
		tempPath.push_back(v); 	//tempPath中保存了当前正在递归的最短路径
		//计算该条最短路径的第二标尺,注意tempPath是倒序的!!! 
		int value;
		if(value 优于 optValue)
		{
			optValue=value;
			path=tempPath;
		} 
		tempPath.pop_back(); 	//删除刚加入的结点
		return ; 	//递归一定要return啊!
	} 
	tempPath.push_back(v);
	for(int i=0;i<pre[v].size();i++)
	{
		dfs(pre[v][i]);
	}
	tempPath.pop_back(); 	//回溯一个结点
}

10.4.2 Bellman-Ford算法和SPFA算法

1. Bellman-Ford算法

可处理带负权边单源最短路问题

根据环的边权之和,可以将环分为零环、正环和负环。零环和正环不会影响最短路,但如果是从源点可以到达的负环,就会影响最短路径的求解。

Bellman-Ford算法

  • 设置一个数组d,存放源点到各个顶点的最短距离。
  • 如果存在从源点可达的负环,算法返回false;否则返回true
  • 对图中的边进行V-1轮操作,每轮都遍历图中所有的边。对于每条边 (u,v) ,如果以u作为中介点可以使d[v]更小,就更新d[v]
  • 算法复杂度为O(VE)
  • V-1轮操作后,如果没有源点可达的负环,那么此时数组d中的所有值都应当已经达到最优。所以再对所有边进行一轮操作,如果有还能被优化的值,说明图中存在源点可达的负环,返回false

【注】由于Bellman-Ford算法要遍历所有的边,所以使用邻接表更好,邻接矩阵会使复杂度增加到O(n3)。

struct Node{
	int v,dis;
};
vector<Node> adj[maxn];
int n;
int d[maxn];

bool bellman(int s)
{
	fill(d,d+maxn,inf);
	d[s]=0;
	for(int i=0;i<n-1;i++) 	//v-1轮操作
	{
		for(int u=0;u<n;u++)
		{
			for(int j=0;j<adj[u].size();j++)
			{
				int v=adj[u][j].v;
				int dis=adj[u][j].dis;
				if(d[u]+dis<d[v])
				{
					d[v]=d[u]+dis;
				}
			}
		}
	} 
	//判断负环
	for(int u=0;u<n;u++)
	{
		for(int j=0;j<adj[u].size();j++)
		{
			int v=adj[u][j].v;
			int dis=adj[u][j].dis;
			if(d[u]+dis<d[v])
				return false;
		}
	} 
	return true;
}

【注】如果在某一轮操作中,没有任何边被松弛,说明已达到最优,可以直接退出。

统计最短路径条数
由于Bellman-Ford会多次访问曾经访问过的顶点,所以需要设置记录前驱的数组set<int> pre[maxn],当遇到一条和已有最短路径长度相同的路径时,必须重新计算最短路径条数。

set<int> pre[maxn];

if(d[u]+dis<d[v])
{
	d[v]=d[u]+vis;
	num[v]=num[u]; 	//继承
	pre[v].clear();
	pre[v].insert(u);
}
else if(d[u]+dis==d[v])
{
	pre[v].insert(u);
	num[v]=0;
	set<int>::iterator it;
	for(it=pre.begin();it!=pre.end();it++)
	{
		num[v]+=num[*it];
	}
}
2. SPFA算法

只有当某个顶点ud[u]值改变时,从他出发的边的邻接点vd[v]值才可能被改变。所以

  • 建立一个队列
  • 每次将队首顶点u取出,对u的所有邻接边进行松弛操作
  • 如果可能松弛并且顶点v不在队列中,将v加入到队列
  • 直到队列为空,或者某个顶点的入队次数超过V-1(有可达的负环)

这就是SPFA算法,期望时间复杂度为O(kE),甚至由于Dij。但如果有源点可达的负环,复杂度会退化到O(VE)

#include <cstdio>
#include <vector>
using namespace std;

vector<Node> adj[maxn];
int n,d[maxn],num[maxn]; 	//若已知没有负环,可以不要num[]数组
bool inq[maxn];

bool SPFA(int s)
{
	//ini
	memset(inq,false,sizeof(inq));
	memset(num,0,sizeof(num));
	fill(d,d+maxn,inf);
	//源点入队
	queue<int> q;
	q.push(s);
	inq[s]=true;
	num[s]++; 	//判断有没有负环
	d[s]=0;
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		inq[u]=false; 	//出队
		//遍历u的所有邻接边
		for(int j=0;j<adj[u].size();j++)
		{
			int v=adj[u][j].v;
			int dis=adj[u][j].dis;
			//松弛
			if(d[u]+dis<d[v])
			{
				d[v]=d[u]+dis;
				if(!inq[v])
				{
					q.push(v);
					inq[v]=true;
					num[v]++;
					if(num[v]>=n)	return false;
				}
			} 
		} 
	} 
	return true;
} 

10.4.3 Floyd算法

全源最短路问题:给定图g(V,E),求任意两点u,v之间的最短路径长度,时间复杂度为O(n3)

【注】顶点数<=200时可用。

int n,m;
int dis[maxn][maxn];

void floyd()
{
	for(int k=0;k<n;k++)
	{
		for(int i=0;i<n;i++)
		{
			for(int j=0;j<n;j++)
			{
				if(dis[i][k]!=inf&&dis[k][j]!=inf&&dis[i][k]+dis[k][j]<dis[i][j])
				{
					dis[i][j]=dis[i][k]+dis[k][j];
				}
			}
		}
	}
}

10.5 最小生成树

10.5.1 最小生成树及其性质

10.5.2 prim算法

10.6 拓扑排序

10.6.1 有向无环图

如果一个有向图的任一个点都无法通过一些有向边回到自身,则称这个图为有向无环图DAG。

10.6.2 拓扑排序

如果存在边(u,v),则拓扑排序中u一定在v的前面。

具体实现

  • 定义一个队列,把所有入度为0的结点加入到队列。
  • 取队首结点,输出。删去所有从他出发的边,并令这些边的另一端的结点入度-1。如果某个顶点的入度减为0,则将其入队。
  • 反复执行,直到队列为空。
  • 如果输出的结点数恰好为n,则拓扑排序成功,图G为有向无环图DAG,反之,图G中有环。

【注】邻接表实现比较好。

vector<int> g[maxn];
int n,m,inD[maxn];

bool topologicalSort()
{
	int num=0;
	queue<int> q;
	for(int i=0;i<n;i++)
		if(inD[i]==0) 	//入度为0
			q.push(i);
	while(!q.empty())
	{
		int u=q.front();
		printf("%d",u);
		for(int i=0;i<g[u].size();i++)
		{
			int v=g[u][v];
			inD[v]--; 	//入度--
			if(inD[v]==0)
				q.push(v); 
		}
		g[u].clear(); 	//清空顶点u出发的所有边
		num++; 
	} 
	if(num==n) return true;
	return false;
}

【注】如果要求有多个入度为0的顶点时选择序号最小的,把queue改成priority_queue,或者set也可以。


10.7 关键路径

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值