3.21 最短路

思路

  • 首先设置一个d[i]数组,用于表示从起点到点i最短距离是多少
  • 对于起点来说,d[]=0,对于其他点来说d[]=正无穷
  • 然后遍历n次(n个节点)
  • 假设我们设定一个集合s,表示已经确定最短路径的节点
  • for循环中,每次都选择一个d[]最小的节点放入s中,然后该节点的最短距离就确定了(因为其他节点的d[]都更大,不可能在更新时使得这个d[]最小的节点可以以更短的路径到达)
  • 对于选出的节点t,看其是否能将不在s中的节点的最短路径(d[])更新,如果可以便更新,重复该流程,直到所有节点都被放进s

Dijkstra求最短路 I

这题需要注意的是!
关于0x3f的用法
memset中使用0x3f
但是实际上被赋的值是0x3f3f3f3f

迪杰斯特拉算法边权必须都是正数!!!


#include
#include
#include

using namespace std;

const int N=1e4;
const int M=1e5+10;
int n,m;

int h[N],ne[M],e[M],w[M],idx=1;
int s[N];
int d[N];

void add(int x,int y,int z){
	e[idx]=y;
	ne[idx]=h[x];
	w[idx]=z;
	h[x]=idx;
	idx++;
}

void Dijkstra(){
	
	memset(d,0x3f,sizeof d);
	d[1]=0;
	int min_num;
	for(int i=1;i<=n;i++){
		int t;
		min_num=0x3f3f3f3f;
		
		for(int j=1;j<=n;j++){
			//找到最小的t 
			if(d[j]<=min_num&&s[j]==0){
				min_num=d[j];
				t=j;
			}
		}
		
		s[t]=1;//将t放入已确定最小路径的集合s中
		
		//用t更新其他节点的值 
		for(int p=h[t];p!=-1;p=ne[p]){
			int j=e[p];
			if(d[t]+w[p]<d[j]){
				d[j]=d[t]+w[p];
			}
		} 
		
	}
	
	if(d[n]==0x3f3f3f3f)
		cout<<-1<<endl;
	else
		cout<<d[n]<<endl;
	
}

int main(){
	
	cin>>n>>m;
	
	memset(h,-1,sizeof h);
	
	while(m--){
		int x,y,z;
		cin>>x>>y>>z;
		add(x,y,z);
	}
	
	Dijkstra(); 
	
	return 0;
}

优化

我们可以很容易的看出迪杰斯特拉算法中最耗时的一步是查找当前的最小d[],这个的时间复杂度是n方
很容易想到使用堆来解决,这样每次查找最小值就是O(1)

直接使用priority_queue, 头文件是#include
**priority_queue<数据类型, vector<数据类型>, greater<数据类型>> heap; ** 这是定义的格式,记住。
greater是小顶堆,less是小顶堆
特别要注意的是!!
当数据类型是int等类型时,自然而然就排序了。
但是,当使用类型是PII等类型时候,优先排序第一个元素!!!然后再排序第二个元素!
所以本题中将distance放在PII的第一个元素!
当然也可以指定排序方式,见下面的链接

指定priority_queue排序方式


#include
#include
#include

using namespace std;

const int N=1e6+10;
int n,m;

int h[N],ne[N],e[N],w[N],idx=1;
int s[N];
int d[N];

typedef pair <int,int> PII;



 
void add(int x,int y,int z){
	e[idx]=y;
	ne[idx]=h[x];
	w[idx]=z;
	h[x]=idx;
	idx++;
}

void Dijkstra(){
	priority_queue<PII, vector<PII>, greater<PII>> heap;
	memset(d,0x3f,sizeof d);
	d[1]=0;
	heap.push({0,1});
		
	while(heap.size()){
		auto t=heap.top();
		heap.pop();
		
		int v=t.second;	
		
		if(s[v])	continue;
		
		s[v]=1;
		
		for(int p=h[v];p!=-1;p=ne[p]){
			int j=e[p];
			if(d[v]+w[p]<d[j]){
				d[j]=d[v]+w[p];
				heap.push({d[j],j}); 
			}
		} 
			
	}
	
	if(d[n]==0x3f3f3f3f)
		cout<<-1<<endl;
	else
		cout<<d[n]<<endl;
	
}

