了解了优先队列,本来想写一道题目练练手,结果就看到了8441,看着像是bfs求最短路,然而T了,并不知道怎么优化,然后又去找老师要了标程,结果神仙代码看不懂(主要是因为太菜..),看到里面用了dijstra,就干脆先从最短路问题入手。
最短路问题,一般有三种方法,dijstra,bellman-forward,floyed,三者个有特色,适合于不同的场合。
一。dijstra(迪杰斯特拉)
Dijkstra算法
1.定义概览
Dijkstra(迪杰斯特拉)算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。该算法无法处理负权边。
问题描述:在无向图 G=(V,E) 中,假设每条边 E[i] 的长度为 w[i],找到由顶点 V0 到其余各点的最短路径。(单源最短路径)
2.算法描述
1)算法思想:设G=(V,E)是一个带权有向图,把图中顶点集合V分成两组,第一组为已求出最短路径的顶点集合(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将加入到集合S中,直到全部顶点都加入到S中,算法就结束了),第二组为其余未确定最短路径的顶点集合(用U表示),按最短路径长度的递增次序依次把第二组的顶点加入S中。在加入的过程中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度。此外,每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度,U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度。
2)算法步骤:
a.初始时,S只包含源点,即S={v},v的距离为0。U包含除v外的其他顶点,即:U={其余顶点},若v与U中顶点u有边,则<u,v>正常有权值,若u不是v的出边邻接点,则<u,v>权值为∞。
b.从U中选取一个距离v最小的顶点k,把k,加入S中(该选定的距离就是v到k的最短路径长度)。
c.以k为新考虑的中间点,修改U中各顶点的距离;若从源点v到顶点u的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值的顶点k的距离加上边上的权。
d.重复步骤b和c直到所有顶点都包含在S中。
3.执行动画过程如下图
4.例题:http://icpc.upc.edu.cn/problem.php?id=2716
算法实现:
#include <iostream> #include <bits/stdc++.h> // dijstra n^2 TLE using namespace std; const int maxn=5e6+10; const int inf=1e9+7; struct E { int v,w; }; vector <E> edge[maxn]; int in[maxn],dis[maxn];//in 数组表示在集合S内,dis表示到个点的最短距离 int dijstra(int s,int e,int n)//s 出发点 e 终止点 n 点数 { for (int i=0; i<=n; i++) dis[i]=inf; dis[s]=0,in[s]=1; for (int i=0; i<edge[s].size();i++) { dis[edge[s][i].v]=edge[s][i].w; //printf("to%d=%d\n",edge[s][i].v,dis[edge[s][i].v]); } //初始化 for (int i=0; i<=n; i++) { int mi=inf,k=s;//找到s点最短距离的点 for (int j=1; j<=n; j++) { if (!in[j] && dis[j]<mi) { mi=dis[j]; k=j; } } in[k]=1;//将最短的新点加入集合S int num=edge[k].size();//用新点k去扩展新点 for (int j=0; j<num; j++) { int v=edge[k][j].v,w=edge[k][j].w; if (!in[v]) { if (dis[k]+w<dis[v]) //relax { dis[v]=dis[k]+w; } } } } return dis[e]; } int main() { int n,m,t; scanf("%d%d%d",&n,&m,&t); for (int i=1; i<=m; i++) { int u,v,w; scanf("%d%d%d",&u,&v,&w); edge[u].push_back({v,w}); edge[v].push_back({u,w}); } int ans=dijstra(1,t,n); printf("%d\n",ans); return 0; }
其实dj算法就是BFS+贪心,它每次选一个点,然后扩散(bfs)到它的邻点之后,再从所有点中,选出离起点最近的点,继续扩散出去。这样总共n-1次之后,图上所有点离起点的距离必然是最小的。时间复杂度为n^2,n>1000一般稳稳地TLE。
6.优化:
考虑到每次都是用最近的那一个结点更新,暴力跑需要n的时间,太慢了。
怎么样能gkd呢?我们自然可以想到优先队列,因为每次要找的点有鲜明的特征,是距离s最近的点。这样优化后,n变成了logn,所以总的复杂度变为nlogn,瞬间快乐。
代码实现:
#include <iostream> #include <bits/stdc++.h> using namespace std; const int maxn=5e6+10; const int inf=INT_MAX/2; /*struct E { int v,w; bool operator< (const E& b) const { return w > b.w; } };*/ struct E { int v,w; friend bool operator< (E x,E y) { return x.w>y.w; } //重载<运算符,使得距离小的优先级大 }; /*struct cmp { bool operator() (const E &x,const E &y) const { return x.w>y.w; } };*/ int vis[maxn],dis[maxn]; vector <E> edge[maxn]; int dijheap(int s,int e,int n) { priority_queue <E> Q; for (int i=0; i<=n; i++) dis[i]=inf; Q.push({s,0}); dis[s]=0; while(!Q.empty()) { E cur=Q.top();//保证取出的队首元素就是距离s最近的 Q.pop(); int cv=cur.v; if (vis[cv]) continue; vis[cv]=1; int num=edge[cv].size(); for (int i=0;i<num;i++)//用这个点去扩展relax { int v=edge[cv][i].v,w=edge[cv][i].w; if (!vis[v]) { if (dis[v]>dis[cv]+w) { dis[v]=dis[cv]+w; Q.push({v,dis[v]}); } } } } return dis[e]; } int main() { int n,m,t; scanf("%d%d%d",&n,&m,&t); for (int i=1; i<=m; i++) { int u,v,w; scanf("%d%d%d",&u,&v,&w); edge[u].push_back({v,w}); edge[v].push_back({u,w}); } int ans=dijheap(1,t,n); printf("%d\n",ans); return 0; }
7.小结
Dijstra算法十分优秀,在使用堆(优先队列)优化的情况下,时间复杂度为nlogn,如果题目中不是单源的最短路,那么可以每个点都作为起点跑一下dj算法,n^2logn。
缺点:
dj算法是无法处理负权边的!为什么呢,因为dj算法是贪心BFS,而BFS有一个特点,就是短视! 它只能看到与自己相邻的点的情况,但是对于远方,它就一脸蒙蔽了。如果有两种走法,一种是直接走边长5到达,一种是先走10,再走-20到达,显然,我们的dijstra算法会直接走第一种。
8.扩展
其实,dijstra算法,还可以输出最短路的路径,只需要用一个pre数组记录一下每个节点的前驱结点,递归输出就可以了。
代码实现:
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int maxn=1e3+20; const int inf=1e9+7; int dis[maxn],vis[maxn],pre[maxn]; struct E { int v,w; bool friend operator< (E x,E y) { return x.w>y.w; } }; vector <E> edge[maxn]; void dij(int s,int n) { priority_queue <E> Q; while(Q.size()) Q.pop(); for (int i=1; i<=n; i++) dis[i]=inf; dis[s]=0;pre[s]=s; Q.push({s,0}); while(!Q.empty()) { E cur=Q.top(); Q.pop(); int cv=cur.v; int num=edge[cv].size(); if (vis[cv]) continue; vis[cv]=1; for (int i=0; i<num; i++) { int v=edge[cv][i].v,w=edge[cv][i].w; if (dis[v]>dis[cv]+w) { dis[v]=dis[cv]+w; Q.push({v,dis[v]}); pre[v]=cv; } } } } void outway(int i) { if (pre[i]!=i) { printf("%d-->",i); outway(pre[i]); } else printf("1\n"); return ; } int main() { int n,m; freopen("out2.txt","w",stdout); while(~scanf("%d%d",&n,&m)) { if (n==0&&m==0) break; memset(vis,0,sizeof(vis)); memset(edge,0,sizeof(edge)); memset(pre,0,sizeof(pre)); for (int i=1; i<=m; i++) { int u,v,w,flag=0; scanf("%d%d%d",&u,&v,&w); edge[u].push_back({v,w}); } dij(1,n); for (int i=2; i<=n; i++) i==n ? printf("%d\n",dis[i]) :printf("%d ",dis[i]); for (int i=2; i<=n; i++) outway(i); } return 0; }
需要正序输出的话,其实也可以,记录