最长路
题目描述
设 (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 imes 10^4),(1 leq u, v leq n),(-10^5 leq w leq 10^5)。
分析
此题有两种解法。一是最短路魔改,二是拓扑排序。
最短路魔改这个解法相信很简单,用支持负图的最短路算法(比如SPFA,BF等),将所有边权值取反,跑最短路,然后结果再取反即可。当然不支持负图的最短路算法,比如dij,也可以通过更改一些内容来实现最长路,这些都是很简单的,不再细说。
拓扑排序解法也很简单。定义 (dis_i) 为 (1 ightarrow i) 的最短路径, (ind_i) 为 (i) 节点的入度,(flag_i) 为 (i) 节点是否与 (1) 节点连通。算法如下:
- 设置 (ind),(dis_n) 设置为 (-1),(flag_1) 设置为true。
- 将所有满足 (ind_i = 0) 的 (i) push进队列 (q)。
- 若队列非空,取队列第一个作为节点 (u),并pop掉该节点。
- 遍历 (u) 指向的点 (v),令 (ind_v - 1 ightarrow ind_v)。
- 若 (flag_u) 为true,且 (dis_v < dis_u + w)((w) 为 (u ightarrow v) 这条边的权值),那么令 (dis_v = dis_u + w)。(最长路核心步骤)
- 另外,如果 (flag_u) 为true,那么 (flag_v) 也要设置为 true。
- 如果 (ind_v = 0),将 (v) 节点入队 (q)。
- 重复 (3 sim 7) 步,直到 (q) 队列为空。此时的 (dis_n) 即为 (1 ightarrow 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
", dis[n]);
return 0;
}
番外:一个错误的解法
有些人可能会说,啊东北小蟹蟹,你一开始就把所有的 (ind_i = 0) 的 (i) 都入队了,这是对的,但是感觉有点浪费,如果我把所有满足 (ind_i = 0) 且 (i eq 1) 的节点的出边都砍掉,然后直接从1开始,不会很省时间吗?
但实际上,这个解法是错误的。来举一个反例:
如果按照这个算法,一开始是先删除了 (3 ightarrow 2) 这条边,然后进入拓扑,(4 ightarrow 5)。到这里都是正常的,但是从 (5) 开始出现了问题。因为有 (2 ightarrow 5) 这条边,所以 (ind_5 eq 0),因此 (5) 没有入队,于是算法提前迷之结束了,也就是说算法认为 (1 ightarrow 6) 不连通。离谱吧?
但是,这个算法还真的可过这道题……为什么呢?
蟹蟹的推测是,数据是按照若 (u ightarrow v in G),那么 (u < v) 来造的。如果数据一定满足这个条件,那么这个算法就是没问题的。这又是为什么呢?
再来举一个和刚刚非常相似的例子。
区别在哪里?没错,(2) 和 (3) 序号颠倒了。你可能想问了,这和刚刚有什么区别啊?算法不还是认为 (1 ightarrow 6) 不连通吗?
你想,我从 (2 ightarrow n) 删边,那么我首先删了 (2 ightarrow 3) 这条边,那么 (ind_3) 变为了 (0)。如下图:
此时开始到 (3) 号节点。发现 (ind_3 = 0),因此算法会把 (3 ightarrow 5) 这条边删掉。如下:
此时再运行拓扑排序,答案就是正常的,为 (8)。而最开始的那张图呢?
算法会先尝试处理 (2),发现 (ind_2 eq 0),因此没有任何变化,转而处理 (3),删掉了 (3 ightarrow 2) 这条边。但是 (2 ightarrow 5) 这条边并没有成功删除,因此 (5) 节点变成了死点。