单源最短路的综合应用例题整理

1135. 新年好(活动 - AcWing

这里有五个点需要被遍历到,要求遍历完五个点总路程的最小值,那么首先的问题就是确定这五个点的遍历顺序,显然我们需要dfs一下,然后我们还需要知道任意两点之间距离的最小值,很容易想到floyd算法,但是这道题n的范围有些大,并不支持floyd算法,但我们注意到我们只用知道这6个点之间的关系就可,那么我们可以在每个点的位置做一次单源最短路,这样的话时间复杂度是支持的。然后还有一个小问题,我们是预处理出来,还是每次确定一种顺序了就分别求一次最短路。当然是预处理更优,那么问题实际就解决了,预处理以这六个点为起点的最短路,然后dfs找顺序求值,然后这道题就解决了。

至于最短路算法,这里既可以用spfa也可以用堆优化的dijkstra,但实际上数据会把spfa卡掉,那么换dijkstra即可。

#include<bits/stdc++.h>
using namespace std;
const int N=50010,M=200010;
int n,m;
int h[N],e[M],ne[M],w[M],idx;
int d[6][N],st[N],id[10];
void add(int a,int b,int c)
{
    w[idx]=c,e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
typedef pair<int,int> pii;
void spfa(int u)
{
    memset(d[u],0x3f,sizeof d[u]);
    memset(st,0,sizeof st);
    priority_queue<pii,vector<pii>,greater<pii>>q;
    d[u][id[u]]=0;
    q.push({0,id[u]});
    while(q.size())
    {
        auto t=q.top();
        q.pop();
        int dist=t.first,v=t.second;
        if(st[v]) continue;
        st[v]=1;
        for(int i=h[v];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(d[u][j]>d[u][v]+w[i])
            {
                d[u][j]=d[u][v]+w[i];
                q.push({d[u][j],j});
            }
        }
    }
}
int vis[10];
vector<int>q;
int ans=0;
void dfs(int k)
{
    if(k==5) 
    {
        int last=0,res=0;
        for(auto it:q)
        {
            res += d[last][id[it]];
            last=it;
        }
        ans=min(ans,res);
        return;
    }
    for(int i=1;i<=5;i++)
    {
        if(!vis[i]) 
        {
            vis[i]=1;
            q.push_back(i);
            dfs(k+1);
            q.pop_back();
            vis[i]=0;
        }
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    id[0]=1;
    for(int i=1;i<=5;i++) scanf("%d",&id[i]);
    memset(h,-1,sizeof h);
    for(int i=1;i<=m;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c),add(b,a,c);
    }
    for(int i=0;i<=5;i++) spfa(i);
    ans=0x3f3f3f3f;
    dfs(0);
    cout<<ans;
}

ps:这里有一点值得注意,就是d[][]的第一维对应的是车站的映射,而非车站,这个要理清楚。

340. 通信线路(340. 通信线路 - AcWing题库)

我们再进一步分析一下题意,定义一条从起点到终点得路径得长度为路径中第k+1大的边,如果边数没有这么多,那么就将这条路径的长度定义为0。求在这种情况下,从起点到终点的最小距离。

这里实际上可以视为求最大值最小的题,可以用二分来写。我们对结果进行二分,然后就要去对二分出来的结果进行验证,问题就转化成如何验证了,验证的条件找出一个性质,使得以答案为分界线,一边满足,一边不满足。这里我们可以将大于二分值的边的边权视为1,小于等于它的视为0,然后看能否找到一条路径使得大于二分值的边的数量小于等于k,如果不成立,那么很显然,二分值应该再取大一点,如果成立那么这个点就可能为答案,当然,它可以继续往小的找,是符合要求的。

然后既然这里的边权只剩0和1,那么实际上可以用双端bfs实现,这个的时间复杂度是线性的。

#include<bits/stdc++.h>
using namespace std;
const int N=1010,M=20010;
int n,m,k;
int h[N],e[M],ne[M],w[M],idx;
void add(int a,int b,int c)
{
    w[idx]=c,e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int d[N],st[N];
int check(int mid)
{
    memset(d,0x3f,sizeof d);
    memset(st,0,sizeof st);
    deque<int>q;
    q.push_back(1);
    d[1]=0;
    while(q.size())
    {
        auto t=q.front();
        q.pop_front();
        if(st[t]) continue;
        st[t]=1;
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            int x=w[i]>mid;
            if(d[j]>d[t]+x)
            {
            d[j]=d[t]+x;
            if(!x) q.push_front(j);
            else q.push_back(j);
                
            }
        }
    }
    return d[n]<=k;
}
int main()
{
    scanf("%d%d%d",&n,&m,&k);
    memset(h,-1,sizeof h);
    for(int i=1;i<=m;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c),add(b,a,c);
    }
    int l=0,r=1000001;
    while(l<r)
    {
        int mid=l+r>>1;
        if(check(mid)) r=mid;
        else l=mid+1;
    }
    if(l!=1000001)cout<<l;
    else cout<<-1;
}

342. 道路与航线(活动 - AcWing

 这里花费c有负数的情况,那么很显只能用spfa,但是这题用spfa会被卡。那么我们现在必须想其他的办法。这里有三个很关键的条件,道路是双向的;如果有航线从A到B,那么无论是通过道路还是航线都不能从B到A;道路的边权都是正的,航线的边权都是负的。

我们依次来看,很显然如果只考虑道路我们可以用任何一种最短路算法,因为道路的边权都是正的。然后如果A,B之间有一条航线,那么就相当于把A,B所在的与A,B可以互达的点分隔开,因为可以从A到B,就没有任何办法可以从B到A,有向无环,很容易想到拓扑图。我们将A,B各自所在的由无向边组成的单元视为一个点的话,那么实际上就可以通过航线得到一张拓扑图。关于拓扑图,我们可以用线性的时间复杂度完成遍历。那么这里实际上就有一个思路了,我们在每个无向边构成的独立集合内部使用堆优化的dijkstra,在各个独立集合之间使用拓扑图的遍历。

然后就要考虑如何实现,

1.首先按照输入我们应该先建立无向边
2.建好之后我们先通过dfs将每个连通块打上不同的标记。
3.然后开始建立有向边,同时将不同标记对应的集合的入度统计出来。
4.然后开始拓扑排序算法,将入度为0的集合放入队列
5.对于入度为0的集合进行dijkstra算法,在dijkstra算法中,如果访问到标记不同的两点,那么就将指向的那个点的入度减1,同时判断是否需要入队(拓扑排序的队列,这里定义一个全局变量);然后距离正常更新,如果两点标记相同,也即在一个集合当中,那么就将被更新的点放入优先队列中。
6.最后遍历判断所有的点是否都被更新成有效距离,这里要注意,因为有负权,所以即使没有被有效更新可能还是被更新了。

#include<bits/stdc++.h>
using namespace std;
const int N=25010,M=150010,inf=0x3f3f3f3f;
int n,mr,mp,s;
int h[N],e[M],ne[M],w[M],idx;
int d[N],st[N],id[N],rd[N];
int bcnt;
queue<int>q;
vector<int>block[N];
typedef pair<int,int> pii;
void add(int a,int b,int c)
{
    w[idx]=c,e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u,int cnt)
{
    id[u]=cnt;
    block[cnt].push_back(u);
    for(int i=h[u];i!=-1;i=ne[i])
    {
        int j=e[i];
        if(!id[j])
        dfs(j,cnt);
    }
    
}
void dijkstra(int u)
{
    priority_queue<pii,vector<pii>,greater<pii>>p;
    for(auto it:block[u])
        p.push({d[it],it});
    while(p.size())
    {
        auto t=p.top();
        p.pop();
        int dist=t.first,v=t.second;
        if(st[v]) continue;
        st[v]=1;
        for(int i=h[v];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(id[j]!=id[v]&&--rd[id[j]]==0) q.push(id[j]);
            if(d[j]>dist+w[i])
            {
                d[j]=dist+w[i];
                if(id[j]==id[v]) p.push({d[j],j});
            }
        }
    }
}
void topsort()
{
    memset(d,0x3f,sizeof d);
    d[s]=0;
    for(int i=1;i<=bcnt;i++)
        if(!rd[i]) 
            q.push(i);
    while(q.size())
    {
        auto t=q.front();
        q.pop();
        dijkstra(t);
    }
}
int main()
{
    scanf("%d%d%d%d",&n,&mr,&mp,&s);
    memset(h,-1,sizeof h);
    
    while(mr--)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c),add(b,a,c);
    }
    for(int i=1;i<=n;i++)
    {
        if(!id[i])
        {
            bcnt++;
            dfs(i,bcnt);//集合的下标从1开始
        }
    }
    while(mp--)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        rd[id[b]]++;//入度
        add(a,b,c);
    }
    topsort();
    int flag=1,res=0;
    for(int i=1;i<=n;i++)
    {
        if(d[i]>=inf/2) cout<<"NO PATH"<<endl;
        else cout<<d[i]<<endl;
    }
}