int main(){
	
	cin>>n>>m;
	
	memset(h,-1,sizeof h);
	
	while(m--){
		int x,y,z;
		cin>>x>>y>>z;
		add(x,y,z);
	}
	
	Dijkstra(); 
	
	return 0;
}

Bellman-ford算法

Bellman-ford算法虽然在写法上和迪杰斯特拉很像,但是本质完全不同
Dijkstra算法是每次找到一个最短路径确定的点,然后对该点能到达的路径进行更新,因此需要执行n次
但是Bellmax-ford算法是对边进行遍历,首先外层循环规定了两点之间最多可以走k次边,内层循环则是对所有的
边进行遍历,然后更新。
介绍一下负环,就是如下情况:
3.21 最短路_第1张图片
由于出现了2+(-3)+(-1)<0的情况,因此A->B如果想走最短路径的话就会无限重复这个环
在bellman算法中,每次进行最短路径的更新,那么如果从1到n有路径的话,那么最多n-1条路径(n个点)就可以到达。
如果循环到了n或者更大,那么说明一定存在负环了
不过一般不用bellman找负环,这里记住就可以了(一般用spfa找负环)
bellman算法一般用于有边数限制的最短路径查找

需要使用一个last数组记录之前状态,原因如下:

3.21 最短路_第2张图片

上面的图可能看起来有点抽象,我来解释一下:
首先右上角的是原路线图,如果我们不设置last,那么对于k=1的时候,意思是我们只找1条边能到达的最短路径
假如首先遍历1->2这条边,dist[2]被更新为1,然后我们扫描到2->3这条边,此时使用:
dist[3]=min(dist[3],dist[2]+w_2->3),那么dist[3]就被更新成为2,但是这里的dist[2]是使用过一次边的情况,因此这样的更新是错误的
我们需要采用下面的方法,使用一个last数组,用于记录更新前的状态,即图中绿色的(0,+无穷,+无穷)。
然后更新后面的值的时候根据last数组更新就不会出现错误了,过程为:
dist[2]=1,然后dist[3]首先会被更新成为正无穷,因为用dist[2]=+无穷更新的,然后扫描到1->3这条边的时候才会被更新为dist[3]=3
这里使用memcpy复制,memcpy(a,b,sizeof b), 将b的数据复制到a中
注意是在外层的for内部每一次进行复制

还有一件事,如何确定某一个点走不到呢
题目中给定k<500,且每条路径长度绝对值不超过10000.
那么如果能走到一定在51e6内,
为什么不用dist[n]==0x3f3f3f3f判定呢,因为0x3f3f3f3f是一个值,而存在负边。
假设节点m距离节点n的距离是-1,而节点m距离1的距离是正无穷,
实际上dist[n]会被更新为正无穷-1,在本题中就是0x3f3f3f3f-1,并不等于0x3f3f3f3f
但是由于长度有范围,所以我们认为长度在0x3f3f3f3f/2之内的数据都表示能到达 (1e9/2 ) ,因为上面估计5
1e6都能到

,代码可以写了


#include
#include
#include

using namespace std;

const int N=1e4+10;

int n,m,k;

struct node{
	int a,b,w;
} edge[N];

int dist[N];
int last[N]; 

void bellman(){
	
	dist[1]=0;
	
	for(int i=1;i<=k;i++){//可选k次边 
		
		memcpy(last,dist,sizeof dist);
		
		for(int j=1;j<=m;j++){//遍历m条边 
			int a=edge[j].a;
			int b=edge[j].b;
			int w=edge[j].w;
			dist[b]=min(dist[b],last[a]+w);
		} 
	 
	}
	
}


