算法百题斩其三: 单源最短路与算法——其一

算法百题斩其三: 单源最短路与算法——其一

写在前面:何所谓“斩”?
斩,即快速而有力地切断,指我们用最精简的语言,一针见血地点破算法题的核心难点。斩需三思而后行;斩需借助外力、旁征博引;斩需持之以恒、铁杵磨针!
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的关联了)。

例题
  1. 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]);
}



  1. 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;
}

  1. 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);

}
  1. 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;

}

  1. 小技巧之 建反图,建虚拟原点
    本题有多个起点,考虑建反图,这样只需要跑一遍最短路,再求个最小值即可。
    还可以构建一个虚拟节点,其到多个起点的距离为0,到其他点的距离为+无穷,跑一遍从虚拟原点开始的最短路,等效于从多个起点到终点的最短路
    在这里插入图片描述在这里插入图片描述

更多内容请见算法百题斩——四

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值