341. 最优贸易(341. 最优贸易 - AcWing题库)

 思路:这道题虽然是图论的问题,但是可以结合dp来考虑。我们将每个点k视为分界,计算出在k前面买的最小值dmin和在k后面卖的最大值dmax,那么dmax-dmin就是以k点为分界的获利。这里因为第一次出队时未必是最终的答案,比如最大值,有可能出现有环的情况,那么我们用spfa算法来求解。注意这里需要维护的不再是和,而是最大值与最小值。

然后还有一个问题,可能会质疑最大值出现在最小值前面的情况,但是我们并不是将第一次出队时的值就视为答案,如果是双向边,那么前面的也可以更新后面的。

实现:因为这里需要从前往后又需要从后往前找,所以势必要建立一张反向图,那么这个反向图该如何建呢?因为这里有单向边,所以我们要明确,反向图表示的是可以从i到n,还是可以从n到i,显然应该是可以从i到n,因为只要可以从i到n就可以被统计入结果,如果不可以,那么结果就不能包含这个。反向图是从n开始搜的,如果搜到i说明i可以到n,为了搜到i,肯定是建立从n到i的路径,也即将单向边反向来建。

#include<bits/stdc++.h>
using namespace std;
const int N=100010,M=2000010,inf=0x3f3f3f3f;
int hs[N],ht[N],e[M],ne[M],idx;
int dmi[N],dmx[N];
int st[N];
int n,m,p[N];
void add(int h[],int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void spfa(int h[],int d[],int flag)
{
    queue<int>q;
    if(!flag) 
    {
        //memset(d,0x3f,sizeof d);
        for(int i=1;i<=n;i++) d[i]=inf;
        q.push(1);
        d[1]=p[1];
    }
    else 
    {
        //memset(d,-0x3f,sizeof d);
        for(int i=1;i<=n;i++) d[i]=-inf;
        q.push(n);
        d[n]=p[n];
    }
    while(q.size())
    {
        auto t=q.front();
        q.pop();
        st[t]=0;
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            if (flag == 0 && d[j] > min(d[t], p[j]) || flag == 1 && d[j] < max(d[t], p[j]))
            {
                if (flag == 0) d[j] = min(d[t], p[j]);
                else d[j] = max(d[t], p[j]);
                if (!st[j])
                {
                    q.push(j);
                    st[j] = 1;
                }
            }
        }
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&p[i]);
    memset(hs,-1,sizeof hs);
    memset(ht,-1,sizeof ht);
    for(int i=1;i<=m;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(hs,a,b),add(ht,b,a);
        if(c==2) add(hs,b,a),add(ht,a,b);
    }
    spfa(hs,dmi,0);
    spfa(ht,dmx,1);
    int mx=0;
    for(int i=1;i<=n;i++) 
    {
        mx=max(mx,dmx[i]-dmi[i]);
    }
    cout<<mx;
}
  • 22
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值