最长路&&dijkstra不适用的情况
一、\(dijkstra\) 不能求带负权最短路
我们知道,\(dijkstra\) 算法在求最短路是基于贪心思想的:每次选取一个点出队,从起点点到这个点的距离一定是最短的(其实就是,\(dijkstra\) 很呆,它简单而又固执的认为,边的个数越多,你的总长度肯定更长!)。然后用这个点来更新其余的点,循环往复,并且每个点只用来更新一次,多更新也没啥用。
但是,有负权边时,\(dijkstra\) 就不可以了,因为此时,边的个数增加,总边权可能更小了,此时 \(dijkstra\) 就无法处理了。
你可能会想,那么我们让每个点重复入队来更新不就好了?可以是可以,但是,这就是 \(spfa\) 😠。
二、图中有环
在求最短路时,当边权都是正数时,有环并不影响结果,此时 \(dijkstra\) 和 \(spfa\) 都可行。
但若存在负环,由于不存在最短路,所以任何算法都不行。
三、\(dijkstra\) 不能求最长路
又是 \(dijkstra\) ,我们上面分析了,\(dijkstra\) 不能求带负权的。同理,对于最长路(假设所有边的权值都是正值),\(dijkstra\) 也会认为边越少,越好,但此时,其实是边越多越好。
那么,我们将所有正权边取一个负数,那么求最长路(正的最大值)就转化为了求最短路(负的最小值),就可以用 \(dijkstra\) 了?但这其实是错误的,因为 \(dijkstra\) 无法处理有负权的情况 😠。
四、\(dijkstra\) 在图当中维护最值
当图中不存在环时,\(dijkstra\) 是可以维护到某个节点的最值的,因为在走这个节点后面的边时,不会影响前面的结果。
但如果图中存在环,那么后续的边就可能会影响前面的边,这跟 \(dijkstra\) 无法维护负权边是一个道理,负权边会回过头来更新前面已经维护完的节点,但是前面的节点由于已经出队,因此无法再用它的值去更新它后面的节点,这就导致后面节点的值未必是最优的。
五、如何求最长路
方法一
一种很显然的思路,就是将边权取负,然后就求最长路等价于 \(spfa\) 求最短路,记得最后结果再次取负,同时判断能否可达也很容易。
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 1510, M = 5e4 + 10, INF = 0x3f3f3f3f;
int n, m;
int h[N], e[M], ne[M], w[M], idx;
int dist[N];
bool st[N];
void add(int a, int b, int c)
{
w[idx] = c;
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
int spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while(q.size())
{
auto t = q.front(); q.pop();
st[t] = false;
for(int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if(dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if(!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return dist[n];
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
while (m -- )
{
int a, b, c; cin >> a >> b >> c;
add(a, b, -c);
}
int t = spfa();
if(t >= INF / 2) cout << -1 << endl;
else cout << -t << endl;
return 0;
}
方法二
那么,\(spfa\) 能否直接求最长路,而不用转化为最短路问题来求呢?答案是可以的!难点主要在于如何判断不存在可达路径
在求最短路时,贪心的想,可达路径上的所有边权为正的最大值,边数有限,那么最短路的值不可能大于一个最大值。
在求最长路时,贪心的想,可达路径上的所有边权为负的最小值,边数有限,那么最长路的值不可能小于一个最小值。
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 1510, M = 5e4 + 10, INF = -0x3f3f3f3f; // 😊
int n, m;
int h[N], e[M], ne[M], w[M], idx;
int dist[N];
bool st[N];
void add(int a, int b, int c)
{
w[idx] = c;
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
int spfa()
{
// 最短路:memset(dist, 0x3f, sizeof dist);
memset(dist, -0x3f, sizeof dist); // 😊
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while(q.size())
{
auto t = q.front(); q.pop();
st[t] = false;
for(int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
// 最短路:if(dist[j) > dist[t] + w[i]
if(dist[j] < dist[t] + w[i]) // 😊
{
dist[j] = dist[t] + w[i];
if(!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return dist[n];
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
while (m -- )
{
int a, b, c; cin >> a >> b >> c;
add(a, b, c);
}
int t = spfa();
// 求最短路是 if(t>=INF/2),求最长路就是 if(t<=INF/2)
if(t <= INF / 2) cout << -1 << endl; // 😊
else cout << t << endl;
return 0;
}
方法三
如果题目告诉我们这是一个 \(DAG\) 的话,我们可以通过 拓扑 + \(DP\) 来求解,即设置一个数组 \(f[i]\) 表示走到 \(i\) 时的最长路,然后在做 \(top\_sort\) 的过程中维护这个 \(f[]\),由于 \(DAG\) 图满足拓扑序,因此 \(DP\) 是合法的。
题目的难点主要在于,我们需要保证 \(n\) 是从 \(1\) 作为起点转移过去的,而这里有一个巨大的坑点!!!
题目要求 \(n\) 必须从 \(1\) 走过去,因此在拓扑排序时,我一开始只将 \(1\) 入队了,这样就不会出现从其它节点更新 \(n\) 的情况了。但是有下面一种情况:1->3->4
, 2->3->4
(\(n=4\)),此时 \(1\) 和 \(2\) 的入度都为 \(0\),如果我们只将 \(1\) 入队,那么由于顶点 \(3\) 还有 \(2\)->\(3\) 的入度没法消去,因此 \(3\) 无法入对,从而无法 \(3\)->\(4\) 将 \(n\) 入队,从而更新不了 \(f[n]\),由此误认为无法通过 \(1\) 转移到 \(n\)。
因此我们需要将除了起点 \(1\) 的所有入度为 \(0\) 的顶点入队,注意 \(1\) 的入度一定为 \(0\),因为题目要求 i->j (i<j)
,然后消去它们连接的边产生的入度,注意此时如果有点入度消为 \(0\),它也需要入队,原理同上面分析!然后在正常从 \(1\) 开始做 \(top\_sort + dp\)。
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int M = 5e4 + 10, N = 1510;
int n, m;
int in[N], f[N];
int h[N], e[M], w[M], ne[M], idx;
void add(int a, int b, int c)
{
in[b] ++ ;
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void top_sort()
{
queue<int> q;
// 先消除其他入度为0的点的影响
for(int i = 2; i <= n; i ++ )
if(!in[i])
q.push(i);
while(q.size())
{
auto t = q.front(); q.pop();
for(int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if( -- in[j] == 0) q.push(j);
}
}
// 从1开始dp
q.push(1);
while(q.size())
{
auto t = q.front(); q.pop();
for(int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
f[j] = max(f[j], f[t] + w[i]);
if(-- in[j] == 0) q.push(j);
}
}
}
int main()
{
memset(h, -1, sizeof h);
// 因为1是起点,所以f[1]=0,其余结尾负无穷
for(int i = 2; i <= N; i ++ ) f[i] = -1e9;
cin >> n >> m;
for(int i = 1; i <= m; i ++ )
{
int a, b, c; cin >> a >> b >> c;
add(a, b, c);
}
top_sort();
if(f[n] == -1e9) cout << -1 << endl;
else cout << f[n] << endl;
return 0;
}