题目:
前情提要:求最长路可以转换为最短路,把每条边都*-1就能把求最长路转换为求最短路,最后输出时再*-1即可(翻翻题解,大佬NB)
1.拓扑排序(感觉是神)(好吧,拓扑排序只适用无环图,老老实实学其他算法吧)
拓扑排序简介:https://blog.csdn.net/xxcdsg/article/details/127720727
适用条件:无环图
思路分析:拓扑排序能实现排到每个节点时,之前的节点均遍历过,也就保证了这个节点的最值性,也就是说保证每个节点算完才算之后的节点。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1.5e3 + 10,MAXM = 5e4 + 10;
int a[MAXN] = {0,1};//记录距离1的层数
int son[MAXN];//记录入度
//构图
int ptop = 1;
struct link{
int next,val;
link* nex;
}p[MAXM];
link *head[MAXN];
void add(int x,int y,int val)
{
p[ptop].nex = head[x];
head[x] = &p[ptop];
p[ptop].next = y;
p[ptop].val = val;
ptop++;
}
//拓扑排序
queue<int> qu;
//广搜
void dfs(int x)
{
link* pp = head[x];
while(pp != NULL)
{
int y = pp -> next,val = pp -> val;
son[y]--;
if(son[y] == 0)
qu.push(y);
if(a[x] != 0)
if(a[y] < a[x] + val)
a[y] = a[x] + val;//更新距离1的层数
pp = pp -> nex;
}
}
int main()
{
int n,m;cin >> n >> m;
for(int i = 1;i <= m;i++)
{
int x,y,val;scanf("%d %d %d",&x,&y,&val);
add(x,y,val);
son[y]++;
}
for(int i = 1;i <= n;i++)
if(son[i] == 0)
qu.push(i);
while(!qu.empty())
{
int x = qu.front();qu.pop();
dfs(x);
}
cout << a[n] - 1 << endl;
}
效率:41ms\1.20MB
2.dijkstra算法
适用条件:无负权
思路:如果一个节点到起点的距离为现在所有还没遍历的节点的最小值,那么它就无法找到比它距离更小的距离(以没有负距离为前提,所以有负权的图不能用)
步骤:
1.初始化起点距离为0,其他为INF
2.找最小距离的节点并更新它的出边(使用优先队列(堆) + 结构体来优化)
3.标记该节点已处理
4.重复2、3步骤直到所有节点(或者你要找的点)均被处理
图示:
emmm因为不能有负权值,所以例题洛谷P1807 最长路过不了,所以我找了另外一个例题洛谷P4779 【模板】单源最短路径(标准版)
代码(稍稍折磨了一小时):
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 10,MAXM = 2e5 + 10;
struct node{
int dis,p;
bool operator <(const node &x)const{
return dis > x.dis;
}//重载运算符
};
priority_queue<node> a;//存点
//构图
int ptop = 1;
struct link{
int next,val;
link* nex;
}p[MAXM];
link* head[MAXN];
void add(int x,int y,int val)
{
p[ptop].nex = head[x];
p[ptop].next = y;
p[ptop].val = val;
head[x] = &p[ptop];
ptop++;
}
int dis[MAXN];//距离答案
bool use[MAXN];
int main()
{
int n,m,s;cin >> n >> m >> s;
for(int i = 1;i <= m;i++)
{
int x,y,val;scanf("%d %d %d",&x,&y,&val);
add(x,y,val);
}
a.push((node){0,s});
while(!a.empty())
{
int predis = a.top().dis,po = a.top().p;a.pop();
if(use[po])
continue;
use[po] = 1;
dis[po] = predis;//确定这个点
link* pp = head[po];
while(pp != NULL)
{
int nextp = pp -> next,val = pp -> val;
if(!use[nextp] && (dis[nextp] == 0 || dis[nextp] >= predis + val))
{
dis[nextp] = predis + val;
a.push((node){dis[nextp],nextp});
}
pp = pp -> nex;
}
}
for(int i = 1;i <= n;i++)
cout << dis[i] << ' ';
}
3.Floyd弗洛伊德算法(暴力,真是太暴力了)
适用条件:数据量小的情况,毕竟O(n^3)时间复杂度
思想:枚举中间点,起点,终点来进行松弛,就像把每条路径比作绳子,你把它拉到最长。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1.5e3 + 10,MAXM = 5e4 + 10;
int a[MAXN][MAXN] = {0};
int main()
{
int n,m;cin >> n >> m;
for(int i = 1;i <= n;i++)
for(int j = 1;j <= n;j++)
if(i == j);
else
a[i][j] = -1;
for(int i = 1;i <= m;i++)
{
int x,y,val;scanf("%d %d %d",&x,&y,&val);
if(a[x][y] < val)//存最长的
a[x][y] = val;
}
for(int k = 1;k <= n;k++)
for(int i = 1;i <= n;i++)
if(a[i][k] != -1)
for(int j = 1;j <= n;j++)
if(a[k][j] != -1)
{
a[i][j] = min(a[i][j],a[i][k] + a[k][j]);
}
cout << a[1][n];
}
效率:203ms/4.53MB
4.Bellman-ford算法(这也好暴力,时间复杂度:O(MN))
思想:有n个节点,那么路径长度肯定小于n - 1,那么我们进行n - 1次松弛便能得到最短(长)路
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1.5e3 + 10,MAXM = 5e4 + 10,INF = 0x3f3f3f3f;
//构图
struct link{
int nextp,val;
link* nex;
}p[MAXM];
link *head[MAXN];
int ptop = 1;
void add(int x,int y,int val)
{
p[ptop].nex = head[x];
head[x] = &p[ptop];
p[ptop].nextp = y;
p[ptop].val = val;
ptop++;
}
int dis[MAXN] = {0};
void Bellman_ford(int n)
{
for(int i = 2;i <= n;i++)
{
dis[i] = INF;//不连通
}
for(int i = 1;i < n;i++)
{
for(int j = 1;j <= n;j++)
{
if(dis[j] == INF)
continue;
link* pp = head[j];
while(pp != NULL)
{
int y = pp -> nextp,val = pp -> val;
if(dis[y] == INF)
dis[y] = dis[j] + val;
else if(dis[y] < dis[j] + val)
dis[y] = dis[j] + val;
pp = pp -> nex;
}
}
}
}
int main()
{
int n,m;cin >> n >> m;
for(int i = 1;i <= m;i++)
{
int x,y,val;scanf("%d %d %d",&x,&y,&val);
add(x,y,val);
}
Bellman_ford(n);
if(dis[n] == INF)
cout << -1;
else
cout << dis[n];
}
效率:225ms/1.15MB
5.SPFA算法(本质为搜索,判断是否需要松弛)
思路:搜索队列中的点,找到相邻的点并更新与起点的距离,如果更新了,那么后面的点也可能要更新,所以加入队列。
步骤:
1.将起点加入队列
2.从队列中取一点,遍历其下一节点,如果需要更新,更新并加入队列
3.重复2步骤直到队列为空
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1.5e3 + 10,MAXM = 5e4 + 10,INF = 0x3f3f3f3f;
//构图
struct link{
int nextp,val;
link* nex;
}p[MAXM];
link *head[MAXN];
int ptop = 1;
void add(int x,int y,int val)
{
p[ptop].nex = head[x];
head[x] = &p[ptop];
p[ptop].nextp = y;
p[ptop].val = val;
ptop++;
}
int dis[MAXN] = {0};
void SPFA(int n)
{
for(int i = 2;i <= n;i++)//除起点距离均为INF
dis[i] = INF;
queue<int> qu;
qu.push(1);//起点入队
while(!qu.empty())
{
int x = qu.front();qu.pop();
link* pp = head[x];
if(dis[x] != INF)
while(pp != NULL)
{
int y = pp -> nextp,val = pp -> val;
if(dis[y] == INF || dis[y] < dis[x] + val)
{
qu.push(y);
dis[y] = dis[x] + val;
}
pp = pp -> nex;
}
}
}
int main()
{
int n,m;cin >> n >> m;
for(int i = 1;i <= m;i++)
{
int x,y,val;scanf("%d %d %d",&x,&y,&val);
add(x,y,val);
}
SPFA(n);
if(dis[n] == INF)
cout << -1;
else
cout << dis[n];
}
效率:73ms/1.16MB
不过这种算法非常容易被(坏心眼的)构造数据卡导致达到复杂度上界O(MN),这时我们就要对这种算法进行优化(玄学)
1.栈优化(劣化):就是把广度搜索改成深度搜索,在这题的效率为
172ms/1.16MB,属实劣化
2.堆优化:改队列为堆(优先队列),属实吓到我了
效率:458ms/1.16MB
然后我想了一下,优先队列默认为大根堆,所以适用于求最小,而这题要求最大,所以效率如此"惊人",如果改成小根堆(priority_queue <int,vector<int>,less<int>>)的话
效率:70ms/1.28MB(这就对了嘛)
来点正经的优化
3.SLF(Small Label First):直译小标签优先,就是拿要加入队列的节点与起点的距离和队列头相比,如果比它小就放在队首,否则放在队尾,很玄学,当然这是我们就要使用双端队列了(STL真好用)(这题是最大所以反过来)
部分代码:
void SPFA(int n)
{
for(int i = 2;i <= n;i++)//除起点距离均为INF
dis[i] = INF;
deque<int> qu;//双端队列
qu.push_front(1);//起点入队
while(!qu.empty())
{
int x = qu.front();qu.pop_front();
link* pp = head[x];
if(dis[x] != INF)
while(pp != NULL)
{
int y = pp -> nextp,val = pp -> val;
if(dis[y] == INF || dis[y] < dis[x] + val)
{
dis[y] = dis[x] + val;
if(qu.empty())//如果为空,直接放
qu.push_front(y);
else if(dis[qu.front()] < dis[y])//如果比较大,放队首
qu.push_front(y);
else//比较小放队尾
qu.push_back(y);
}
pp = pp -> nex;
}
}
}
效率:75ms/1.21MB
4.LLL(Large Label Last):直译大标签最后,但是不同于SLF,它是在从数列取数时,如果取出的数大于队列中的数的平均值则将它放在队列最后,直到取出的数小于队列中的数的平均值。
部分代码:
void SPFA(int n)
{
int sum = 0,num = 1;
for(int i = 2;i <= n;i++)//除起点距离均为INF
dis[i] = INF;
queue<int> qu;
qu.push(1);//起点入队
while(!qu.empty())
{
int x = qu.front();
while(dis[x] * num < sum)//核心 小于平均值
{
qu.pop();
qu.push(x);
x = qu.front();
}
qu.pop();
sum -= dis[x];
num--;
link* pp = head[x];
while(pp != NULL)
{
int y = pp -> nextp,val = pp -> val;
if(dis[y] == INF || dis[y] < dis[x] + val)
{
dis[y] = dis[x] + val;
qu.push(y);
sum += dis[y];
num++;
}
pp = pp -> nex;
}
}
}
效率:69ms/1.22MB
5.SLF + LLL:既然SLF和LLL一个针对入队,一个针对出队,那么结合一下也是可以的
部分代码:
void SPFA(int n)
{
for(int i = 2;i <= n;i++)//除起点距离均为INF
dis[i] = INF;
deque<int> qu;//双端队列
int sum = 0,num = 1;
qu.push_front(1);//起点入队
while(!qu.empty())
{
int x = qu.front();
while(dis[x] * num < sum)//核心 小于平均值
{
qu.pop_front();
qu.push_back(x);
x = qu.front();
}
qu.pop_front();
sum -= dis[x];
num--;
link* pp = head[x];
if(dis[x] != INF)
while(pp != NULL)
{
int y = pp -> nextp,val = pp -> val;
if(dis[y] == INF || dis[y] < dis[x] + val)
{
dis[y] = dis[x] + val;
sum += dis[y];
num++;
if(qu.empty())//如果为空,直接放
qu.push_front(y);
else if(dis[qu.front()] < dis[y])//如果比较大,放队首
qu.push_front(y);
else//比较小放队尾
qu.push_back(y);
}
pp = pp -> nex;
}
}
}