图
在数学中,图是描述于一组对象的结构,其中某些对象对在某种意义上是“相关的”。这些对象对应于称为顶点的数学抽象(也称为节点或点),并且每个相关的顶点对都称为边(也称为链接或线)。
存图
1.邻接矩阵
用二维数组存图
2.邻接表
//链式前向星存图
//适用于边数较少的「稀疏图」使用,当边数量接近点的数量,可定义为「稀疏图」。
int[]he=new int[N],e=new int[M],ne=new int[M],w=new int[M];
int idx;//idx 是用来对边进行编号的
void add(int a,int b,int c){
e[idx] = b;//e数组:由于访问某一条边指向的节点;
ne[idx] = he[a];//ne 数组由于是以链表的形式进行存边,该数组就是用于找到下一条边;
he[a] = idx;//he数组:存储是某个节点所对应的边的集合(链表)的头结点;
w[idx] = c;//w数组:用于记录某条边的权重为多少。
idx++;}
//因此当我们想要遍历所有由 a 点发出的边时,可以使用如下方式:
for (int i=he[a]; i!=-1; i=ne[i])
{
int b=e[i], c=w[i];
}
// 存在由 a 指向 b 的边,权重为 c
示例代码如下
#include<bits/stdc++.h>
#define F(i,n,m) for(int i=n;i<=m;i++)
int n,m,k;
int u[6],v[6],w[6];//要比m的最大值要大1
int first[5],next[5];//要比n的最大值要大1
using namespace std;
int main()
{
scanf("%d %d",&n,&m);
F(i,1,n) first[i]=-1;
//初始化first数组下标1~n的值为-1,表示1~n顶点暂时都没有边
F(i,1,m)//读入每一条边
{
scanf("%d %d %d",&u[i],&v[i],&w[i]);
//下面两句是关键啦
next[i]=first[u[i]];
first[u[i]]=i;
}
k=first[1]; //1号顶点其中的一条边的编号(其实也是最后读入的边)
while(k!=-1) //其余的边都可以在next数组中依次找到
{
k=next[k];
F(i,1,n)
{
k=first[i];
while(k!=-1)
{
k=next[k];
}
}
}
return 0;
}
3.类
最短路
给定两个点,起点是s,终点是t,在所有能连接s和t的路径中寻找边的权值之“和” 最小的路径,这就是最短路径问题
路径长度:一条路径上所经过的边的数目
带权路径长度:路径上所经过边的权值之和
最短路径:带权路径长度值最小的那条路径
单源最短路:从单个节点出发,到所有节点的最短路//如求两点间最短路
多源最短路:整个图中所有点到其他点的最短路//如列举每两两个城市最短距离
无权图算法
BFS
后续的有向图算法也可以在边的时候存下反的
有权图算法
算法\对比 | 主要适用方向 | 时间复杂度 | 处理负权边 | 处理负权回路 |
Floyd | 带权图的多源最短路径 | O(V^3) | YES | NO |
Dijkstra | 带权图的单源最短路径 | O(E*logE) | NO | NO |
Bellman-Ford | 带权图的单源最短路径 | O(V*E) | YES | YES |
SPFA | 带权图的单源最短路径 | 最坏O(V*E) | YES | YES |
Floyd
动态规划
Floyd-Warshall算法是一种在具有正或负边缘权重(但没有负周期)的加权图中找到最短路径的算法。算法的单个执行将找到所有顶点对之间的最短路径的长度(加权)。 虽然它不返回路径本身的细节,但是可以通过对算法的简单修改来重建路径。
Floyd算法适用于APSP(多源最短路径),稠密图效果最佳,边权可正可负。此算法简单有效,由于三重循环结构紧凑,对于稠密图,效率要高于执行|V|次Dijkstra算法,也要高于执行|V|次SPFA算法。
优点:容易理解,可以算出任意两个节点之间的最短距离,代码编写简单。
缺点:时间复杂度比较高,不适合计算大量数据。
#include<bits/stdc++.h>
using namespace std;
const int INF=0x3f3f3f;
int e[1000][1000],n,m,x,y,z;
int main()
{
cin>>n>>m;//n表示顶点个数,m表示边的条数
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(i!=j) e[i][j]=INF;
//此题存储:0即回路 inf即没有边 正数or负数表示有路,路的权值
}
}
for(int i=1;i<=m;i++)//存边 x起点 y终点 z是权值
{
cin>>x>>y>>z;
e[x][y]=z;
}
for(int k=1;k<=n;k++)//k表示的是中转的那个值
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
e[i][j]=min(e[i][j],e[i][k]+e[k][j]);
//情况不多,无路不会覆盖,回路不影响,路短覆盖符合要求
}
}
}//e[i][j]如果没有变过,但不是最小值,可以覆盖(即使有直达边,也不一定是权值最小的)
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
cout<<e[i][j]<<" ";
}
cout<<endl;
}
return 0 ;
}
向量(Vector)
是一个封装了动态大小数组的顺序容器,跟任意其它类型容器一样,它能够存放各种类型的对象。
1.顺序序列 顺序容器中的元素按照严格的线性顺序排序。可以通过元素在序列中的位置访问对应的元素。
2.动态数组 支持对序列中的任意元素进行快速直接访问,甚至可以通过指针算述进行该操作。提供了在序列末尾相对快速地添加/删除元素的操作。
3.能够感知内存分配器的(Allocator-aware) 容器使用一个内存分配器对象来动态地处理它的存储需求。
优先队列priority_queue
priority_queue<int> q;
priority_queue<int, vector<int>, less<int> > q;
//less表示按照递减(从大到小)的顺序插入元素
priority_queue<int, vector<int>, greater<int> > q;
//greater表示按照递增(从小到大)的顺序插入元素
q.empty();//队列空为真
q.pop();//删除对顶元素
q.push();//加入一个元素
q.size();//返回拥有元素个数
q.top();//返回对顶元素 (默认int型最大的优先级高,先出队)
struct cmp
{
operator bool()(int x,int y)
{
return p[x]>p[y];
//自定义优先级高的为p[i]小的
}
}
Dijkstra
贪心思想
给定图G和起点v0,通过算法得到v0到达其他每个顶点的最短距离。
每拓展一个路程最短的点,更新与其相邻点的路程,当所有边权为正的时候,不会存在一个路程更短的没拓展的点,所以这个点的路程永远不会再被改变,因而保证了算法正确性。
但是本题不能求负权边,因为拓展到负权边的时候会产生更短的路程,就可能破坏已经更新的点路程不改变的性质。
代码讲解
#include<bits/stdc++.h>
#define F(i,n,m) for(int i=n;i<=m;i++)
int e[10][10],dis[10],book[10],i,j,n,m,t1,t2,t3,u,v,minn;//初始都是0
const int inf=0x3f3f3f;
using namespace std;
int main()
{
scanf("%d %d",&n,&m); //读入n和m,n表示顶点个数,m表示边的条数
F(i,1,n)
F(j,1,n)
if(i!=j) e[i][j]=inf;//不存在的路会是inf
F(i,1,m)
{
scanf("%d %d %d",&t1,&t2,&t3);
e[t1][t2]=t3;
}//读入边
F(i,1,n)
dis[i]=e[1][i];
//初始化dis数组,这里是1号顶点到其余各个顶点的初始路程
book[1]=1;
//book数组初始化
F(i,1,n-1)
{
minn=inf;
F(j,1,n)
{
if(book[j]==0 && dis[j]<minn)//从第一条1可以到的j开始
{
minn=dis[j];
u=j;
}
}//1到j的最短路覆盖
book[u]=1;
F(v,1,n)//u是中转点,
{
if(e[u][v]<inf)//如果u到v有路
{
if(dis[v]>dis[u]+e[u][v]) dis[v]=dis[u]+e[u][v];//1-v有路则比较距离,1-v没路则直接覆盖
}
}
}
//输出最终的结果
F(i,1,n)
printf("%d ",dis[i]);
return 0;
}
1.把所有的顶点分为两部分:已知最短路程的顶点集合P和未知最短路径的顶点集合Q。 最开始,P集合中只有源点一个顶点,用visit[i]数组来记录哪些点在集合P。visit[i]=1表示这个顶点在P集合中,visit[i]=0表示这个顶点在Q集合中。
2.设置源点s到自己的最短路径为0,即dis[s]=0,若存在有源点能直接到达的顶点i,则把dis[i]设为e[s][i],同时把所有其他(源点不能到达的)顶点的最短路径设为inf。
3.在集合Q的所有顶点中选择一个离源点s最近的顶点u(dis[u]最小)加入到集合P,并考察所有以点u为起点的边,对每条边进行松弛操作。例如:存在一条u到v的边,通过u->v添加到尾部来拓展一条从s到v的路径,这条路径的长度是dis[u]+e[u][v]。如果它的值比目前已知的dis[v]的值要小,我们可以用新值来替代当前dis[v]中的值。
4.重复第3步,如果集合Q为空,算法结束,最终dis数组中的值就是源点到所有顶点的最短路径
#include<bits/stdc++.h>
#define F(i,n,m) for(int i=n;i<=m;i++)
using namespace std;
//int head[10],e[10],next[10],dist[10],vis[10],n,m,t1,t2,t3,u,v,minn;//初始都是0
const int inf=0x3f3f3f;
vector<int>head(20,-1),e(20,0),nex(100,0),w(20,0),dist(20,inf);//head存第一条边的序号,head的下标是点的序号
vector<bool>vis(20,false);
int n,m,t1,t2,t3,u,v,minn;
int main()
{
scanf("%d %d",&n,&m); //读入n和m,n表示顶点个数,m表示边的条数
F(i,1,m)
{
scanf("%d %d %d",&t1,&t2,&t3);
e[i]=t2;
nex[i]=head[t1];
head[t1]=i;
w[i]=t3;
}//读入边
dist[1]=0;
//初始化dis数组,这里是1号顶点到其余各个顶点的初始路程
priority_queue<int,vector<pair<int,int>>,greater<>>q;//第一个数代表距离,第二个数代表点
q.push({0,1});
while(!q.empty())
{
int id=q.top().second;
q.pop();
if(vis[id]) continue;
vis[id]=true;
for(int i=head[id];i!=-1;i=nex[i])//i是边的序号,id是点的序号,j是id连接的点的序号
{
int j=e[i];
if(dist[j]>dist[id]+w[i])
{
dist[j]=dist[id]+w[i];
}
q.push({dist[j],j});
}
}
//输出最终的结果
F(i,1,n)
printf("%d ",dist[i]);
return 0;
}
L2-1紧急救援 (25 分)
地图上显示有多个分散的城市和一些连接城市的快速道路。每个城市的救援队数量和每一条连接两个城市的快速道路长度都标在地图上。当其他城市有紧急求助电话给你的时候,你的任务是带领你的救援队尽快赶往事发地,同时,一路上召集尽可能多的救援队。
输入格式: 输入第一行给出4个正整数N、M、S、D,其中N(2≤N≤500)是城市的个数,顺便假设城市的编号为0 ~ (N−1);M是快速道路的条数;S是出发地的城市编号;D是目的地的城市编号。 第二行给出N个正整数,其中第i个数是第i个城市的救援队的数目,数字间以空格分隔。随后的M行中,每行给出一条快速道路的信息,分别是:城市1、城市2、快速道路的长度,中间用空格分开,数字均为整数且不超过500。输入保证救援可行且最优解唯一。
输出格式: 第一行输出最短路径的条数和能够召集的最多的救援队数量。第二行输出从S到D的路径中经过的城市编号。数字间以空格分隔,输出结尾不能有多余空格。
a数组存城市救援队数量
h,e向量,邻接表存图 存每个点连接边
w存每个边的权值(距离)
注意该题是无向图需要反向存一下
dist向量存点s到其他各点的最短距离(dij
c 取s到某处城市集结的最多救援队
q大根堆先更新距离最近的点的最短距离
vis bool向量判断走没走过
citys 二维向量 存s到其他点最短路经过的点
num存s到其他点 最短路径的数量
初始化处理
取出堆中s到其他点最小的距离,从堆中删除
dij寻找以它的点为中转点的到别的点j的最短距离
如果dis[j]需要更新,则更新一些数组向量,并把j和s到j的最短路更新
#include<bits/stdc++.h>
#define F(i,n,m) for(int i=n;i<=m;i++)
using namespace std;
const int inf=0x3f3f3f;
int t1,t2,t3;
int n,m,s,d;
int main()
{
cin>>n>>m>>s>>d;
vector<int>h(n*2+1,-1),e(m*2+1,0),ne(m*2+1,0),w(m*2+1,0),dist(m*2+1,inf),num(n*2+1,1);//head存第一条边的序号,head的下标是点的序号
vector<bool>vis(n*2+1,false);
int a[n*2+1];//每个城市的救援队
vector<int>c(n*2+1,0);//到每个城市集结的最多救援队
F(i,0,n-1)
{
cin>>a[i];
}
F(i,0,m-1)
{
scanf("%d %d %d",&t1,&t2,&t3);
e[i]=t2;
ne[i]=h[t1];
h[t1]=i;
w[i]=t3;
e[i+m]=t1;
ne[i+m]=h[t2];
h[t2]=i+m;
w[i+m]=t3;
}
dist[s]=0;
priority_queue<int,vector<pair<int,int>>,greater<>>q;//第一个数代表距离,第二个数代表点
vector<vector<int>>citys(n*2+1);
q.push({0,s});
c[s]=a[s];
citys[s].push_back(s);
while(!q.empty())
{
int id=q.top().second;
q.pop();
if(vis[id]) continue;
vis[id]=true;
for(int i=h[id];i!=-1;i=ne[i])//i是边的序号,id是点的序号,j是id连接的点的序号
{
int j=e[i];
if(dist[j]>dist[id]+w[i])
{
dist[j]=dist[id]+w[i];
num[j]=num[id];
c[j]=c[id]+a[j];
citys[j]=citys[id];
citys[j].push_back(j);
q.push({dist[j],j});
}
else if(dist[j]==dist[id]+w[i])
{
num[j]+=num[id];
if(c[j]<c[id]+a[j])
{
c[j]=c[id]+a[j];
citys[j]=citys[id];
citys[j].push_back(j);
}
q.push({dist[j],j});
}
}
}
cout<<num[d]<<" "<<c[d]<<endl;
int l=citys[d].size();
F(i,0,l-2) cout<<citys[d][i]<<" ";
cout<<citys[d][l-1]<<endl;
}
L3-011 直捣黄龙 (30 分)
#include<bits/stdc++.h>
using namespace std;
#define F(i,n,m) for(int i=n;i<=m;i++)
int main()
{
int n,m;
string s,d;
cin>>n>>m>>s>>d;
vector<int> sum(n+1,0),e(2*m+1,0),ne(2*m+1,0),f(2*n+1,-1),w(2*m+1),dis(n+1,0xffffff),c(n+1,0),num(n+1,1);
vector<int> vis(n+1,0);
vector<vector<int> >city(n+1);
map<string ,int> a;
a[s]=1;
sum[1]=0;
F(i,2,n)
{
string x;
int y;
cin>>x>>y;
a[x]=i;
sum[i]=y;
}
F(i,1,m)
{
string x,y;
int z;
cin>>x>>y>>z;
e[i]=a[y];
ne[i]=f[a[x]];
f[a[x]]=i;
w[i]=z;
e[i+m]=a[x];
ne[i+m]=f[a[y]];
f[a[y]]=i+m;
w[i+m]=z;
}
dis[a[s]]=0;
priority_queue<int,vector<pair<int ,int>>,greater<> >q;
q.push({0,a[s]});
c[a[s]]=sum[a[s]];
city[a[s]].push_back(a[s]);
while(!q.empty())
{
int id=q.top().second;
q.pop();
if(vis[id]) continue;
vis[id]=1;
for(int i=f[id];i!=-1;i=ne[i])
{
int j=e[i];
if(dis[j]>dis[id]+w[i])
{
dis[j]=dis[id]+w[i];
num[j]=num[id];
c[j]=c[id]+sum[j];
city[j]=city[id];
city[j].push_back(j);
}
else if(dis[j]==dis[id]+w[i])
{
num[j]+=num[id];
if(city[id].size()+1>city[j].size())
{
c[j]=c[id]+sum[j];
city[j]=city[id];
city[j].push_back(j);
}
if(city[id].size()+1==city[j].size())
{
if(c[j]<c[id]+sum[j])
{
c[j]=c[id]+sum[j];
city[j]=city[id];
city[j].push_back(j);
}
}
}
q.push({dis[j],j});
}
}
int l=city[a[d]].size();
F(i,0,l-1)
{
for(auto j:a)
{
if(city[a[d]][i]==j.second) cout<<j.first;
}
if(i!=l-1) cout<<"->";
else cout<<endl;
}
cout<<num[a[d]]<<" "<<dis[a[d]]<<" "<<c[a[d]]<<endl;
}
Bellman-Ford
初始时将源点加入队列。每次从队首(head)取出一个顶点,并对与其相邻的所有顶点进行松弛尝试,若某个相邻的顶点松弛成功,且这个相邻的顶点不在队列中(不在head和tail之间),则将它加入到队列中。对当前顶点处理完毕后立即出队,并对下一个新队首进行如上操作,直到队列为空算法结束。
使用队列优化的Bellman-Ford算法在形式上与广度优先搜索非常类似,不同的是在广度优先搜索的时候一个顶点出队后通常就不会再重新进入队列。而队列优化的Bellman-Ford算法一个顶点很可能在出队列之后再次被放入队列,也就是当一个顶点的最短路程估计值变小后,仍需要对其所有出边再次进行松弛,这样才能保证相邻顶点的最短路程估计值同步更新。
如何判断通过队列优化的Bellman-Ford算法一个图有负环呢?
如果某个点进入队列的次数超过n次,那么这个图肯定存在负环。
使用队列优化的Bellman-Ford算法的关键之处在于:
只有那些在前一遍松弛中改变了最短路程估计值的点,才可能引起它们邻接点最短路程估计值发生改变。
部分资料来源网络,参考书籍《啊哈!算法》