存图
邻接矩阵
定义(x,y)是x到y的有向边,边权为w(x,y):
用矩阵A存,空间消耗是O(n^2)
A[x,y] | 含义 |
---|---|
0 | x=y |
w(x,y) | x,y联通 |
∞ | x,y不联通 |
邻接链表
定义(x,y)是x到y的有向边,边权为ver
开特定数组来存图,空间消耗是O(n+m),n代表出发点的值的数量,m为边数
名称 | 意义 |
---|---|
head[x] | 当前x的第一条边 |
nxt[i] | 定义i是x的一条边,表示x的下一条边 |
ver[i] | 表示i边到哪里 |
edge/weight[i] | i边的边权 |
tot | 表示当前有几个边 |
//加入有向边(x,y)权为z
void add(int x, int y, int z) {
ver[++tot] = y, edge[tot] = z; //构建边
nxt[tot] = head[x], head[x] = tot; //插入边
}
//遍历方法
for (int i = head[x];i;i = nxt[i]) {
int y = ver[i], z = edge[i];//此边到y,权值为z
}
单源最短路
Single Source Shortest Path, SSSP问题
给定有向图 G = (V,E)V是点集合,E是边集,|V|=n,|E|=m,用(x,y,z)描述一条边,求出1号点到所有点的最短路,并存入dis[i] 之中
Dijkstra算法
- 初始化 dis[1] = 0, 其余都为无穷大
- 找出一个为被标记过的点,并且dis[x] 最小,标记节点 x
- 扫描i的所有出边(x,y,z), 若dis[y] > dis[x] + z, 则用dis[x]+z更新dis[y]
- 重复2~3直到所有节点都被标记(在这个联通块内)
Dijkstra 基于贪心思想,所以只适用于边长非负数的图!
code:
#include <algorithm>
#include <cstdio>
#include <queue>
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010, M = 1000010;
int head[N],ver[M],edge[M],nxt[M],d[N];
bool vis[N];
int n,m,tot;
priority_queue< pair<int, int> > q;
// 大堆跟维护即可(取最大值即负数最小值)
// 第一个是距离的负数,第二个是节点编号
void add(int x,int y,int z) {
ver[++tot]=y, edge[tot]=z, nxt[tot]=head[x], head[x]=tot;
}
void dijkstra() {
memset(d, 0x3f, sizeof(d));
memset(vis, 0, sizeof(vis));
d[1] = 0;
q.push(make_pair(0, 1));
while(!q.empty()) {
int x = q.top().second; q.pop();
if(vis[x]) continue;
vis[x] = 1;
for(int i=head[x];i;i=nxt[i]) {
int y = ver[i], z = edge[i];
if(d[y] > d[x]+z){
d[y] = d[x] + z;
q.push(make_pair(-d[y],y));
}
}
}
}
int main() {
cin >> n >> m;
for(int i=1;i<=m;i++) {
int x, y, z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
dijkstra();
for(int i=1;i<=n;i++) {
printf("%d\n",d[i]);
}
}
Bellmen-Ford算法和SPFA算法
SPFA 为 Shortest Path Fast Algorithm 的缩写
给定一张有向图,若对图中的任意一遍(x,y,z), 有 dis[y] <= dis[x] + z 成立那么则称该边满足三角形不等式。若所有边都满足三角形不等式,则 dis 数组就是所求最短路。
迭代思想的Bellman-Ford算法:
- 扫描所有边(x,y,z),若 dis[y] > dis[x] + z 则用 dis[x] + z 更新 dis[y]
- 重复步骤1直到没有更新操作发生
实际上, SPFA在国际上统称之为“队列优化的Bellman-Ford算法”, 仅在中国叫SPFA,流程如下:
- 建立一个队列,最初队列只有起点1
- 取出对头节点x,扫描他的所有出边(x,y,z),若 dis[y] > dis[x] + z, 则用 dis[x] + z 更新dis[y]。同时,若y不在队列中,则把y入队
- 重复上述操作,直到队列为空
在任意时刻,该算法的队列仅存了需要扩展的节点,每一次入队完成一次dis数组的更新操作,使其满足三角不等式,一个节点可能会出对入队多次,最终图会都收敛到满足三角形不等式的状态,这个队列避免了 Bellman-Ford 算法对于不需要的点的冗余扫描,在稀疏图的理想情况下可以实现O(km)级别,并且k是一个较小的常数,但在稠密图上或者特殊的网格图上会退化到O(n,m)。但是SPFA可以跑负权图!
code:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
const int N = 100010, M = 1000010;
int head[N], ver[M], edge[M], nxt[M], d[N];
int n, m, tot;
bool vis[N];
queue<int> q;
void add(int x, int y, int z) {
ver[++tot] = y, edge[tot] = z, nxt[tot] = head[x], head[x] = tot;
}
void spfa() {
memset(d, 0x3f, sizeof(d));
memset(vis, 0, sizeof(vis));
d[1] = 0; vis[1] = 1;
q.push(1);
while(!q.empty()){
int x = q.front();q.pop();
vis[x] = 0;
for(int i=head[x];i;i = nxt[i]) {
int y = ver[i], z = ver[z];
if(d[y] > d[x] + z) {
d[y] = d[x] +z;
if(!vis[y]) q.push(y), vis[y]=1;
}
}
}
}
int main(){
cin >> n >> m;
for(int i=1;i<=m;i++) {
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x, y, z);
}
spfa();
for(int i=1;i<=n;i++) {
printf("%d\n", d[i]);
}
}
当图中存在负权边时,bellman-ford算法和 spfa 能够正常工作,但是时间复杂度会进一步增加,有神仙用双端队列的方法优化SPFA,其名为SLF,如果当前的 dis[y] 小于队头则压入队头, 否则压入队尾,一般情况能稍稍提高(没啥大作用)。
如果我们不存在负权边时候,可以使用类似dijkstra中的优先队列来进行优化,每次取出当前距离最小的的(堆顶)来进行拓展。。。。。然后,然后,然后,它就变成了dijkstra!。。。
其实在一般情况下是不需要优化滴!所以背会板子就好了
例题1
洛谷P3371:https://www.luogu.org/problemnew/show/P3371
注意:
1、自己改改板子交一下
2、第三个数据点毒瘤,要判重边!存最小的,方法很简答,单独存或者遍历出边即可
例题2
洛谷P1948: https://www.luogu.org/problemnew/show/P1948
目测题目具有单调性,因为当前的支付方案是一定建立在合法的方案上的,所以可以二分,进而转化问题成为:是否存在一种合法的升级方法使得花费不超过mid。转化后的判定问题非常容易,只需要把升级价格大于mid的电缆看作长度为1的边,把升级价格不超过mid的电缆看作为长度为0的边,然后求从1到N的最短路是否超过k即可,流程如下:
洛谷里面包含一组毒瘤数据!
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <queue>
#include <cstring>
using namespace std;
const int N = 1010, M = 20010;
int n,p,k,tot,head[N],ver[M],nxt[M],edge[M],dis[N];
bool vis[1005];
queue<int > q;
void add(int x,int y,int z){
ver[++tot] = y; edge[tot] = z;
nxt[tot] = head[x], head[x] = tot;
}
bool check(int x) {
memset(dis, 0x3f, sizeof(dis));
memset(vis, 0, sizeof(vis));
int s,now;
dis[1]=0,vis[1]=1;
q.push(1);
while(!q.empty()) {
now = q.front();q.pop();
vis[now] = 0;
for(int i=head[now];i;i=nxt[i]) {
int z = edge[i],y = ver[i];
if(z>x) s = dis[now]+1;
else s = dis[now];
if(s<dis[y]) {
dis[y]=s;
if(!vis[y]) {
q.push(y),vis[y]=1;
}
}
}
}
if(dis[n]<=k) return 1;
return 0;
}
int main(){
scanf("%d%d%d",&n,&p,&k);
int x, y, z;
for(int i=1;i<=p;i++) {
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);add(y,x,z);
}
int l=0,r=1000000,ans;
while(l<=r) {
int mid = (l+r)>>1;
if(check(mid)) {
ans = mid,r = mid-1;
}else l = mid+1;
}
cout << ans << endl;
return 0;
}
多元最短路
Floyd算法
为了求出图中任意两点间的最短距离,当然可以把每个点作为起点,求解N次单元最短路,不过在任意两点之间的问题中,图一般较为稠密,使用Floyd可以完成O(N^3)的时间
设 D[k, i, j] 表示经过若干个编号不超过k的节点从 i 到 j 的最短路长度,该问题可以划分成为两个子问题:经过编号k-1 的节点从i到j或者从i先到k再到j,于是我们可以得到公式:
D[ k, i ,j] = min(D [k-1,i ,j ], D[k-1,i,k] + D [k-1, k ,j] )
其实可以这样理解:每一轮把编号为k的点加进去,比较距离,因为之后的节点会保存之前节点的对应信息,所以关系会被一直传递下去
for (int k=1;k<=n;l++) //模拟加入一个点k
for(int i=1;i<=n;i++) //模拟起点i
for(int j=1;j<=n;j++) //模拟终点位置j
d[i][j] = min(d[i][j], d[i][k]+d[k][j]); //是否需要过这个点k