图论最短路(模板/模板题)

最短路问题就是字面意思 求解两点之间的最短路程/时间

最短路一般有四种算法模板

ad4158b3a3514d6caa65ecfd8e8d0f87.png

图的存储一般有三种方法

1.邻接矩阵

直接开一个二维数组mp[x][y]表示 x---->y的距离如果存在重边(平行边)存储最短的即可因为求的是最短路嘛hhhhh 

2.邻接链表的方法

用结构体存储边的所有信息 终点 权值再用一个vecotr<struct>e[N]

e[i]就表示所有以i为起点的边

具体code:

struct node{
    int from,to,w;//分别表示起点终点个权
}edge;
vector<edge>e[N];
void add(int a,int b,int c){
    e[a].push_back(a,b,c);
}

因为是动态存储所以效率较低不推荐使用

3.链式前向星

有点类似于哈希表里的拉链法用一个数组h[]表示头h[i]表示以i为起点的所有边 在一个头下面拉一个链表(模拟链表)

两种实现方法

1.结构体法

struct node{
    int from,to,w,next;//分布表示起点,终点,权,以i为起点的下一条边的下标值
}egde[N];
int h[N],idex;
void add(int a,int b,int c){
    edge[idex]={a,b,c,h[a]};
    h[a]=idex++;
}//模拟链表的添加

2.数组法

h[]表示头 e[]表示终点 ne[]表示下一条以i为起点的下一条边的下标值 w[]表示权

int h[N],e[N],ne[N],w[N],idex;
void add(int a,int b,int c){
    e[idex]=b;
    w[idex]=c;
    ne[idex]=h[a];
    h[a]=idex++;
}

上述三种方法适合用的时机为:确定题目需要建的图是稀疏图还是稠密图 稀疏用邻接矩阵 稠密图用邻接链表/链式前向星 

 从最简单的Dijkstra开始说起

手动实现的稀疏矩阵好像更好一点 因为是静态的 再来需要一个数组记录距离起点的距离 dist[N] 还有一个数组记录这个点到起点的距离是不是最短距离
这里有一个小定理 就是对于当前点为起点的所有未被确认为最短距离邻接边最短的一定是这个点到起点的最短距离 拥有这些东西 就可以解决问题了(这个定理就是Dij的实现原理)下面以一个模板题目来看一下具体操作

题目描述:
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。

输入格式
第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。

如果路径不存在,则输出 −1。

数据范围
1≤n≤500,
1≤m≤105,
图中涉及边长均不超过10000。

输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3

题源:acwing

code:

#include<bits/stdc++.h>
using namespace std;
const int N=510;
int g[N][N],dis[N],n,m;//dis表示从i点到起点的距离
bool st[N];//表示i点是否确定了最短距离
int Dijkstra(){
    memset(dis,0x3f,sizeof(dis));//注意初始化
    dis[1]=0;
    for(int i=1;i<=n;i++){
        int t=-1;
        for(int j=1;j<=n;j++){
            if(!st[j]&&(t==-1||dis[t]>dis[j]))t=j;//寻找还没有确定最短距离的点的最小(定理的应用)
        }
        st[t]=true;
        for(int j=1;j<=n;j++)dis[j]=min(dis[j],dis[t]+g[t][j]);//跟新暂时最短距离
    }
    if(dis[n]==0x3f3f3f3f)return -1;
    else return dis[n];
}
int main(){
    cin>>n>>m;
    int x,y,c;
    memset(g,0x3f,sizeof(g));//因为可能出现重边
    while(m--){
        scanf("%d%d%d",&x,&y,&c);
        g[x][y]=min(g[x][y],c);
    }
    cout<<Dijkstra()<<endl;
}

2.Dij堆优化版

这个版本适用于稀疏图

优化的点就是 寻找还没确定最短路径的边的最小值的时候 直接将边放入一个优先队列中然后从队头取出如果这个边确定了最短边再取出一个即可

题目描述:
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。

输入格式
第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。

如果路径不存在,则输出 −1。

数据范围
1≤n,m≤1.5×105,
图中涉及边长均不小于 0,且不超过 10000。
数据保证:如果最短路存在,则最短路的长度不超过 109。

输入样例:
3 3
1 2 2
2 3 1
1 3 4
因为是稀疏图用邻接链表
 

