单源最短路的综合应用

本文介绍了最短路算法在求新年好、通信线路和道路与航线等实际问题中的应用,分别结合DFS、二分查找和拓扑序进行解题。对于新年好问题,通过6次最短路计算和DFS枚举顺序求解;通信线路问题中,采用二分查找确定最小最大边权值;道路与航线问题中,利用拓扑序处理负权边。
摘要由CSDN通过智能技术生成

1.最短路+DFS

1135. 新年好 - AcWing题库

思路:

 题目让我们求起点1出发,途径2,3,4,5,6号点的最短路径。

由于我们无法在求单源最短路的过程中确定我们的最短路顺序,否则就无法保证最短路是最短的!

根据数据范围,我们可以依次以1,2,3,4,5,6为起点,跑一遍最短路,然后用dfs枚举23456的顺序,求最短路。

时间复杂度:O(6mlog(n))

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>

using namespace std;

typedef pair<int, int> PII;

const int N = 50010, M = 200010;

int h[N], e[M], w[M], ne[M], idx;
int dist[10][N];
bool st[N], vis[10];
int n, m, res = 0x3f3f3f3f;
int pos[10];//亲戚在车站的位置

void add(int a, int b, int c)  
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

void dijkstra(int id, int u)//dist数组中的下标u,亲戚或者我的id
{
    memset(st, false, sizeof st);
    priority_queue<PII, vector<PII>, greater<PII> > q;
    
    q.push({0, u});
    dist[id][u] = 0;
    
    while(q.size())
    {
        auto t = q.top();   q.pop();
        int ver = t.second, distance = t.first;
        if(st[ver]) continue;
        st[ver] = true;
        
        for(int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i];
            if(dist[id][j] > distance + w[i])
            {
                dist[id][j] = distance + w[i];
                q.push({dist[id][j], j});
            }
        }
    }
}

void dfs(int pre, int u, int cost)//起点是pre,已经遍历了u个人,已经花费了cost
{
    if(u >= 6)
    {
        // cout << "cost: " << cost << endl;
        res = min(res, cost);
        return ;
    }
    
    // if(cost >= res) return ;//最优性剪枝   
    
    for(int i = 2; i <= 6; i ++ )
    {
        if(!vis[i])
        {
            // cout << dist[pre][pos[i]] << ' ';
            vis[i] = true;
            dfs(i, u + 1, cost + dist[pre][pos[i]]);
            vis[i] = false;
        }
    }
}

int main()
{
    memset(h, -1, sizeof h);
    
    cin >> n >> m;
    pos[1] = 1;//佳佳的位置
    for(int i = 2; i <= 6; i ++ )   cin >> pos[i];
    while(m -- )
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
        add(b, a, c);
    }
    memset(dist, 0x3f, sizeof dist);
    for(int i = 1; i <= 6; i ++ )   dijkstra(i, pos[i]);
    
    // for(int i = 1; i <= 6; i ++ )
    // {
    //     for(int j = 1; j <= n; j ++ )   cout << dist[i][j] << ' ';
    //     cout << endl;
    // }
    
    vis[1] = true;
    dfs(1, 1, 0);
    
    cout << res << endl;
    
    return 0;
}


2.最短路+二分

340. 通信线路 - AcWing题库

 首先本题无法用最短路直接解决,一个错误且直接的想法是求出最短路的路径,然后排序,求倒数第k+1条边的边权,就是最小花费,但这是错误的想法。例如:

上图的1->4的最短路显然是 1->2->3->4,如果k=1,那么去掉最大的一条边5,剩下的最大的边是5。但如果我们走1->3->4,去掉一条最大的边15,剩下的最大的边是3,。虽然134的顺序不是最短路,但在去掉k条边之后,最大的边权是最小的。

如果我们比较敏感的话,可以发现本题让我们求的是,在求得的所有可达路径中(不一定是最短路径)去掉k条边之后的最大边权值中的最小值!

