342. 道路与航线

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

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

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

每条道路 i 或者航线 i 连接城镇 Ai 到 Bi,花费为 Ci。

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

道路是双向的,可以从 Ai 到 Bi,也可以从 Bi 到 Ai,花费都是 Ci。

然而航线与之不同,只可以从 Ai 到 Bi。

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

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

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

输入格式

第一行包含四个整数 T,R,P,S。

接下来 R 行,每行包含三个整数(表示一个道路)Ai,Bi,Ci。

接下来 P 行,每行包含三个整数(表示一条航线)Ai,Bi,Ci。

输出格式

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

数据范围

1≤T≤25000,
1≤R,P≤50000,
1≤Ai,Bi,S≤T

输入样例:
6 3 3 4
1 2 5
3 4 5
5 6 10
3 5 -100
4 6 -100
1 3 -10

输出样例:

NO PATH
NO PATH
5
0
-95
-100
思路:
/*
这题如果直接用spfa来做的话,因为spfa已经死了,所以会被卡成 O(nm) TLE。而因为有负权边的原因,不能直接用 dij 来做。这里我们可以发现题目利用恐怖分子之名为我们提供了一个很有用的条件:单向边没有回路。如果通过一条单向边从 a 走到 b ,那么必不可能通过另外一条单向边从 b 走到 a 。因此这里我们可以把单向边连起来的每一块看成是一个连通块。因为联通块内部是没有负权边的,因此在联通块内部可以使用 dij 来求得最短路。而在联通块之间虽然有负权边,但这些负权边组成了一个 DAG (有向无环图)。因此我们可以通过求拓扑序列的方式来求得其最短路。时间复杂度是 O(n)。这样我们就能把总体的时间复杂度控制在 O(nlogm) 了。

1、先输入所有双向道路,然后dfs出所有连通块,计算两个数组:id[]存储每个点属于哪个连通块;
   vector<int>block[]存储每个连通块里有哪些点
2、输入所有航线,同时统计出每个连通块的入度
3、按照拓扑序,依次处理每个连通块,先将所有入度为0的连通块的编号加入队列中。
4、每次从队头取出一个连通块的编号。
5、将block[bid]中的所有点加入堆中,然后对堆中所有点做dijkstra。
6、每次取出堆中的距离最小的点ver。
7、然后遍历ver的所有邻点j,如果id[ver] == id[j],如果j能被更新,则将j插入堆中;
   否则说明访问到一条航线,则将id[j]这个连通块的入度减一,如果减到0了,就将其插入到拓扑序的队列中
*/
代码:
#include <bits/stdc++.h>
#define x first;
#define y second;
using namespace std;
typedef pair<int, int> PII;
const int N = 25010, M = 150010, INF = 0x3f3f3f3f;
int n, mr, mp, S;
int id[N];
int h[N], ne[M], e[M], w[M], idx;
int dist[N], din[N];
vector<int> block[N];
int bcnt;
queue<int> qu;
int st[N];

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

void dfs(int u, int bid) // 遍历所有块
{
    id[u] = bid, block[bid].push_back(u);
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!id[j])
            dfs(j, bid);
    }
}

void dijkstra(int bid)
{
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    for (auto it : block[bid]) // 全都要放入 不知道哪个最小
        heap.push({dist[it], it});

    while (heap.size())
    {
        PII t = heap.top();
        heap.pop();

        int ver = t.y;
        if (st[ver])
            continue;
        st[ver] = 1;

        for (int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (id[j] != id[ver] && --din[id[j]] == 0)
                qu.push(id[j]);
            if (dist[j] > dist[ver] + w[i])
            {
                dist[j] = dist[ver] + w[i]; // 负权边也更新
                if (id[j] == id[ver])
                    heap.push({dist[j], j});
            }
        }
    }
}

void topsort()
{
    memset(dist, 0x3f, sizeof(dist));
    dist[S] = 0;

    for (int i = 1; i <= bcnt; i++)
    {
        if (!din[i])
            qu.push(i);
    }

    while (qu.size())
    {
        int t = qu.front();
        qu.pop();
        dijkstra(t);
    }
}

int main()
{
    cin >> n >> mr >> mp >> S;
    memset(h, -1, sizeof(h));
    while (mr--)
    {
        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, bcnt);
        }
    }

    while (mp--)
    {
        int a, b, c;
        cin >> a >> b >> c;
        din[id[b]]++;
        add(a, b, c);
    }

    topsort();

    /*
     对于这题来说,假设起点在第三个连通块里,第一个连通块到第二个连通块有一条负权的航线,
    那么第二个连通块中的dist会被更新为dist[i] < INF。根据题意不存在环,因此第一个和第二个连通块中的城镇都是不可达的
    */
    for (int i = 1; i <= n; i++)
        if (dist[i] > INF / 2)
            cout << "NO PATH" << endl;
        else
            cout << dist[i] << endl;

    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

追寻远方的人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值