#include using namespace std; const int N = 25010, M = 150010; const int INF = 0x3f3f3f3f; typedef pair 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 block[N]; // 连通块包含哪些节点 int bcnt; // 连通块序号计数器 int dis[N]; // 最短距离(结果数组) int in[N]; // 每个DAG(节点即连通块)的入度 bool st[N]; // dijkstra用的是不是在队列中的数组 queue 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 j = e[i]; if (!id[j]) dfs(j, bid); // Flood Fill } } // 计算得到bid这个连通块中最短距离 void dijkstra(int bid) { priority_queue, greater> pq; /* 因为不确定连通块内的哪个点可以作为起点,所以就一股脑全加进来就行了, 反正很多点的dis都是inf(这些都是不能成为起点的),那么可以作为起点的就自然出现在堆顶了 因为上面的写法把拓扑排序和dijkstra算法拼在一起了,如果不把所有点都加入堆, 会导致后面其他块的din[]没有减去前驱边,从而某些块没有被拓扑排序遍历到。 */ for (auto u : block[bid]) pq.push({dis[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 (dis[v] > dis[u] + w[i]) { dis[v] = dis[u] + w[i]; // 如果是同团中的道路,需要再次进入Dijkstra的小顶堆,以便计算完整个团中的路径最小值 if (id[u] == id[v]) pq.push({dis[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(dis, 0x3f, sizeof dis); // 初始化最短距离 dis[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 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 (dis[i] > INF / 2) puts("NO PATH"); else cout << dis[i] << endl; } return 0; }