[luogu p1807] 最长路

传送门

最长路

题目描述

\(G\) 为有 \(n\) 个顶点的带权有向无环图,\(G\) 中各顶点的编号为 \(1\)\(n\),请设计算法,计算图 \(G\)\(<1,n>\) 间的最长路径。

输入输出格式

输入格式

输入的第一行有两个整数,分别代表图的点数 \(n\) 和边数 \(m\)

\(2\) 到第 \((m + 1)\) 行,每行 \(3\) 个整数 \(u, v, w\),代表存在一条从 \(u\)\(v\) 边权为 \(w\) 的边。

输出格式

输出一行一个整数,代表 \(1\)\(n\) 的最长路。

\(1\)\(n\) 不联通,请输出 \(-1\)

输入输出样例

输入样例 #1

2 1
1 2 1

输出样例 #1

1

说明

数据规模与约定

  • 对于 \(20\%\)的数据,\(n \leq 100\)\(m \leq 10^3\)
  • 对于 \(40\%\) 的数据,\(n \leq 10^3\)\(m \leq 10^{4}\)
  • 对于 \(100\%\) 的数据,\(1 \leq n \leq 1500\)\(1 \leq m \leq 5 \times 10^4\)\(1 \leq u, v \leq n\)\(-10^5 \leq w \leq 10^5\)

分析

此题有两种解法。一是最短路魔改,二是拓扑排序。

最短路魔改这个解法相信很简单,用支持负图的最短路算法(比如SPFA,BF等),将所有边权值取反,跑最短路,然后结果再取反即可。当然不支持负图的最短路算法,比如dij,也可以通过更改一些内容来实现最长路,这些都是很简单的,不再细说。

拓扑排序解法也很简单。定义 \(dis_i\)\(1 \rightarrow i\) 的最短路径, \(ind_i\)\(i\) 节点的入度,\(flag_i\)\(i\) 节点是否与 \(1\) 节点连通。算法如下:

  1. 设置 \(ind\)\(dis_n\) 设置为 \(-1\)\(flag_1\) 设置为true。
  2. 将所有满足 \(ind_i = 0\)\(i\) push进队列 \(q\)
  3. 若队列非空,取队列第一个作为节点 \(u\),并pop掉该节点。
  4. 遍历 \(u\) 指向的点 \(v\),令 \(ind_v - 1 \rightarrow ind_v\)
  5. \(flag_u\) 为true,且 \(dis_v < dis_u + w\)\(w\)\(u \rightarrow v\) 这条边的权值),那么令 \(dis_v = dis_u + w\)。(最长路核心步骤)
  6. 另外,如果 \(flag_u\) 为true,那么 \(flag_v\) 也要设置为 true。
  7. 如果 \(ind_v = 0\),将 \(v\) 节点入队 \(q\)
  8. 重复 \(3 \sim 7\) 步,直到 \(q\) 队列为空。此时的 \(dis_n\) 即为 \(1 \rightarrow n\) 的最长路。

这个算法的正确性也挺显然的。如果 \(ind_u = 0\),那么 \(dis_u\) 就会变为定值。

那就上代码吧。

代码

/*
 * @Author: crab-in-the-northeast 
 * @Date: 2020-09-25 21:24:06 
 * @Last Modified by: crab-in-the-northeast
 * @Last Modified time: 2020-09-26 11:36:28
 */
#include <iostream>
#include <cstdio>
#include <queue>
#include <vector>

const int maxn = 1505;
const int maxm = 50005;

struct edge {
    int u, v, dis;
};

int n, m;
std :: vector <edge> G[maxn];
int ind[maxn], dis[maxn];
bool flag[maxn];

void topsort() {
    flag[1] = true;
    std :: queue <int> q;
    for (int i = 1; i <= n; ++i)
        if (ind[i] == 0)
            q.push(i);
    
    while (!q.empty()) {
        int u = q.front();
        q.pop();

        for (int i = 0; i < G[u].size(); ++i) {
            int v = G[u][i].v;
            --ind[v];
            if (flag[u]) {
                if (dis[v] < dis[u] + G[u][i].dis)
                    dis[v] = dis[u] + G[u][i].dis;
                flag[v] = true;
            }

            if (ind[v] == 0)
                q.push(v);
        }
    }
}

int main() {
    std :: scanf("%d %d", &n, &m);
    for (int i = 1; i <= m; ++i) {
        edge now;
        std :: scanf("%d %d %d", &now.u, &now.v, &now.dis);
        G[now.u].push_back(now);
        ++ind[now.v];
    }

    dis[n] = -1;
    topsort();
    std :: printf("%d\n", dis[n]);
    return 0;
}

番外:一个错误的解法

有些人可能会说,啊东北小蟹蟹,你一开始就把所有的 \(ind_i = 0\)\(i\) 都入队了,这是对的,但是感觉有点浪费,如果我把所有满足 \(ind_i = 0\)\(i \neq 1\) 的节点的出边都砍掉,然后直接从1开始,不会很省时间吗?

但实际上,这个解法是错误的。来举一个反例:

0P3niQ.png

如果按照这个算法,一开始是先删除了 \(3 \rightarrow 2\) 这条边,然后进入拓扑,\(4 \rightarrow 5\)。到这里都是正常的,但是从 \(5\) 开始出现了问题。因为有 \(2 \rightarrow 5\) 这条边,所以 \(ind_5 \neq 0\),因此 \(5\) 没有入队,于是算法提前迷之结束了,也就是说算法认为 \(1 \rightarrow 6\) 不连通。离谱吧?

但是,这个算法还真的可过这道题……为什么呢?

蟹蟹的推测是,数据是按照\(u \rightarrow v \in G\),那么 \(u < v\) 来造的。如果数据一定满足这个条件,那么这个算法就是没问题的。这又是为什么呢?

再来举一个和刚刚非常相似的例子。

0P8lfH.png

区别在哪里?没错,\(2\)\(3\) 序号颠倒了。你可能想问了,这和刚刚有什么区别啊?算法不还是认为 \(1 \rightarrow 6\) 不连通吗?

你想,我从 \(2 \rightarrow n\) 删边,那么我首先删了 \(2 \rightarrow 3\) 这条边,那么 \(ind_3\) 变为了 \(0\)。如下图:

0P80hQ.png

此时开始到 \(3\) 号节点。发现 \(ind_3 = 0\),因此算法会把 \(3 \rightarrow 5\) 这条边删掉。如下:

0P8z3d.png

此时再运行拓扑排序,答案就是正常的,为 \(8\)。而最开始的那张图呢?

0P3niQ.png

算法会先尝试处理 \(2\)发现 \(ind_2 \neq 0\),因此没有任何变化,转而处理 \(3\),删掉了 \(3 \rightarrow 2\) 这条边。但是 \(2 \rightarrow 5\) 这条边并没有成功删除,因此 \(5\) 节点变成了死点

评测记录

评测记录

posted @ 2020-09-26 11:38  东北小蟹蟹  阅读(62)  评论(0编辑  收藏