• AcWing 340. 通信线路


    \(AcWing\) \(340\). 通信线路

    题目传送门

    一、题目描述

    在郊区有 \(N\) 座通信基站,\(P\)双向 电缆,第 \(i\) 条电缆连接基站 \(A_i\)\(B_i\)

    特别地,\(1\)号基站是通信公司的总站 (起点),\(N\)号基站 (终点) 位于一座农场中。

    现在,农场主希望对通信线路进行升级,其中升级第 \(i\) 条电缆需要花费 \(L_i\)

    电话公司正在举行优惠活动

    农产主可以指定一条从 \(1\) 号基站到 \(N\) 号基站的路径,并指定路径上不超过 \(K\) 条电缆,由电话公司 免费 提供升级服务

    农场主只需要支付在该路径上 剩余的电缆中升级价格最贵 的那条电缆的花费即可

    至少用多少钱 可以完成升级

    输入格式
    \(1\) 行:三个整数 \(N,P,K\)

    \(2..P+1\) 行:第 \(i+1\) 行包含三个整数 \(A_i,B_i,L_i\)

    输出格式
    包含一个整数表示最少花费。

    \(1\) 号基站与 \(N\) 号基站之间不存在路径,则输出 \(−1\)

    二、题目解析

    理解题意:找一条路径,边权最大的\(k\)条边忽略,\(k + 1\)大的边权作为该条路径的代价求最小代价

    转换:从 起点终点 的所有路径中,第\(k + 1\)大的 边权最小 是多少

    三、二分解法【入门解法】

    思考下二分一般应用于什么情况下:给一组数,先判断下要找的数在不在左半部分,在就在左半部分继续二分,不在就到右半部分去找

    可以用二分取解决的问题要满足两个特性

    • 解在一定的范围内,以便确定二分的左右端点

    • 给定数据具有一定的单调性

      这种单调性既可以是 显性 的,比如有序的数组,也可以是 隐性 的,只要知道中间数 满不满足条件 就可以 确定下一步查找的范围

    \(Q\):既然我们不知道这题的解如何求,那么是否有办法说:假定给我\(x\)元钱,我有没有办法确定这么多钱能否够升级一条路径呢?

    \(A\):有办法! 题目给定的\(L\)\(1\)\(100w\)间,这说明本题的解一定在\(0\)\(100w\)之间,否则就是无解,输出\(-1\)

    • 解为\(0\)的情况是这条路径上的边不超过\(k\)条,意味着不用花钱就可以升级线路
    • 无解的情况是从起点无法到达终点 : 中间没有路,或者,需要的钱太多,就是减免\(k\)条最长的,剩下的第\(k+1\)条边还是比\(x\)要贵,办不成事

    既然本题的解有一定的范围,并且,如果\(x\)元能够升级某条路径,那么解一定不会超过\(x\),这就是 单调性,也就意味着本题可以用 二分 解决

    边权只有\(0,1\)的最短路

    如何确定\(x\)元钱能否升级一条线路?

    给我们一条路径,我们把这条路径上的边权与\(x\)比较,只要大于\(x\)的边不超过\(k\)条就说明可以用不超过\(x\)元去升级这条线路!

    继续思考会发现,其实我们不关心这条路径上的每条边的具体权值是多少,只关心其与\(x\)的大小关系,因此整个图上的边就分为两类:

    • 边权 大于\(x\)
    • 边权不大于\(x\)

    大于\(x\)的边我们将其边权视为\(1\),否则边权视为\(0\),只要从起点到终点的 最短路径长度 不超过\(k\),(也就是边权大于\(x\)的路线不超过\(k\)条),说明\(x\)元升级线路是可行的。

    双端\(bfs\)

    边权只有\(0\)\(1\)两种情况的最短路问题可以用双端队列\(bfs\)解决,双端队列\(bfs\)相关的问题题解见\(AcWing\) \(175\) 电路维修,解释下双端队列\(BFS\)的思想:

    \(dijkstra\)算法的思路是维护一个小根堆,堆顶元素的距离永远是最小的,然后不断取出堆顶元素去松弛周围点的距离。而边权只有\(01\)两种情况的图我们无需维护一个小根堆,只需要维护一个双端队列,保证双端队列的队头元素离起点的距离永远是最小的即可,为此,从起点开始我们将起点加入队列,然后尝试去松弛周围的点,只要周围的点还未出队过,并且可以被松弛,就将该点松弛后加入队列中,边权是\(0\)就加入队头,边权是\(1\)就加入队尾。这就是双端队列\(BFS\)的基本思路。

    4.整理思路

    • \(0\)\(100w\)间二分答案,每次二分时通过双端队列\(bfs\)的方法判断 起点终点最短路径 是否不超过\(mid\),是的话就在左半部分继续二分,否则在右半部分二分,直到找到答案为止。

    • 任意一次\(bfs\)的过程中,一旦发现\(bfs\)终点起点 的距离还是 无穷大,说明终点不可达,直接输出\(-1\)终止程序。

    二分+双端\(bfs\)

    #include <bits/stdc++.h>
    using namespace std;
    const int N = 1010;  // 1000个点
    const int M = 20010; // 10000条,记录无向边需要两倍空间
    int idx, h[N], e[M], w[M], ne[M];
    void add(int a, int b, int c) {
        e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
    }
    int n;        //点数
    int m;        //边数
    deque<int> q; //双端队列bfs模拟最短路径
    bool st[N];   //记录是不是在队列中
    int k;        //不超过K条电缆,由电话公司免费提供升级服务
    int d[N];     //记录最短距离
    
    bool check(int cost) {
        //多次检查,每次初始化
        memset(d, 0x3f, sizeof d);
        memset(st, false, sizeof st);
        // 1号基站是通信公司的总站
        q.push_front(1);
        d[1] = 0;
    
        while (q.size()) {
            int u = q.front();
            q.pop_front();
            //以后这种continue写法应该优先选择,因为可以使下面的代码减少括号层数
            if (st[u]) continue;
    
            st[u] = true;
            for (int i = h[u]; ~i; i = ne[i]) {
                int j = e[i];
                //如果边权大于二分值,视为1,否则为0,相当于利用最短路求大于cost的边个数
                int dist = d[u] + (w[i] > cost);
                if (dist < d[j]) {
                    d[j] = dist;
                    //大的靠后
                    if (w[i] > cost)
                        q.push_back(j);
                    else
                        //小的靠前
                        q.push_front(j);
                }
            }
        }
        //如果按上面的方法计算后,n结点没有被松弛操作修改距离,则表示n不可达
        if (d[n] == 0x3f3f3f3f) {
            puts("-1"); //不可达,直接输出-1
            exit(0);
        }
        return d[n] <= k;
    }
    int main() {
        memset(h, -1, sizeof h);
        cin >> n >> m >> k;
        int a, b, c;
        for (int i = 0; i < m; i++) {
            cin >> a >> b >> c;
            add(a, b, c), add(b, a, c);
        }
        /*这里二分的是直接面对答案设问:最少花费
        依题意,最少花费其实是所有可能的路径中,第k+1条边的花费
        如果某条路径不存在k+1条边(边数小于k+1),此时花费为0
        同时,任意一条边的花费不会大于1e6
        整理一下,这里二分枚举的值其实是0 ~ 1e6*/
        int l = 0, r = 1e6;
        while (l < r) {
            int mid = l + r >> 1;
            if (check(mid)) // check函数的意义:如果当前花费可以满足要求,那么尝试更小的花费
                r = mid;
            else
                l = mid + 1;
        }
        printf("%d\n", l);
        return 0;
    }
    

    二分+\(Dijkstra\)

    #include <bits/stdc++.h>
    using namespace std;
    typedef pair<int, int> PII;
    const int INF = 0x3f3f3f3f;
    const int N = 1010;  // 1000个点
    const int M = 20010; // 10000条,记录无向边需要两倍空间
    int idx, h[N], e[M], w[M], ne[M];
    void add(int a, int b, int c) {
        e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
    }
    int n;        //点数
    int m;        //边数
    bool st[N];   //记录是不是在队列中
    int k;        //不超过K条电缆,由电话公司免费提供升级服务
    int dist[N];  //记录最短距离
    // u指的是我们现在选最小花费
    bool check(int x) {
        memset(st, false, sizeof st);
        memset(dist, 0x3f, sizeof dist);
        priority_queue<PII, vector<PII>, greater<PII>> q;
        dist[1] = 0;
        q.push({0, 1});
    
        while (q.size()) {
            PII t = q.top();
            q.pop();
            int d = t.first, u = t.second;
            if (st[u]) continue;
            st[u] = true;
            for (int i = h[u]; ~i; i = ne[i]) {
                int j = e[i], v = w[i] > x; //如果有边比我们现在选的这条边大,那么这条边对方案的贡献为1,反之为0
                if (dist[j] > d + v) {
                    dist[j] = d + v;
                    q.push({dist[j], j});
                }
            }
        }
        //如果按上面的方法计算后,n结点没有被松弛操作修改距离,则表示n不可达
        if (dist[n] == INF) {
            puts("-1"); //不可达,直接输出-1
            exit(0);
        }
        return dist[n] <= k; //如果有k+1条边比我们现在这条边大,那么这个升级方案就是不合法的,反之就合法
    }
    int main() {
        memset(h, -1, sizeof h);
        cin >> n >> m >> k;
        int a, b, c;
        for (int i = 0; i < m; i++) {
            cin >> a >> b >> c;
            add(a, b, c), add(b, a, c);
        }
        /*这里二分的是直接面对答案设问:最少花费
        依题意,最少花费其实是所有可能的路径中,第k+1条边的花费
        如果某条路径不存在k+1条边(边数小于k+1),此时花费为0
        同时,任意一条边的花费不会大于1e6
        整理一下,这里二分枚举的值其实是0 ~ 1e6*/
        int l = 0, r = 1e6;
        while (l < r) {
            int mid = l + r >> 1;
            if (check(mid)) // check函数的意义:如果当前花费可以满足要求,那么尝试更小的花费
                r = mid;
            else
                l = mid + 1;
        }
        printf("%d\n", l);
        return 0;
    }
    

    四、分层图解法

    理论与例题

    本题是一道比较祼的分层图题,只要比标准例题变化了一下,不是求累加路径和,而是求所有路径的最大值,其它都没有变化:

    1、直接建图\(K+1\)大法

    #include <bits/stdc++.h>
    using namespace std;
    typedef pair<int, int> PII;
    const int N = 1e6 + 10; //只要开不死,就往死里开!
    const int INF = 0x3f3f3f3f;
    
    int h[N], e[N], ne[N], idx, w[N];
    void add(int a, int b, int c) {
        e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
    }
    
    int n, m, k;
    int dist[N];
    bool st[N];
    
    void dijkstra() {
        memset(dist, 0x3f, sizeof dist);
        memset(st, false, sizeof st);
        priority_queue<PII, vector<PII>, greater<PII>> q;
        q.push({0, 1});
        dist[1] = 0;
    
        while (q.size()) {
            PII t = q.top();
            q.pop();
            int u = t.second;
            if (st[u]) continue;
            st[u] = true;
    
            for (int i = h[u]; ~i; i = ne[i]) {
                int j = e[i];
                if (dist[j] > max(dist[u], w[i])) { //农场主只需要支付在该路径上剩余的电缆中,升级价格最贵的那条电缆的花费即可。
                    dist[j] = max(dist[u], w[i]);
                    q.push({dist[j], j});
                }
            }
        }
    }
    
    int main() {
        memset(h, -1, sizeof h);
        cin >> n >> m >> k;
        while (m--) {
            int a, b, c;
            cin >> a >> b >> c;
            //分层图建图
            for (int i = 0; i <= k; i++) {                                  //创建k+1层分层图
                add(a + i * n, b + i * n, c), add(b + i * n, a + i * n, c); //无向图
                if (i < k)                                                  //从第0层开始,到k-1层结束,都需要向下一层建立通道
                    add(a + i * n, b + (i + 1) * n, 0), add(b + i * n, a + (i + 1) * n, 0);
            }
        }
    
        dijkstra();
    
        // k+1个层中,都去找t的最短路径,再取最小值,就是答案
        int ans = INF;
        for (int i = 0; i <= k; i++) ans = min(ans, dist[n + i * n]);
    
        if (ans == INF) ans = -1;
        printf("%d\n", ans);
    
        return 0;
    }
    

    2、二维数组省空间解法

    #include <bits/stdc++.h>
    using namespace std;
    const int INF = 0x3f3f3f3f;
    
    //分层图+二维数组解法
    typedef pair<int, int> PII;
    typedef pair<int, PII> PIII;
    
    const int N = 1010, M = 20010;
    
    int n, m, k;
    int h[N], e[M], w[M], ne[M], idx;
    bool st[N][N];
    int dist[N][N];
    
    void add(int a, int b, int c) {
        e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
    }
    
    void dijkstra() {
        priority_queue<PIII, vector<PIII>, greater<PIII>> q; //优先队列
    
        memset(dist, 0x3f, sizeof dist);
        dist[1][0] = 0;                                      //起点
        q.push({0, {1, 0}});                                 //距离(用于小顶堆排序,寻找最短距离),[节点,层](用于描述是哪个点)
    
        while (q.size()) {
            PIII t = q.top();
            q.pop();
    
            int u = t.second.first, p = t.second.second; // p层u节点
            if (st[u][p]) continue;                      //如果p层u节点出过队列,则跳过
            st[u][p] = true;                             //标识p层u节点已经出过队列
            
            for (int i = h[u]; ~i; i = ne[i]) {          //枚举u节点的每个连接边
                int j = e[i];                            // u节点的相邻接节点j
                //先更新同层
                if (dist[j][p] > max(dist[u][p], w[i])) {
                    dist[j][p] = max(dist[u][p], w[i]);   //更新它
                    q.push({dist[j][p], {j, p}});         //被更新的点入队列
                }
                //更新下一层
                if (p < k && dist[j][p + 1] > dist[u][p]) { // p<k以保证最后一层不继续下探
                    dist[j][p + 1] = dist[u][p];            //因为此边权为0
                    q.push({dist[j][p + 1], {j, p + 1}});
                }
            }
        }
    }
    
    int main() {
        memset(h, -1, sizeof h);
        scanf("%d%d%d", &n, &m, &k);
        while (m--) {
            int a, b, c;
            scanf("%d%d%d", &a, &b, &c);
            add(a, b, c), add(b, a, c); //无向边
        }
        //分层图+最短路
        dijkstra();
    
        int ans = INF;
        for (int i = 0; i <= k; i++) ans = min(ans, dist[n][i]);
    
        //最小值都是正无穷,说明从1出发无法到达n这个位置
        if (ans == INF) ans = -1;
        printf("%d\n", ans);
        return 0;
    }
    

    五、\(DP+SPFA\)

    下面采用\(DP\)的思想出发,来解决此题:

    农场主只需要支付在该路径上剩余的电缆中,升级价格 最贵 的那条电缆的花费即可
    注意:这里 不是所有电缆的累加和 ,而是找 最贵 的,也就是\(max\)

    状态表示

    \(dist[u][p]\)为从\(1\)走到\(u\),花了\(p\)次机会的 答案。最终所求为$$\large min(dist[n][t]),t \in [0,k]$$

    所谓的 答案 ,也就是这条路径中,最贵 的那段路的价格,也就是\(max(w[i])\)

    再直白一点,就是\(dist\)里装的不是传统最短路径中的累加路径和,而是最长路径

    状态计算

    考虑一条边\((u,j,w[i])\)\(dp\)的贡献:

    • 在这条边 不使用机会

    \[\large dist[j][p] = min(dist[j][p], max(dist[u][p], w[i])) \]

    • 在这条边 使用机会

    \[\large dist[j][p+1] = min(dist[j][p+1], dist[u][p]) \]

    接下来直接\(dp\)就行了。但是码代码时不知道如何\(dp\),也就是 不知道\(dp\)顺序

    回过头来看题,发现图并没有说是\(DAG\)(有向无环图),那么转移就有后效性了。借助\(spfa\)的思想——迭代思想。

    \(spfa\)核心算法思想即不断松弛,直到松弛不了结束。

    那我不知道\(dp\)顺序,我就一直\(dp\)\(dp\)到无法继续\(dp\)为止(也就是\(dp\)无法更新状态为止)

    这两者是不是很像呢?于是我们将以前\(spfa\)中的松弛条件替换为\(dp\)转移方程就\(ok\)了。

    这样就无需考虑\(dp\)顺序了, 完成

    #include <bits/stdc++.h>
    using namespace std;
    typedef pair<int, int> PII;
    
    const int N = 1010;
    const int M = 20010;
    const int K = 1010;
    const int INF = 0x3f3f3f3f;
    
    int n, m, k, ans = INF;
    int dist[N][K];
    bool st[N][K];
    
    //邻接表
    int e[M], h[N], idx, w[M], ne[M];
    void add(int a, int b, int c) {
        e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
    }
    
    void spfa() {
        queue<PII> q;
        //出发点入队列
        q.push({1, 0}); //节点号,使用了几次机会
        dist[1][0] = 0; //距离也扩展为二维的,第二维记录使用了几次机会
    
        while (q.size()) {
            int u = q.front().first;  //节点号
            int p = q.front().second; //使用了几次机会
    
            st[u][p] = false;
            q.pop();
    
            for (int i = h[u]; ~i; i = ne[i]) {
                int j = e[i];
                //在这条边不使用机会
                if (dist[j][p] > max(dist[u][p], w[i])) {
                    dist[j][p] = max(dist[u][p], w[i]);
                    if (!st[j][p]) { //注意这里不能使用continue,否则直接下一次循环了,后面的 分枝无法走到!
                        q.push({j, p});
                        st[j][p] = true;
                    }
                }
                if (p < k) {
                    //在这条边使用机会
                    if (dist[j][p + 1] > dist[u][p]) {
                        dist[j][p + 1] = dist[u][p];
                        if (!st[j][p + 1]) {
                            q.push({j, p + 1});
                            st[j][p + 1] = true;
                        }
                    }
                }
            }
        }
    }
    
    int main() {
        //初始化
        memset(h, -1, sizeof h);
        memset(dist, 0x3f, sizeof(dist));
    
        //建图
        scanf("%d %d %d", &n, &m, &k);
        while (m--) {
            int a, b, c;
            scanf("%d %d %d", &a, &b, &c);
            add(a, b, c), add(b, a, c);
        }
        //利用spfa的松驰操作,做DP的数据填充
        spfa();
    
        //最终的结果,一定出现在使用0,1,2,...,n次机会里面
        for (int i = 0; i <= k; i++) ans = min(ans, dist[n][i]);
    
        //如果无法到达n,则输出-1
        if (ans == INF) ans = -1;
        printf("%d\n", ans);
        return 0;
    }
    
  • 相关阅读:
    使用WCF实现消息推送
    T31P电子秤数据读取
    持续性任务代码的一些测试
    XP+Android手机DIY家庭视频点播系统-历时3周全力打造吊丝的幸福生活
    Android 上传文件到XP
    Android ListView的一个坑,你可掉进去过?
    无脑无负担网站架构-- Application Request Route的一些应用
    Android 一些注意
    懒人的ERP开发框架--2B&苦B程序员专用
    PHP Token(令牌)设计应用
  • 原文地址:https://www.cnblogs.com/littlehb/p/16011807.html
Copyright © 2020-2023  润新知