最短路
单源最短路:
- Dijkstra
没有堆优化的是\(O(n^2)\)的。但是由于防止一些毒瘤完全图,在此一并表出。(其实好像也卡不了多少)
dijkstra的思想其实就是贪心。每次扫一遍所有点然后找当前距离最短的一个更新答案。某种程度上很像dfs。另外由于它的贪心性质,它不能判负权边。
void dijkstra(int st){
memset(dis,0x3f,sizeof(dis));//距离
memset(v,false,sizeof(v));//是否扫过
int x=st;
while(!v[x]){
v[x]=true;int minn=2147483647;
for(int i=head[x];i;i=edge[i].next){
if(!v[edge[i].v])dis[edge[i].v]=min(dis[edge[i].v],dis[x]+edge[i].w);
}
for(int i=1;i<=n;i++){
if(dis[i]<minn&&!v[i])minn=dis[i],x=i;
}
}
}
然后我们发现这个扫所有点的过程可以拿个堆扔进去然后就看起来快了不少。于是我们就有了如下的优化。
struct stu{
int x,w;
bool operator>(const stu &s)const{
return w>s.w;
}
}g;//按距离排序的堆
priority_queue<stu,vector<stu>,greater<stu> >q;
void dijkstra(int st){
memset(dis,0x3f,sizeof(dis));
memset(v,false,sizeof(v));
g.x=st;g.w=0;q.push(g);
dis[st]=0;
while(!q.empty()){
int x=q.top().x;q.pop();
if(!v[x]){
v[x]=true;
for(int i=head[x];i;i=edge[i].next){
if(dis[edge[i].v]>dis[x]+edge[i].w){
dis[edge[i].v]=dis[x]+edge[i].w;
g.x=edge[i].v;g.w=dis[edge[i].v];q.push(g);
pre[edge[i].v]=x;//如果你要记录路径的话
}
}
}
}
}
其实就是把刚才的东西放到堆里面。
2.spfa
首先这个是bellman-ford(大概是每次扫所有边然后能更新就更新答案的\(O(nm)\)算法)的一个队列优化。随机图很快,但是一般都会被卡。
大体是采用bfs的思想。开一个队列,每次扫到不在队列里的点时加入队列,然后更新答案。所以它可以跑带负边权的图。
bool spfa(int st){
memset(v,false,sizeof(v));
memset(cnt,0,sizeof(cnt));//spfa可以跑负权边所以可以用它判负环 这是每个点的访问次数
memset(dis,0x3f,sizeof(dis));
dis[st]=0;v[st]=true;q.push(st);
while(!q.empty()){
int x=q.front();q.pop();v[x]=false;
for(int i=head[x];i;i=edge[i].next){
if(dis[edge[i].v>dis[x]+edge[i].w]){
dis[edge[i].v]=dis[x]+edge[i].w;
if(!v[edge[i].v]){
cnt[edge[i].v]++;//
if(cnt[edge[i].v]>n)return true;//如果你想判负环的话就看入队超过n次的就有负环了
v[edge[i].v]=true;q.push(edge[i].v);
}
}
}
}
return false;
}
几个spfa优化(及卡法):
1.普通的spfa:一个链套菊花的图就可以把它卡死。或者一个网格图,要行边权小列边权大,列边数少行边数多的。
2.LLL优化:每次出队的时候判断当前点答案与队内所有答案的平均值,如果当前点大则重新插入队尾。(玄学优化,我能过的一个spfa加上这个tle60分)
卡法:向起点连一条权值巨大的边。结束。
3.SLF优化:每次入队的时候如果当前距离比队首元素小就进对头,否则队尾。(这个东西差分约束的时候还蛮有用的)
卡法:还是链套菊花,链的边权整的比较小,就可以让它多次进去。
4.mcfx优化:每个点第\([L,R]\)次进队的时候队首,否则队尾。
卡法:还是链套菊花。
5.Dfs优化:把队列改成栈,这样就是个长的像dfs的东西了。(玄学优化,慎用)
卡法:本质上这个东西的复杂度是假的。既然dfs了那随便卡吧。
全源最短路:
- Floyd
这是个\(O(n^3)\)的东西,而且实现极其的暴力。
void floyd(){
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
a[i][j]=min(a[i][j],a[i][k]+a[k][j]);//这个a是邻接矩阵
}
}
}
}
注意k必须放第一维。
两个应用:
1.最小环:
int floyed(){
memset(a,0x1f,sizeof(a));
int ans=2147483647;
for(int k=1;k<=n;k++){
for(int i=1;i<k;i++){
for(int j=i+1;j<k;j++){
ans=min(ans,dis[i][j]+a[j][k]+a[k][i]);
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i!=j&&i!=k&&j!=k){
if(dis[i][j]>dis[i][k]+dis[j][k]){
dis[i][j]=dis[i][k]+dis[j][k];
pre[i][j]=pre[k][j];
}
}
}
}
}
return ans;
}
2.传递闭包(就是判两个点是否联通):
bitset<100010>f[100010];
for(int k=1;k<=n;k++)
for (i=1;i<=n;i++)
if(f[i][k])f[i]=f[i]|f[k];
- Johnson
这个东西是\(O(nm\log m)\)的。具体地,我们首先用一个能判负环的最短路(比如spfa)从新建的虚点跑一下负环,然后重新对每条边赋值,使其变成正权能跑dijkstra。
如何重新赋值?我们从0点跑spfa之后,将每条边\((u,v)\)的边权变为\(edge[i].w+dis[u]-dis[v]\),最后再变回来就行了。
bool spfa(int st){
queue<int>q;
memset(h,0x3f,sizeof(h));
h[st]=0;q.push(st);cnt[0]=1;
while(!q.empty()){
int x=q.front();q.pop();
v[x]=false;
for(int i=head[x];i;i=edge[i].next){
if(h[edge[i].v]>h[x]+edge[i].w){
h[edge[i].v]=h[x]+edge[i].w;
cnt[edge[i].v]=cnt[x]+1;
if(cnt[edge[i].v]>n+1)return false;//注意多了一个0点所以是n+1
if(!v[edge[i].v]){
v[edge[i].v]=true;q.push(edge[i].v);
}
}
}
}
return true;
}//正常的spfa判负环
struct stu{
int x,w;
bool operator<(const stu &s)const{
return w>s.w;
}
}g;
void dijkstra(int st){
priority_queue<stu>q;
for(int i=1;i<=n;i++)dis[i]=1e9;
dis[st]=0;
memset(v,false,sizeof(v));
g.x=st;g.w=0;q.push(g);
while(!q.empty()){
int x=q.top().x;q.pop();
if(!v[x]){
v[x]=true;
for(int i=head[x];i;i=edge[i].next){
if(dis[edge[i].v]>dis[x]+edge[i].w){
dis[edge[i].v]=dis[x]+edge[i].w;
if(!v[edge[i].v]){
g.x=edge[i].v;g.w=dis[edge[i].v];
q.push(g);
}
}
}
}
}
}//普通的dijkstra
signed main(){
scanf("%lld%lld",&n,&m);
for(int i=1;i<=m;i++){
int u,v,w;scanf("%lld%lld%lld",&u,&v,&w);
add(u,v,w);
}
for(int i=1;i<=n;i++)add(0,i,0);
if(!spfa(0)){
printf("-1");return 0;
}
for(int i=1;i<=n;i++){
for(int j=head[i];j;j=edge[j].next){
edge[j].w+=h[i]-h[edge[j].v];//转换边权为正
}
}
for(int i=1;i<=n;i++){
dijkstra(i);
long long ans=0;
for(int j=1;j<=n;j++){
if(dis[j]==1e9)ans+=j*1e9;
else ans+=j*(dis[j]+h[j]-h[i]);//转回来统计答案
}
printf("%lld\n",ans);
}
return 0;
}
其他技巧:
- 分层图最短路
这个的一般模型是看到有 \(k\) 次免费走的机会跑最短路的。大板子是这个题。标志一般是 \(k\) 很小。
遇到这种我们可以建 \(k\) 个图,每个图内部就是原图,然后图之间在原来有边的节点间连上特定边权的边(比如这个题就是边权为 \(0\) 的边),然后跑最短路就行了。
int main(){
scanf("%d%d%d",&n,&m,&k);
if(k>m)k=m;
for(int i=1;i<=m;i++){
int u,v,w;scanf("%d%d%d",&u,&v,&w);
add(u,v,w);add(v,u,w);//原图连边
for(int i=1;i<=k;i++){
add(u+i*n,v+i*n,w);
add(v+i*n,u+i*n,w);//图内部连边
add(u+(i-1)*n,v+i*n,0);//图之间的0权边
add(v+(i-1)*n,u+i*n,0);
}
}
dijkstra(1);
printf("%d",dis[n+k*n]);
}
另一个题是这个
我们看到 \(k\) 很小,所以我们建 \(2k\) 张图,并规定分层图之间向上层走为购买汉堡,下层走为购买可乐,图之间的边可以根据该点连到的点是汉堡还是可乐决定。由于图只有 \(2k\)层我们可以保证方案合法。然后根据初始位置决定从哪一层图开始跑最短路。