You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

10 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

AcWing 342 道路与航线

一、题目描述

农夫约翰正在一个新的销售区域对他的牛奶销售方案进行调查。

他想把牛奶送到 T 个城镇,编号为 1T

这些城镇之间通过 R 条道路 (编号为 1R) 和 P 条航线 (编号为 1P) 连接。

每条道路 i 或者 航线 i 连接城镇 A_iB_i,花费为 C_i

对于道路,0≤C_i≤10,000;然而航线的花费很神奇,花费 C_i 可能是负数(10,000≤Ci≤10,000)

道路是双向的,可以从 A_iB_i,也可以从 B_iA_i,花费都是 C_i

然而 航线与之不同,只可以从 A_iB_i

事实上,由于最近恐怖主义太嚣张,为了社会和谐,出台了一些政策: 保证如果有一条航线可以从 A_iB_i,那么保证不可能通过一些道路和航线从B_i 回到 A_i

由于约翰的奶牛世界公认十分给力,他需要运送奶牛到每一个城镇。

他想找到从发送中心城镇 S 把奶牛送到每个城镇的最便宜的方案

输入格式 第一行包含四个整数 T,R,P,S

接下来 R 行,每行包含三个整数(表示一个道路)A_i,B_i,C_i

接下来 P 行,每行包含三个整数(表示一条航线)A_i,B_i,C_i

输出格式1..T 行:第 i 行输出从 S 到达城镇 i 的最小花费,如果不存在,则输出 NO PATH

二、Dijkstra不能处理负权边,但可以处理负权初值

我们说了Dijkstra算法不能解决带有负权边的图,这是为什么呢?下面用一个例子讲解一下

以这里图为例,一共有五个点,也就说要循环5次,确定每个点的最短距离

Dijkstra算法解决的的详细步骤

  1. 初始dist[1] = 01号点距离起点1的距离为0
  2. 找到了未标识且离起点1最近的结点1,标记1号点,用1号点更新和它相连点的距离,2号点被更新成dist[2] = 23号点被更新成dist[3] = 5
  3. 找到了未标识且离起点1最近的结点2,标识2号点,用2号点更新和它相连点的距离,4号点被更新成dist[4] = 4
  4. 找到了未标识且离起点1最近的结点4,标识4号点,用4号点更新和它相连点的距离,5号点被更新成dist[5] = 5
  5. 找到了未标识且离起点1最近的结点3,标识3号点,用3号点更新和它相连点的距离,4号点被更新成dist[4] = 3

结果

Dijkstra算法在图中走出来的最短路径是1 -> 2 -> 4 -> 5,算出 1 号点到5 号点的最短距离是2 + 2 + 1 = 5,然而还存在一条路径是1 -> 3 -> 4 -> 5,该路径的长度是5 + (-2) + 1 = 4 因此 dijkstra 算法 失效

总结

我们可以发现如果有负权边的话4号点经过标记后还可以继续更新 但此时4号点已经被标记过了,所以4号点不能被更新了,只能一条路走到黑 当用负权边更新4号点后5号点距离起点的距离我们可以发现可以进一步缩小成4
所以总结下来就是:dijkstra不能解决负权边 是因为 dijkstra要求每个点被确定后,dist[j]就是最短距离了,之后就不能再被更新了(一锤子买卖),而如果有负权边的话,那已经确定的点的dist[j]不一定是最短了,可能还可以通过负权边进行更新。

负权初始值 那如果不是负权的边长,而是负权的初值呢?这个就没关系了,因为初值不影响算法逻辑,不信你看下有好多算法题都是判断INF/2,正无穷不也是在过程中松弛操作更改过吗,你是负的初始值也是没有问题,可以正确运行算法。

三、拓扑序+Dijkstra + 缩点

  • ① 分析题目可知城镇内部之间的权值是非负的,内部可以使用dijkstra算法
  • ② 城镇之间的航线 有负权,不能用Dijkstra。虽然SFPA可以搞定负权,但记住它已经死了,不考虑它~
  • ③ 如果有严格的顺序关系,即拓扑序,按照 城镇拓扑序的关系,是可以使用Dijkstra的,原因如下: 每个城镇称为一个 ,按照 拓扑序 遍历到某个团时,此时该团中城市的距离不会再被其它团更新,因此可以 按照拓扑序 依次 运行 dijkstra 算法

算法步骤

Code

#include <bits/stdc++.h>
using namespace std;
const int N = 25010, M = 150010;
const int INF = 0x3f3f3f3f;

typedef pair<int, int> PII;

// 存图
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 T; // 城镇数量
int R; // 道路数量
int P; // 航线数量
int S; // 出发点

// 下面两个数组是一对
int id[N];            // 节点在哪个连通块中
vector<int> block[N]; // 连通块包含哪些节点
int bcnt;             // 连通块序号计数器