#include<bits/stdc++.h>
using namespace std;
const int N=150010;
int h[N],e[N],ne[N],w[N],dis[N],n,m,idex;
bool st[N];//表示是否确定了最短距离
typedef pair<int,int>PII;
priority_queue<PII,vector<PII>,greater<PII> >q;
void add(int a,int b,int c){
    e[idex]=b;
    ne[idex]=h[a];
    w[idex]=c;
    h[a]=idex++;
}
int Dijkstra(){
    memset(dis,0x3f,sizeof(dis));
    dis[1]=0;
    q.push({0,1});
    while(!q.empty()){
        int distance=q.top().first,id=q.top().second;
        q.pop();
        if(st[id])continue;
        st[id]=1;
        for(int i=h[id];i!=-1;i=ne[i]){
            if(dis[e[i]]>dis[id]+w[i])dis[e[i]]=dis[id]+w[i];
            q.push({dis[e[i]],e[i]});
        }
    }
    if(dis[n]==0x3f3f3f3f)return -1;
    else return dis[n];
}
int main(){
    cin>>n>>m;
    int a,b,c;
    memset(h,-1,sizeof(h));
    while(m--){
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    cout<<Dijkstra()<<endl;
} 

注意:Dij只能用于无负权边的情况

2.bellman-ford(可以处理负权边)

Bellman_ford求最短路一般解决的问题都是存在负边 或者 最短路有边个数量限制的 而且Bellman_ford中存边的信息直接用结构体存储比较简单 bellman_ford大概的思路就是一个二重循环第一个循环循环n次第二个循环遍历每一个边用每一个边更新与其相邻的点到终点的距离即可(第一重循环n次的实际意义就是从起点到目标的不超过n条边的最短路)

例题:

题目描述:
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。

请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible。

注意:图中可能 存在负权回路 。

输入格式
第一行包含三个整数 n,m,k。

接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

点的编号为 1∼n。

输出格式
输出一个整数,表示从 1 号点到 n 号点的最多经过 k 条边的最短距离。

如果不存在满足条件的路径,则输出 impossible。

数据范围
1≤n,k≤500,
1≤m≤10000,
1≤x,y≤n,
任意边长的绝对值不超过 10000。

输入样例:
3 3 1
1 2 1
2 3 1
1 3 3
输出样例:
3

code:

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int n,m,k,dist[N],backup[N];/*记录上次各个点到起点的最短路(暂时)因为循环的边没有顺序如果不记录上次的点的距离的话 那么这个点可能已经在前面已经被更新过了而我们的操作就错误了*/
struct node{
int from,to,w;
}edge[N];
int bellman_ford(){
	memset(dist,0x3f,sizeof(dist));
	dist[1]=0;
	for(int i=0;i<k;i++){//循环k次的意义就是边数小于等于k的最短路
		memcpy(backup,dist,sizeof(dist));
		for(int j=1;j<=m;j++){
			int a=edge[j].from,b=edge[j].to,c=edge[j].c;
			dist[b]=min(dist[b],backup[a]+c);
		}
	}
	return dist[n];
}
int main(){
	cin>>n>>m>>k;
	int a,b,c;
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&a,&b,&c);
		edge[i]={a,b,c};
	}
 int t=bellman_ford();
if(t>0x3f3f3f3f/2)cout<<"imposiable";
else cout<<t;
}

 spfa(bellman的优化)

pfa是对bellman_ford的优化不再去循环比较每一个边是否能更新最短路 而是通过如果当前点的最短路变化那么其他的邻接边的最短路一定也变化的思想
所以只需要一个队列将这些最短路变化的点存入即可 然后再需要一个数组来记录每一个点是否在队列里 防止重复

例题

题目描述:
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible。

数据保证不存在负权回路。

输入格式
第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。

如果路径不存在,则输出 impossible。

数据范围
1≤n,m≤105,
图中涉及边长绝对值均不超过 10000。

输入样例:
3 3
1 2 5
2 3 -3
1 3 4
输出样例:
2
code:

#include<bits.stdc++.h>
using namespace std;
const int N=100010;
int h[N],e[N],ne[N],w[N],idex,n,m,dist[N];
bool st[N]//判断当前点是否在队列里
void add(int a,int b,int c){
	e[idex]=b;
	ne[idex]=h[a];
	w[idex]=c;
	h[a]=idex++;
}
int spfa(){
	memset(dist,0x3f,sizeof(dist));
	dist[1]=0;
	st[1]=true;
	queue<int>q;
	while(!q.empty()){
	int pos=q,front();
	q.pop();
	st[pos]=false;
	for(int i=h[pos];i!=-1;i=ne[i]){
		if(dist[e[i]]>dist[pos]+w[i]){//如果距离变短就更新并且放入因为这个点的变小可能会引起其他点的变小
			dist[e[i]]=dist[pos]+w[i];
			if(!st[e[i]])q.push(e[i]),st[e[i]]=true;
		}
	}
	return dist[n];
}
int main(){
	cin>>n>>m;
	int a,b,c;
  	while(m--){
		cin>>a>>b>>c;
		add(a,b,c)
	}
	int t=spfa();
	if(t==0x3f3f3f3f)cout<<"impossible";
	else cout<<t<<endl;
} 

spfa判断负环

什么是负环

例如

f77a8e3662e7415a88fdab2db3022ce8.png

 我们目标是从1到5的最短路但是可以发现从2——>3——>4——>2这个环上总和是一个负数每多走一圈距离就变短一次所以这个1——5的最短路为-∞那么怎么判断呢 根据抽屉原理可以得知n个点里任意两个点的任意路程如果不经过重复的点那么边的个数一定小于n 那么可以根据此方法来判断负环 维护一个cnt数组表示从起点到当前点的最短路的边 只要出现一个cnt[i]>n就一定有负环出现值得注意的是 负环不一定从1点可以到达 所以我们要把所有的点都当作起点枚举一下寻找

code:

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int h[N],ne[N],e[N],w[N],dist[N],n,m,cnt[N],idex;
bool st[N];
void add(int a,int b,int c){
	e[idex]=b;
	ne[idex]=h[a];
	w[idex]=c;
	h[a]=idex++;
}
bool spfa(){
	queue<int>q;
	for(int i=1;i<=n;i++)q.push(i),st[i]=true;
	while(!q.empty()){
		int pos=q.front();
		q.pop();
		st[pos]=false;
		for(int i=h[pos];i!=-1;i=ne[i]){
			if(dist[e[i]]>dist[pos]+w[i]){
				dist[e[i]]=dist[pos]+w[i];
				cnt[e[i]]=cnt[pos]+1;
				if(!st[e[i]])st[e[i]]=true,q.push(e[i]);
				if(cnt[e[i]]>=n)return true;
			}
		}
	}
	return false;
}
int main(){
	memset(h,-1,sizeof(h));
	int a,b,c;
	cin>>n>>m;
	while(m--){
		scanf("%d%d%d",&a,&b,&c);
		add(a,b,c);
	}
	if(spfa())cout<<"Yes"<<endl;
	else cout<<"No"<<endl;
}

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值