一、最短路
1.基础知识
a.Dijkstra算法:基于贪心。具体算法见蓝书P350。但是我个人更习惯从优先队列的bfs角度来理解。所以Dijkstra算法具有两个性质:1.每个点可能被更新多次,但是只能被取出扩展一次。2.当某个点第一次出队时,就已经找到了起点到它的最短路径。
b.Bellman-Ford算法与SPFA算法:Bellman-Ford算法基于迭代思想,而SPFA算法是在Bellman-Ford算法的基础上加入队列优化,可认为算法思想基于bfs。具体可见蓝书P353。
c.Floyd算法:基于动态规划,有方程F(k,i,j)=min(F(k-1,i,j),F(k-1,i,k)+F(k-1,k,j)) (F(k,i,j)表示只经过前k个点中的若干个点,i与j之间的最短路径)。这里着重强调一下简化的方程F(i,j)=min(F(i,j),F(i,k)+F(k,j))。这里的关键在于弄明白为什么F(i,k)与F(k,j)仍处于第k-1层的状态。F(i,k)若被更新,显然有F(i,k)=min(F(i,k),F(i,k)+F(k,k)),其中F(k,k)=0,相当于没有更新,仍处于第k-1层。所以这个方程的正确性得到了严谨的证明。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=210;
int d[N][N],INF=1e9;
int m,n,Q;
void floyd(){
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
}
}
}
}
int main(){
cin>>n>>m>>Q;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i==j) d[i][j]=0;
//注意这类涉及两点之间距离的存储需要把d[i][i]置为0
else d[i][j]=INF;
}
}
while(m--){
int a,b,c;
cin>>a>>b>>c;
d[a][b]=min(d[a][b],c);
}
floyd();
while(Q--){
int a,b;
cin>>a>>b;
if(d[a][b]>INF/2) cout<<"impossible"<<endl;
//别忘了玄学判断
else cout<<d[a][b]<<endl;
}
}
2.例题
例题1:AcWing 340.通信线路(堆优化的Dijkstra算法)
这题看到“使得第K+1贵的电缆最便宜”一类的字样,就要考虑二分答案L。我们又发现:答案具有明显的单调性:当二分的值L增大时,最终其排名K就要减小;反之亦然。所以这题采用二分答案,并每次将图转化为无权图(0/1边权图)处理即可。即每次将大于L的边权置为1,其它置为0,求新图的最短路,dist[n]就是边权L的排名,根据排名来确定二分的取值变化。
代码如下:
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N=1005,M=20*N;
int n,m,k;
int head[N],ne[M],to[M],w[M],dist[N],st[N],idx;
void add(int x,int y,int z){
ne[++idx]=head[x];
head[x]=idx;
to[idx]=y;
w[idx]=z;
}
bool spfa(int mid){
queue<int> q;
q.push(1);
memset(dist,0x3f,sizeof dist);
dist[1]=0;
st[1]=true;
while(q.size()){
int t=q.front();
q.pop();
st[t]=false;
for(int i=head[t];i;i=ne[i]){
int j=to[i],s;
if(w[i]>mid) s=dist[t]+1;
else s=dist[t];
if(s<dist[j]){
dist[j]=s;
if(!st[j]){
q.push(j);
st[j]=true;
}
}
}
}
if(dist[n]<=k) return true;
return false;
}
int main(){
cin>>n>>m>>k;
while(m--){
int a,b,w;
cin>>a>>b>>w;
add(a,b,w);
add(b,a,w);
}
int l=0,r=1000000;
while(l<r){
int mid=(l+r)>>1;
if(spfa(mid)){
r=mid;
}
else{
l=mid+1;
}
}
if(r==1000000) cout<<-1<<endl;
else cout<<r<<endl;
return 0;
}
例题2:AcWing 341.最优贸易(SPFA算法)
这题题意一句话概括:求一条1~n的路径,使得该路径上的节点权值的最大值与最小值之差最大(且最大权值节点出现在最小权值节点之后,若路径存在环,也要满足路径序列上存在这样的两个点)。考虑到括号内的条件,想到用在原图上以1为起点,对所有点求一个dmin,即以该点为终点的,且路径上的节点权值的最小值最小。再在反图上以n为起点,对所有点求一个dmax,即以改点为终点的,且路径上的节点权值的最大值最大。那么最终求答案时,求最大的dmax[x]-dmin[x]即可。求法与求最短路相近,但是不能用Dijkstra算法。因为Dijkstra算法是在节点第一次出队时就取得最优值,而这题显然不满足这个条件,因为路径可能存在环,所以环上完全可能存在更小权值的节点去更新答案。所以这题显然应当采用SPFA算法。代码如下:
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N=1e5+5,M=2e6+5;
int h[N],rh[N],ne[M],to[M],idx;
int dmax[N],dmin[N],p[N];
bool st[N];
queue<int> q;
void add(int h[],int x,int y){
ne[++idx]=h[x];
to[idx]=y;
h[x]=idx;
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>p[i];
while(m--){
int x,y,z;
cin>>x>>y>>z;
if(z==1){
add(h,x,y);add(rh,y,x);
}
else{
add(h,x,y);add(h,y,x);
add(rh,y,x);add(rh,x,y);
}
}
memset(dmin,0x3f,sizeof dmin);
q.push(1);
dmin[1]=p[1];
while(q.size()){