简直是赤裸裸的提示我们二分啊! 我们可以二分这个最小边权值bound,所有边权组成的区间划分成两个区间,一部分满足要求(最大边权为bound),一部分不满足二分的要求。并且最后的答案一定是最小边权(反证法)。

那么如何判断我们选取的二分值是否可行?我们可以把所有的边分成两类:一类是大于我们的二分值的,一类是小于我们的二分值的。根据我们的二分值bound表示的是该路径中最大边权为bound,所以在我们的路径中去掉k条边之后不能存在边权大于>bound的边,如果存在这样得边就不合法,或者说边权>bound的边的数量不能大于K

————————————重点——————————————————————————

我们可以发现,我们要找的并不是实际意义上边权之和最小的最短路,而是走最多的边权小于等于bound的边或者说走最少的边权大于bound的边,这就与上面我们将所有的边按边权划分成两类不谋而合了。

由于边权的意义仅仅在于与bound的大小关系,而不是具体是多少,100000or-10000都没有影响。所以我们可以把所有的边权抽象成两类:0(边权<=bound)和1(边权>bound),这样在我们求最短路的过程中就可以实现找到最少的边权大于bound的边。这时的dist[n]就表示边权>bound的边的数量,此时我们就可以根据dist的值与K的关系判断该二分边界bound是否可行了。

二分边界说明:花费的值是可能取到0和1e6的

  • 0可以表示路径数小于K的情况,我们把所有路径全免费了,花费当然是0,1e6显然就是花费最大的情况。
  • 由于不存在路径时花费也会二分到最大值,为了区分不存在路径和存在路径时的花费最大(1e6)两种情况,要设置一个>1e6的数表示不存在路径的情况
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#include <deque>

using namespace std;

typedef pair<int, int> PII;

const int N = 1010, M = 20010;

int n, m, k;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
bool st[N];

void add(int a, int b, int c)  
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

bool check(int bound)//O(n+m)
{
    memset(st, false, sizeof st);
    memset(dist, 0x3f, sizeof dist);
    
    deque<int> q;
    q.push_back(1);
    dist[1] = 0;
    
    while(q.size())
    {
        auto t = q.front();
        q.pop_front();
        
        if(st[t])   continue;
        st[t] = true;
        
        for(int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i], v = w[i] > bound;
            if(dist[j] > dist[t] + v)
            {
                dist[j] = dist[t] + v;
                if(!v)  q.push_front(j);
                else    q.push_back(j);
            }
        }
    }
    return dist[n] <= k;
}

int main()
{
    memset(h, -1, sizeof h);
    
    cin >> n >> m >> k;
    while (m -- )
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
        add(b, a, c);
    }
    

    int l = 0, r = 1e6 + 1;
    while(l < r)
    {
        int mid = l + r >> 1;
        if(check(mid))  r = mid;
        else    l = mid + 1;
    }
    
    if(l == 1e6 + 1)    l = -1;
    cout << l << endl;
    
    return 0;
}


3.最短路+拓扑序

342. 道路与航线 - AcWing题库

 参考:道路与航线

 负权边 + spfa必被卡 = 懵逼???

————————————————————————————————————————

在本题中,题目有一个重要信息:如果有一条航线可以从 Ai 到 Bi,那么保证不可能通过一些道路和航线从 Bi 回到 Ai。这句话的意思就是告诉我们,由航线构成的图是一个有向无环图DAG,即拓扑图

我们可以把图上的所有点看做K个连通块,每两个连通块之间都通过m条航线相连,那么每个连通块就是拓扑图上的一个点。

然后按照拓扑顺序,依次对拓扑图上的每个点做Dij,这样就避免了在Dij中处理负权回路。(这里的避免指的是,我们不会把由航线连过去的点加入Dij的堆当中,实际上Dij仍然会使用负权更新边,但只要我们不把连过去的点加入堆,该点就不会提前出堆并标记为true,也就不会造成Dij(曲线救国)的错误情况)

