最短路 差分约束系统 学习笔记

关于最短路,就学了几种基础算法,分别是:最简单的bfs求路径、dijk、bellman、floyed、以及基于bellman的SPFA。

其实单纯最短路算法在比赛中几乎很少用到(比如像acm这样的正式比赛,它们在我看来只是提供算法基本思路的形象例子之一)


bfs最为简单不说了。


dijk基于贪心思想,解决的是单源最短路问题。其实现步骤大致如下:找出一个距离起点最近的未用过的点(这个距离本身就可以认为是起点到这个点最短的距离了),然后通过这个点不断更新这个点能够达到的点。所有点都用过之后,d[s][t]就是我们要的答案了。

凭什么这样做能得出最优解?

证明过程如下:(其实不是很严谨凑合看吧)

假设S为已经确定最短距离的点集,k为点集之外的一点,我们假设用点集S之外的a点更新k点,那么a点的距离一定小于k点,那么当a点加入点集S时,还是会更新k点。也就是说,对于k点的入度都会先一步加入点集,然后可以更新k点,这样我们就取得了k点的最短距离。

正常人都会给dijk加个堆优化一下,n2的代码不贴了,就贴个nlogn的

#include<bits/stdc++.h>
#define inf 1e9
using namespace std;
struct node
{
	int to,val;
};
struct nod
{
	int d,dian;
	friend bool operator < (const nod x,const nod y)
	{
		return x.d>y.d;
	}
};
int n,m,s;
int d[200005];
vector<node> a[200005];//这是存的图 
void dijk()
{
	fill(d,d+200005,inf); 
	d[s]=0;
	int use[200005]={0};
	priority_queue<nod> p;
	nod t={0,s};
	p.push(t);
	while(p.size())
	{
		nod t=p.top();
		p.pop();
		if(use[t.dian] || d[t.dian]<t.d) continue;
		use[t.dian]=1;
		int x=t.dian;
		int k=a[x].size();
		for(int i=0;i<k;i++)
		{
			if(d[a[x][i].to]>d[x]+a[x][i].val)
			{
				d[a[x][i].to]=d[x]+a[x][i].val;
				nod tt={d[a[x][i].to],a[x][i].to};
				p.push(tt);
			}
		}
	}
}
int main()
{
	cin>>n>>m;
	cin>>s;
	for(int i=1;i<=m;i++)
	{
		int x,y,z;
		cin>>x>>y>>z;
		//假设是有向图
		node t={y,z};
	   a[x].push_back(t);
	}
	dijk();
	for(int i=1;i<=n;i++)
	if(i!=n) cout<<d[i]<<" ";
	else cout<<d[i]; 
} 

 

差分约束系统

三角形不等式角形还有一个应用,就是求解差分约束系统(差分约束系统就是一堆一次不等式的组合),利用三角不等式的d[v]<=d[u]+w[u][v],刚好满足约束条件,

最终转化为最短路问题,由于一般都带有负边,还要讨论负环有无解的问题,一般都会用SPFA来做。

那怎么确保每一个约束条件都有贡献?(也就是走过每一条边)

reason:当所有边都为正时,即使d数组全为0也一样满足条件;

当存在负边时,根据最短路的原则一定会利用负边进行松弛,这样这条边一定会被用上,所以约束条件会得到满足,不单单只是为了连通性(这一点很多博客都没有提到)


bellman 其实就是暴力求解,考虑了每一个点的前导边是否能优化最短路,还有就是由于最短路上的点有V个,那么路径边一定是V-1条所以我们执行

n-1次的“大松弛”就一定能得到最优解。我们可以在再也无法松弛的情况下提前退出循环。代码过于简单就不贴了,下面会贴一下优化后的bellman(即SPFA)

 

SPFA的思想是更新某个点他的前导点必定要松弛过,我们只把松弛成功的节点加入到队列中(在队列中不能重复存在),注意如果一个点入队超过V-1次的

话就会存在负圈,按照所说的均摊复杂度是O(kn)其中k是一个相对较小的常数。另外,实践证明,当跑的图是一个菊花图(稠密图)时k会变得很大

这时候复杂度会大幅上升,总而言之起不到优化效果了,这是血淋淋的教训!!!

贴一波SPFA代码。

#include<bits/stdc++.h>
#define inf 1e9
using namespace std;
struct node
{
	int to,val;
};  
int n,m,s;
vector<node> a[200005];
long long d[200005];
void spfa()
{
	fill(d,d+200005,inf);
	queue<int> p;
	d[s]=0;
	p.push(s);
	int use[200005]={0};
	use[s]=1;
	while(p.size())
	{
	   int dian=p.front();
	   p.pop();
	   use[dian]=0;
	   int t=a[dian].size();
	   for(int i=0;i<t;i++)
	   {
	   	 if(d[dian]+a[dian][i].val<d[a[dian][i].to])
	   	{
	   		d[a[dian][i].to]=d[dian]+a[dian][i].val;
			if(use[a[dian][i].to]==0) 
			{
				use[a[dian][i].to]=1;
				p.push(a[dian][i].to);	
		    } 	
		}
	   }
	}
}
int main()
{
	freopen("in1.txt","r",stdin);
	std::ios::sync_with_stdio(false);
   	cin>>n>>m>>s;
   for(int i=1;i<=m;i++)
   {
   	int x,y,z;
   	cin>>x>>y>>z;
   	node t={y,z};
   	a[x].push_back(t);
   }
   	spfa();
   	for(int i=1;i<=n;i++)
   	if(i!=n) cout<<d[i]<<" ";
   	else cout<<d[i];
} 

我这个可以说是朴素版的spfa,太辣鸡了,洛谷p4779最短路标准版数据都能卡到32分,天哪!!!spfa成废物了吗???

 

 

关于负边和负环的讨论:

如果存在负边的话是可以得出最优解的,但是dijkstra不行(结合证明过程可知),bellman可以用这时候但是很慢,求带负边单源的最好用spfa,

那负边全源呢?全源可以用Floyd,但是太慢了,Johnson可以用。

这个算法的复杂度可以达到o(nmlogm),比起floyd的n^3好上不少。

而对于负环,理论上就不存在最优解别说设计算法了。

Johnson的思路就是弄一张新图把负边在不影响求解的情况下换成正边。

在讨论这个问题之前,我们先讨论一个物理概念——势能。

诸如重力势能,电势能这样的势能都有一个特点,势能的变化量只和起点和终点的相对位置有关,而与起点到终点所走的路径无关。

势能还有一个特点,势能的绝对值往往取决于设置的零势能点,但无论将零势能点设置在哪里,两点间势能的差值是一定的。

(这里有一个错误思路,就是对于所有边先加上x是所有边的权值都为正,然而这显然是错误的)

证明过程:

这里的h[k]就是建立的超级源点到各点的值,加入各边的权值均为正,那么h数组的贡献为零

操作步骤是:建立一个新点0号点,然后跑一边spfa求好h[i],然后改边的权值,用新图跑n遍dijkstra。(记得吧最后的结果要减去hs-ht!!!)

 

 

水平一般,本蒟蒻欢迎各位大佬神仙来dis

持续更新中。。。

 

 

 

 

 

 

 

 

 

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值