int dist[N];  // 最短距离(结果数组)
int in[N];    // 每个DAG(节点即连通块)的入度
bool st[N];   // dijkstra用的是不是在队列中的数组
queue<int> q; // 拓扑序用的队列

// 将u节点加入团中,团的番号是 bid
void dfs(int u, int bid) {
    id[u] = bid;             // ① u节点属于bid团
    block[bid].push_back(u); // ② 记录bid团包含u节点
    // 枚举u节点的每一条出边将对端的城镇也加入到bid这个团中
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (!id[v]) dfs(v, bid); // Flood Fill
    }
}

// 计算得到bid这个连通块中最短距离
void dijkstra(int bid) {
    priority_queue<PII, vector<PII>, greater<PII>> pq;
    /*
    因为不确定连通块内的哪个点可以作为起点,所以就一股脑全加进来就行了,
    反正很多点的dist都是inf这些都是不能成为起点的,那么可以作为起点的就自然出现在堆顶了

    因为上面的写法把拓扑排序和dijkstra算法拼在一起了如果不把所有点都加入堆
    会导致后面其他块的din[]没有减去前驱边,从而某些块没有被拓扑排序遍历到。
    */
    for (auto u : block[bid]) pq.push({dist[u], u});

    while (pq.size()) {
        int u = pq.top().second;
        pq.pop();
        if (st[u]) continue;
        st[u] = true;
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];
            if (st[v]) continue;

            if (dist[v] > dist[u] + w[i]) {
                dist[v] = dist[u] + w[i];
                // 如果是同团中的道路,需要再次进入Dijkstra的小顶堆以便计算完整个团中的路径最小值
                if (id[u] == id[v]) pq.push({dist[v], v});
            }
            /*如果u和v不在同一个团中,说明遍历到的是航线
             此时,需要与拓扑序算法结合,尝试剪掉此边,是不是可以形成入度为的团

             id[v]:v这个节点所在的团番号
             --in[id[v]] == 0: u->v是最后一条指向团id[v]的边此边拆除后id[v]这个团无前序依赖,稳定了,
             可以将此团加入拓扑排序的queue队列中继续探索
           */
            if (id[u] != id[v] && --in[id[v]] == 0) q.push(id[v]);
        }
    }
}

// 拓扑序
void topsort() {
    for (int i = 1; i <= bcnt; i++) // 枚举每个团
        if (!in[i]) q.push(i);      // 找到所有入度为0的团,DAG的起点

    // 拓扑排序
    while (q.size()) {
        int bid = q.front(); // 团番号
        q.pop();
        // 在此团内部跑一遍dijkstra
        dijkstra(bid);
    }
}

int main() {
    memset(h, -1, sizeof h);              // 初始化
    scanf("%d %d %d %d", &T, &R, &P, &S); // 城镇数量,道路数量,航线数量,出发点

    memset(dist, 0x3f, sizeof dist); // 初始化最短距离
    dist[S] = 0;                     // 出发点距离自己的长度是0,其它的最短距离目前是INF

    int a, b, c; // 起点,终点,权值

    while (R--) { // 读入道路
        scanf("%d %d %d", &a, &b, &c);
        add(a, b, c), add(b, a, c); // 连通块内是无向图
    }

    /* 航线本质是 团与团 之间单向连接边
     外部是DAG有向无环图局部是内部双向正权图
     为了建立外部的DAG有向无环图我们需要给每个团分配一个番号记为bid;
     同时,也需要知道每个团内,有哪些小节点:
     (1) id[i]:节点i隶属于哪个团(需要提前准备好团的番号)
     (2) vector<int> block[N] :每个团中有哪些节点

     Q:一共几个团呢?每个团中都有谁呢?谁都在哪个图里呢?
     A:在没有录入航线的情况下,现在图中只有 大块孤立 但 内部连通 的节点数据,
     可以用dfs进行Flood Fill,发现没有团标识的节点,就创建一个新的团番号,
     并且记录此节点加入了哪个团,记录哪个团有哪些点。
     注意:需要在未录入航线的情况下统计出团与节点的关系,否则一会再录入航线,就没法找出哪些节点在哪个团里了
    */
    // 缩点
    for (int i = 1; i <= T; i++) // 枚举每个小节点
        if (!id[i])              // 如果它还没有标识是哪个团,就开始研究它,把它标识上隶属于哪个团,并且,把和它相连接的其它点也加入同一个团中
            dfs(i, ++bcnt);      // 需要提前申请好番号bcnt

    // 航线
    while (P--) {
        scanf("%d %d %d", &a, &b, &c);
        add(a, b, c); // 单向边
        in[id[b]]++;  // b节点所在团入度+1
    }

    // 拓扑序
    topsort();

    // 从S到达城镇i的最小花费
    for (int i = 1; i <= T; i++) {
        if (dist[i] > INF / 2)
            puts("NO PATH");
        else
            cout << dist[i] << endl;
    }
    return 0;
}