int main(){
	
	cin>>n>>m>>k;
	int x,y,z;
	for(int i=1;i<=m;i++){
		cin>>x>>y>>z;
		edge[i]={x,y,z};
	}
	
	memset(dist,0x3f,sizeof dist);
	
	bellman();
	
	if(dist[n]>0x3f3f3f3f/2)
		cout<<"impossible"<<endl;
		
	else
		cout<<dist[n]<<endl;; 
	
	return 0;
} 

spfa算法

spfa算法是对bellman的优化。
bellman中每次都对所有边进行遍历更新,但是这样并不高效
因为dist[b]=(dist[a]+w,dist[b]);
可以很容易发现,只有在dist[a]变化的时候dist[b]才有可能发生变化,因此我们每次都只遍历那些被更新过的点的边


#include
#include
#include
#include

using namespace std;

int n,m;

const int N=1e5+10;

int e[N],ne[N],h[N],w[N],idx=1; 
int st[N]; 
int dist[N];

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

int spfa(){
	
	memset(dist, 0x3f, sizeof dist);
	
	dist[1]=0;
	
	queue <int> q;//用于记录哪些点可以被更新 
	
	q.push(1);
	
	st[1]=1;
	
	while(q.size()){
		int t=q.front();
		q.pop();
		st[t]=0;
		
		for(int i=h[t];i!=-1;i=ne[i]){
			int j=e[i];
			if(dist[j]>dist[t]+w[i]){
				dist[j]=dist[t]+w[i];//更新
				if(st[j]==0){
					q.push(j);
					st[j]=1;
				}
			}
		}
		
	}
	
	return dist[n]; 
	
}


int main(){
	
	cin>>n>>m; 
	
	memset(h,-1,sizeof h);
	
	for(int i=1;i<=m;i++){
		int x,y,z;
		cin>>x>>y>>z;
		add(x,y,z);
	}
	
	int ans=spfa();
	
	if(ans==0x3f3f3f3f)
		cout<<"impossible"<<endl;
	else
		cout<<ans<<endl;
	
	
	
	
	return 0;
} 

SPFA求负环

判断负环,就是判断到某个点边数是否>=n,如果是,说明有负环,反之则无

此外要注意的是,之前的题目就是讲点1先放进去,但是这里是判断整个图中是否存在负环,因此负环的起始点不一定是从1开始。因此要加入如下语句:
for(int i=1;i<=n;i++) q.push(i);


#include
#include
#include
#include

using namespace std;

int n,m;

const int N=1e5+10;

int e[N],ne[N],h[N],w[N],idx=1; 
int st[N]; 
int dist[N];
int cnt[N];

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

int spfa(){
	
	memset(dist, 0x3f, sizeof dist);
	
	dist[1]=0;
	
	queue <int> q;//用于记录哪些点可以被更新 
	
	for(int i=1;i<=n;i++)
	    q.push(i);
	
	st[1]=1;
	
	while(q.size()){
		int t=q.front();
		q.pop();
		st[t]=0;
		
		for(int i=h[t];i!=-1;i=ne[i]){
			int j=e[i];
			if(dist[j]>dist[t]+w[i]){
				dist[j]=dist[t]+w[i];//更新
				cnt[j]=cnt[t]+1;
				if(cnt[j]>=n)
				    return 1;
				if(st[j]==0){
					q.push(j);
					st[j]=1;
				}
			}
		}
		
	}
	
	return 0; 
	
}


int main(){
	
	cin>>n>>m; 
	
	memset(h,-1,sizeof h);
	
	for(int i=1;i<=m;i++){
		int x,y,z;
		cin>>x>>y>>z;
		add(x,y,z);
	}
	
	int ans=spfa();
	
	if(ans==0)
		cout<<"No"<<endl;
	else
		cout<<"Yes"<<endl;
	
	
	
	
	return 0;
} 

你可能感兴趣的:(机试备考,算法,c++,数据结构)