算法百题斩其三: 单源最短路与算法——其一
写在前面:何所谓“斩”?
斩,即快速而有力地切断,指我们用最精简的语言,一针见血地点破算法题的核心难点。斩需三思而后行;斩需借助外力、旁征博引;斩需持之以恒、铁杵磨针!
1,dijkstra 与其堆优化
1.1 是啥?
dijktra算法是一种基于贪心思想的求正权图单源最短路的算法。其定义了一个“最小距离点集”,循环N次,每次将距离起点最小的点加入点集。
1.2 特性?
朴素dijkstra复杂度为
O
(
n
2
)
O(n^2)
O(n2),而堆优化版dijkstra复杂度为
O
(
n
l
o
g
m
)
O(nlogm)
O(nlogm).
dijkstra算法有一个很好的特性,就是其更新每个节点到起点的距离的过程可以形成一颗最短路树,具有拓扑序。正因如此,很多时候求最短路的问题可以与DP问题相联系。
另外,dijkstra的可行性基于其拓扑序。虽然dijkstra算法求路径极小值可以是加法也可以是乘法(加法、乘法均满足三角不等式),但dijkstra不能解决某个点的属性可以在加入点集后继续被更新的问题,因为无法形成最短距离树。正因如此,dijkstra不可以用来求含负权图的最短路,和其他特殊问题。
用灵魂画师的作品来展示最短路径树的形成过程~
斜线填充起点,实线代表最短路树上的边,虚线代表最短路树待定边,涂黑的点和起点代表最短路径点集中的点
第一次,用起点更新距离,并把距离起点最近的点加入点集
用第一个加入的点更新到起点距离,并再次把距离起点最近的点加入点集
用第二个加入的点更新到起点距离,并再次把距离起点最近的点加入点集
1.3 细节
https://blog.csdn.net/weixin_54648192/article/details/118862067?spm=1001.2014.3001.5501
详情请见我的另一篇博客
1.4 例题
2.Bellman-Ford 与 SPFA
2.1 是啥
B-F算法是一种基于dp的求权图单源最短路的算法,可以用于求出最多不经过K条边的最短路长度(循环K次)。在每次循环中,算法用上一次的结果,来对每个点到起点的距离进行更新(又称作松弛操作),从而避免了“串联”现象的发生。
下面我们再次有请灵魂画师登场~
2.2
Bellman-Ford 有着
O
(
n
m
)
O(nm)
O(nm)的复杂度,与spfa的最坏情况相同,而SPFA的复杂度最好情况为
O
(
m
)
O(m)
O(m)
可以求出负权图最短路。
SPFA很容易被卡,不过SPFA很好写(well,我倒觉得和dijkstra堆优化差不多),而且可以解决负权问题,还可以用于解决不具备拓扑序的dp问题(这里就涉及到最短路模型和dp的关联了)。
例题
- dijkstra可以求路线累计运算为加、乘的极(大、小)值(乘法等效于对数相加,不妨对所有边取对数,最后再取个幂)。
//求边累计方式为乘法的最长路
//要点:dijkstra可以求加、乘的极值
//细节:1.打印n位小数:printf("%.8lf",x)
//细节:2.距离数组初始化为0,因为要求极大值
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=2010,M=1e5+10;
double g[N][N],dj[N];
bool st[N];
int n,m,ps,pe;
void add(int a,int b,int len)
{
double res=(100.0-len)/100.0;
g[a][b]=g[b][a]=max(g[a][b],res);
}
void dijkstra(int u)
{
dj[u]=1;
for(int i=1;i<=n;i++)
{
int t=-1;
for(int j=1;j<=n;j++)
if(!st[j]&&(t==-1||dj[j]>dj[t]))t=j;
st[t]=true;
for(int j=1;j<=n;j++)
if(!st[j])dj[j]=max(dj[j],dj[t]*g[t][j]);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=0;i<m;i++)
{
int a,b,len;
scanf("%d%d%d",&a,&b,&len);
add(a,b,len);
}
scanf("%d%d",&ps,&pe);
dijkstra(ps);
printf("%.8lf",100.0/dj[pe]);
}
- dijkstra算法+枚举(枚举一般因为题目中的某个参数变化的情况下难以求解,但一旦该参数确定下来,就可以求解,且该参数取值范围较小。比如本题中,地位等级差距限制M这个量很烦人(没有固定上下界),但我们发现一旦将其上下界确定下来,就可以在松弛操作时加入新的判断,求出此时的解。那么最后枚举每个情况即可!)
另外,本题用到了构造虚拟原点的技巧,把探险家什么商品都没买的状态抽象为点0,从而用该点和商品点的连线表示直接花钱买的方案。
代码+分析:
//求经过点的附加属性(地位)最大绝对限不超过M时的最短路
//要点:1.枚举+二分
//要点:2.构建虚拟原点
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
const int N=110;
int g[N][N],dj[N],state[N];
bool st[N];
int n,m;
void add(int a,int b,int len)
{
g[a][b]=min(g[a][b],len);
}
void dijkstra(int u,int v)
{
memset(st,false,sizeof st);
memset(dj,0x3f,sizeof dj);
dj[0]=0;
for(int i=0;i<=n;i++)
{
int t=-1;
for(int j=0;j<=n;j++)
if(!st[j]&&(t==-1||dj[j]<dj[t]))t=j;
st[t]=true;
for(int j=0;j<=n;j++)
if(!st[j]&state[j]>=u&&state[j<=v)dj[j]=min(dj[j],dj[t]+g[t][j]);
//范围区间外的点一定不存在于当前区间下的最优路径中,所以不更新其与起点的最短距离
}
}
int main()
{
int ans=0x3f3f3f3f;
memset(g,0x3f,sizeof g);
cin>>m>>n;
for(int i=1;i<=n;i++)
{
int p,l,x;
cin>>p>>l>>x;
state[i]=l;
add(0,i,p);
for(int j=1;j<=x;j++)
{
int t,price;
cin>>t>>price;
add(t,i,price);
}
}
for(int i=1;i+m<=100;i++)
{
dijkstra(i,i+m);
ans=min(ans,dj[1]);
}
cout<<ans<<endl;
}
- dijkstra+二分
发现问题是求路径的第k+1长边的最小值,咦~最大值的最小值…
又发现问题具有典型二分的二段性(非黑即白)…
剩下该咋办不用我说了吧~
//求路径的第k+1长边的最小值
#include <cstdio>
#include <queue>
#include <algorithm>
#include <cstring>
#include <vector>
#include <cstring>
#include <functional>
using namespace std;
const int N=1e4+10;
typedef pair<int,int> PII;
int e[2*N],ne[2*N],h[N],w[2*N],dj[N],w1[2*N];
bool st[N];
int n,m,idx,k;//n点数,m边数
void add(int a,int b,int len)
{
e[idx]=b; ne[idx]=h[a]; h[a]=idx; w[idx++]=len;
}
void dijkstra()
{
memset(dj,0x3f,sizeof dj);
memset(st,false,sizeof st);///
priority_queue<PII,vector<PII>,greater<PII> > pq;
pq.push({0,1});
dj[1]=0;
while(!pq.empty())
{
auto x=pq.top();
int t2=x.first,t1=x.second;
pq.pop();
if(st[t1])continue;
st[t1]=true;
for(int i=h[t1];i!=-1;i=ne[i])
{
int j=e[i];
if(dj[j]>t2+w1[i])
{
dj[j]=t2+w1[i];
pq.push({dj[j],j});
}
}
}
}
bool check(int x)//判断从起点到终点所能经过的最少的权重大于x的边数是否不超过免费线路个数K
{
///*
memset(w1,0,sizeof w1);
for(int i=1;i<=n;i++)
{
for(int j=h[i];j!=-1;j=ne[j])
{
if(w[j]>x)w1[j]=1;
}
}
dijkstra();
//printf("%d\n",dj[n]);
if(dj[n]<=k)return true;
//*/
return false;
}
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
int main()
{
memset(h,-1,sizeof h);
scanf("%d%d%d",&n,&m,&k);
for(int i=0;i<m;i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c); add(b,a,c);
}
int res=bsearch_2(0,1e6+1);
if(res==1e6+1)printf("-1\n");
else printf("%d\n",res);
}
- SPFA+枚举
模板题不谢( ̄_, ̄ )
//SPFA加枚举求最小路径和方案
#include <iostream>//输入输出选用什么要根据输入输出规模判断
#include <cstdio>
#include <algorithm>
#include <queue>
#include <cstring>
using namespace std;
const int N=810,M=1500;
int e[2*M],ne[2*M],h[N],w[2*M],idx,cow[N];//无向图是2*M
int d[N];
bool st[N];
queue<int> q;
void add(int a,int b,int len)
{
e[idx]=b; ne[idx]=h[a]; h[a]=idx; w[idx++]=len;
}
void spfa(int u)
{
memset(d,0x3f,sizeof d);
q.push(u);
st[u]=true;
d[u]=0;/!!!!!!!!!!!!!!!!!!1
while(!q.empty())
{
int t=q.front(); q.pop();
st[t]=false;
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
if(d[j]>d[t]+w[i])
{
d[j]=d[t]+w[i];
if(!st[j])
{
st[j]=true;
q.push(j);
}
}
}
}
}
int main()
{
int xo,t,c,ans=0x3f3f3f3f,sum;
cin>>xo>>t>>c;
memset(h,-1,sizeof h);
for(int i=0;i<xo;i++)
cin>>cow[i];
for(int i=0;i<c;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c); add(b,a,c);
}
for(int i=1;i<=t;i++)
{
sum=0;
spfa(i);
for(int i=0;i<xo;i++)
{
if(d[cow[i]]==0x3f3f3f3f){
sum=0x3f3f3f;
break;
}
sum+=d[cow[i]];
}
ans=min(ans,sum);
}
cout<<ans<<endl;
}
- 小技巧之 建反图,建虚拟原点
本题有多个起点,考虑建反图,这样只需要跑一遍最短路,再求个最小值即可。
还可以构建一个虚拟节点,其到多个起点的距离为0,到其他点的距离为+无穷,跑一遍从虚拟原点开始的最短路,等效于从多个起点到终点的最短路
更多内容请见算法百题斩——四