实际上Dij之所以不能处理负权是因为它假定的是在当前距离的基础上加上任意一条边的权重,总的权重是会变大的,但我们知道正数加负数肯定会变小,所以这不符合Dij的假定。也就不能用Dij处理,因为Dij的“目光短浅”会导致该点提前出队(还没有被负权更新),而出队以后该点可能会被负权更新,但又因为该点被更新之后又不能再更新与它相连的点(if(st[ver])  continue),所以说Dij处理不了负权的原因本质上在于一个点只能出队一次,这也是Dij保证时间复杂度的根本。而Bellman_Ford算法虽然可以多次出队,处理负权,但它的时间复杂度是O(nm)的。

回到上题,我们总体上就是对每一个块(联通块)做Dij,对块与块之间做拓扑序。

当我们对一个块做Dij的时候,我们用到的一定是这个块内的点,这样就不会导致由负边连过去的其他块的点被更新出队。并且当这个块做完之后,块内的点也就不会再用到了。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>

using namespace std;

typedef pair<int, int> PII;

const int N = 25010, M = 3 * 50010;//注意边要开三倍而不是两倍!
const int INF = 0x3f3f3f3f;

int n, m1, m2, S;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
bool st[N];

vector<int> block[N];//连通块集合
int bcnt;//连通块个数
int id[N];//每个点所在的连通块

//拓扑队列,要定义成全局变量,因为在走最短路的过程中,我们是会走航线的
//而航线就是拓扑图中的边,所以我们可能在走最短路过程中将某个连通块加入队列
int q[N], hh = 0, tt = -1;
int in[N];//入度

void dfs(int u)//查找整个联通块
{
    id[u] = bcnt;
    block[bcnt].push_back(u);
    for(int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if(!id[j])  dfs(j);
    }
}

void add(int a, int b, int c)  
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

void dijkstra(int block_id)
{
    priority_queue<PII, vector<PII>, greater<PII> > heap;
    for(auto &u : block[block_id])//将连通块中的所有点加入集合
        heap.push({dist[u], u});
    
    while(heap.size())
    {
        auto t = heap.top();
        heap.pop();
        int ver = t.second, distance = t.first;
        
        if(st[ver]) continue;
        st[ver] = true;
        
        for(int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i];
            if(dist[j] > distance + w[i])
            {
                dist[j] = distance + w[i];
                if(id[j] == block_id)//只有在一个块内才能入堆
                    heap.push({dist[j], j});
            }
            //如果是航线连过去的其他连通块的点,入度-1,判断入度是否为0
            //注意是连通块的入度而不是点的入度,加入队列是连通块也不是点
            if(id[j] != block_id && --in[id[j]] == 0)   q[++ tt] = id[j];
        }
    }
}

void topSort()
{
    memset(dist, 0x3f, sizeof dist);
    dist[S] = 0;
    
    for(int i = 1; i <= bcnt; i ++ )
        if(!in[i])
            q[++ tt] = i;
    
    while(hh <= tt)//依次对拓扑序中的块做Dij
    {
        int t = q[hh ++ ];
        dijkstra(t);
    }
    
}

int main()
{
    memset(h, -1, sizeof h);
    
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    
    cin >> n >> m1 >> m2 >> S;
    while(m1 -- )//加入双向正权边道路
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
        add(b, a, c);
    }
    
    //找到所有连通块,注意这一步必须在加入航线之前
    for(int i = 1; i <= n; i ++ )
        if(!id[i])  
        {
            bcnt ++;//新的连通块
            dfs(i);
        }
    
    // cout << "id:";
    // for(int i = 1; i <= n; i ++ )   cout << id[i] << ' ';
    // cout << endl;
    
    while(m2 -- )//加入单向可能负权边航线
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
        in[id[b]] ++ ;//连通块入度+1,不是点的入度+1
    }
    
    //在进行拓扑排序的过程中走最短路
    topSort();
    
    for(int i = 1; i <= n; i ++ )
    {
        //注意,由于前面关闭了同步流,所以不要使用puts()
        //由于负权边的存在,一个点尽管不能到达,也dist也会被更新
        if(dist[i] > INF / 2)   cout << "NO PATH" << endl;
        else    cout << dist[i] << endl;
    }
    
    return 0;
}


