链式前向星,也叫静态邻接表,今天我讲的是关于图的东西,所以都以图的角度去看待这个较优化的存储结构。
存图优缺点
存图的时候,链式前向星是一种介于邻接矩阵和和邻接表的一种存储结构。
邻接矩阵
存图一般都用这个呀!不过啊,当遇到稀疏图的时候,顶点特别多,而边就那么几条,空间浪费必然很大,所以我们又想到了邻接表
邻接表
邻接表(未优化过的链式前向星)是最常用存储结构之一。 但是 vector(动态数组) 的时间效率较低 (较普通数组而言)。
所以,链式前向星也就因运而生。链式前向星是介于 邻接矩阵 和 邻接表 之间比较均衡的一种数据结构。
链式前向星
先来看一下代码:
//链式前向星的存储结构
struct node
{
int to;//边指向的终点
int next;//下一条边的存储下标
int w;//权值
}edge[maxn];
int head[maxn];
int cnt;
//增边
void add_edge(int u,int v,int w)
{
edge[cnt].w=w;
edge[cnt].to=v;
edge[cnt].next=head[u];
head[u]=cnt++;//需初始化cnt=0
}
举个列子:
这是我拿别人家博客的图片的哦,希望不要介意呀!
对于这样一个图,我们进行增边操作;
1.首先,cnt初始化,可以是0,也可以是一,看你个人喜好。head[]数组初始化,可以是0也可以是1.
2.调用函数增边:
for(int i=0;i<n;i++)//n代表边数
{
cin>>u>>v>>w;//u是起始点,v是这条边的终点,w是权值
add_edge(u,v,w);
}
例如上图,就是输入(我这里没权值,直接忽略吧):
1 2
2 3
3 4
1 3
4 1
1 5
4 5
这样就建成了一张链式前向星的图。
其他相信大家都好理解,就是 edge[].next=head[] 这个比较模糊,其实就是存的是以 i 为起点,连接着 i 的顶点编号的最大编号所在的边数,就如上图(按输入顺序编号),1->2(第0条边),1->3(第3条边),1->5(第5条边),而edge[5].next=3;edge[3].next=0;所以在遍历的时候,就相当于倒着遍历,就有这样的语句
for(int i=head[u];i!=-1;i=edge[i].next){
...
}//u是起点(当前节点)
有了这样一个邻接表的基础,那么对于图的最大经典问题,最短路,在这样的存储结构下又有什么不一样呢。看下面代码:
dijsktra
//堆优化的最短路
typedef pair<int ,int > P;//存编号与距离
void dijkstra(int s)//s为起点
{
priority_queue< P,vector<P>,greater<P> > q;
memset(d,inf,sizeof(d));
//memset(vis,0,sizeof(vis));
d[s]=0;
q.push(P(0,s));
while(!q.empty())
{
P p=q.top();
q.pop();
int u=p.second;
if(d[u]<p.first)
continue;
for(int i=head[u];i!=-1;i=edge[i].next)
{
int v=edge[i].to;
if(d[v]>d[u]+edge[i].w){
d[v]=d[u]+edge[i].w;
q.push(P(d[v],v));
}
}
}
}
上面是用了堆优化(priority_queue)后的dijkstra模板,最主要的思想就是松弛到不能再松弛位置, 然后遍历的过程就是松弛过程。可能对于一些题目, priority_queue< P,vector<P>,greater<P> > q;中的 greater 会有一些超时,重载运算符也是一种解决办法。复杂度:
O((V+E)lgV)
SPFA
int d[maxn];//记录最短距离
int vis[maxn];//判断是否在队列中
void SPFA(int s)//可以计算含负权的图
{
memset(d,inf,sizeof(d));
memset(vis,0,sizeof(vis));
queue<int > q;
q.push(s);
vis[s]=1;
d[s]=0;
while(!q.empty())
{
int u=q.front();
q.pop();
vis[u]=0;
//开始遍历松弛
for(int i=head[u];i!=-1;i=edge[i].next)
{
int v=edge[i].to;
if(d[v]>d[u]+edge[i].w)
{
d[v]=d[u]+edge[i].w;
if(!vis[v])
{
q.push(v);
vis[v]=1;
}
}
}
}
}
有没有觉得这个非常像,就是相差了vis[]这样一个数组判断,而这也就是差别,vis[]在SPFA就是判断当前点是否在队列中,从而进行松弛操作,还有就是vis[]还能判断图中是否有“负权环”(当vis[]>n, 就表示有环)。
复杂度:O(kE)O(kE)。
适用场景
如果是稠密图,Dijkstra+heap比SPFA快。稀疏图则SPFA更快。SPFA可以有SLF和LLL两种优化,SLF就是d比队头小就插入队头,否则插入队尾。
全部代码:
#include<iostream>
#include<queue>
#include<string.h>
using namespace std;
const int maxn=1e5+10;
const int inf=0x3f3f3f3f;
//链式前向星的存储结构
struct node
{
int to;//边指向的终点
int next;//下一条边的存储下标
int w;//权值
}edge[maxn];
int head[maxn];
int cnt;
//增边
void add_edge(int u,int v,int w)
{
edge[cnt].w=w;
edge[cnt].to=v;
edge[cnt].next=head[u];
head[u]=cnt++;//需初始化cnt=0
}
int d[maxn];//记录最短距离
int vis[maxn];//判断是否在队列中
/*
void SPFA(int s)//可以计算含负权的图
{
memset(d,inf,sizeof(d));
memset(vis,0,sizeof(vis));
queue<int > q;
q.push(s);
vis[s]=1;
d[s]=0;
while(!q.empty())
{
int u=q.front();
q.pop();
vis[u]=0;
//开始遍历松弛
for(int i=head[u];i!=-1;i=edge[i].next)
{
int v=edge[i].to;
if(d[v]>d[u]+edge[i].w)
{
d[v]=d[u]+edge[i].w;
if(!vis[v])
{
q.push(v);
vis[v]=1;
}
}
}
}
}
*/
//堆优化的最短路
typedef pair<int ,int > P;//存编号与距离
void dijkstra(int s)//s为起点
{
priority_queue< P,vector<P>,greater<P> > q;
memset(d,inf,sizeof(d));
//memset(vis,0,sizeof(vis));
d[s]=0;
q.push(P(0,s));
while(!q.empty())
{
P p=q.top();
q.pop();
int u=p.second;
if(d[u]<p.first)
continue;
for(int i=head[u];i!=-1;i=edge[i].next)
{
int v=edge[i].to;
if(d[v]>d[u]+edge[i].w){
d[v]=d[u]+edge[i].w;
q.push(P(d[v],v));
}
}
}
}
int main()
{
int m;//顶点数
int n;//边数
int u,v,w;
cnt=0;
cin>>m>>n;
memset(head,-1,sizeof(head));
for(int i=0;i<n;i++)
{
cin>>u>>v>>w;
add_edge(u,v,w);
}
dijkstra(1);
//SPFA(1);
for(int i=1;i<=m;i++)
cout<<d[i]<<" ";
cout<<endl;
return 0;
}
注意:对于inf这个定义,0x7fffffff和0x3f3f3f3f这两个也是一个大坑,
很多人可能设为0x7fffffff,这个数的确是32-bit int的最大值,符号位为0,其他的都是1
但在很多情况下,0x7fffffff会出现错误,比如溢出,这样两个无穷大数相加会变成负数,还有如在做dijkstra求最短路时,当做松弛操作,判断if (d[u]+w[u][v]<d[v]) d[v]=d[u]+w[u][v]时,若u到v没有路劲,w[u][v]=0x7fffffff,这样d[u]+w[u][v]会变成负数,这就产生了错误。
所以,memset(d,0x3f,sizeof(d));