最长路&&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;
}
posted @ 2023-03-12 14:55  光風霽月  阅读(120)  评论(0编辑  收藏  举报