4.最短路+DP

341. 最优贸易 - AcWing题库

详解:最优贸易

思路: 

首先要了解到本题要求得是两个值:max,min

DP

状态表示:max[], min[]

  • 集合:max[i]:从1到i的最小值,包括i。min[i]:从i到n的最大值,包括i

  • 属性:max / min

状态分析:将所有点划分为n个区间(一共n个点),每个点作为一个集合的划分

  • max[i] = max(max[i - 1], w[i])
  • min[i] = min(min(i + 1), w[i])

但我们知道DP是要满足拓扑序的,由于本题可能存在环,所以不满足拓扑序,也就不能用了DP。虽然DP不能使用,但这题本质上还是DP的思想。

我们考虑使用最短路解决DP的状态转移。注意无法使用Dij。

几个注意的点:

1.求n~i的最大值时不能正向求,必须反向求?

  • 因为可能i~n不一定可达,我们被不可达的点更新了
  • 详细解释

2.为什么不能用Dij?

  • 因为dist[i]可能被多次更新,但是每个点又只能出队一次。具体来说,就是存在环可以使dist[x]多次更新为最小值。
  • 例如:给定2个点:1和2,价格分别是2和1,一共有两条边:1->2,和2->1,那么最初优先队列中只有一个点1,此时1被弹出,它的最小价格是2,但2并不是最终的最小值。所以dijkstra算法是不适用的。
  • 还是上面那题同样的思路,Dij成立的条件是:我们假定遍历后面的边,一定会使当前的边的“权值”变大,但这里的权值是“最小值或者最大值”,当前状态的“权值”是可能被后面的边重复更新的。而上面的例子,对于常规的堆优化Dij,它假定走完1之后再走2,1的“权值”一定会变大,但其实不是这样的。
  • 最一般的最短路维护的是路径上的sum性质,本题维护的是max和min性质,sum性质具有累加性(就是要从前面的值基础上累加,后续出现只会越来越大,所以第一次出现的就是最短),而max 和min对于新出现的数,单独比较即可,所以不能用dijkstra(dijkstra就是利用的sum的累加性)
  • 要使用dijkstra的前提条件是明确知道起点一定是最短的,因为基于贪心的策略,每次都是使用距离最短的点去更新其他的点,如果不能保证起点是最短的就不能用

3.为什么是求1~i的最小值,i~n的最大值,而不是1~i的最大值,i~n的最小值

因为买肯定在卖前面,而我们希望买的价格最小,卖的价格最大,所以说小的在前。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 100010, M = 1000010;

int n, m;
int h[N], rh[N], e[M], ne[M], idx;
int dist[N], w[N];
int dmax[N], dmin[N];
bool st[N];

void add(int h[], int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

void spfa(int h[], int dist[], int op)
{
    queue<int> q;
    if(op)//max,反向
    {
        memset(dist, -0x3f, sizeof dmax);
        dist[n] = w[n];
        q.push(n);
    }
    else//min,正向
    {
        memset(dist, 0x3f, sizeof dmin);
        dist[1] = w[1];
        q.push(1);
    }
    
    while(q.size())
    {
        int t = q.front();  q.pop();
        st[t] = false;
        for(int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if(op && dist[j] < max(dist[t], w[j]) || !op && dist[j] > min(dist[t], w[j]))//状态转移
            {
                if(op)  dist[j] = max(dist[t], w[j]);
                else  dist[j] = min(dist[t], w[j]);
                
                if(!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    cin >> n >> m;
    for(int i = 1; i <= n; i ++ )   cin >> w[i];
    
    memset(h, -1, sizeof h);
    memset(rh, -1, sizeof rh);
    
    while(m -- )
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(h, a, b), add(rh, b, a);
        if(c == 2)  add(h, b, a), add(rh, a, b);
    }
    
    spfa(h, dmin, 0);
    spfa(rh, dmax, 1);
    
    int res = -1;
    for(int i = 1; i <= n; i ++ )
        res = max(dmax[i] - dmin[i], res);
    
    cout << res << endl;
    
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值