最短路问题
# Part 01 Floyd
众所周知的水
可以求全源最短路 
板子简单好写

Code
for(int k=1;k<=n;k++)for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)
Distance[i][j]=min(Distance[i][j],Distance[i][k]+Distance[k][j])

很明显这个是可以直接用邻接矩阵的
Floyd O(n^3) 的时间复杂度
如果能过的话
开个 n^2 的矩阵是完全没有问题的 

但是这个 Floyd 毕竟是一个正经算法
这样水掉是不人道的 
为什么这样是对的呢
或者说循环的顺序为何不可以更改呢

Floyd 本意是一个类似 dp 的东西
用 dp[k][i][j] 表示以 [1,k] 中的点为中转点
从 i 走到 j 的最小代价
而转移则是 dp[k][i][j]=min(dp[k-1][i][j],dp[k-1][i][k]+dp[k-1][k][j])
直接压掉即可即为原式
至于为什么不会覆盖一些式子
作者表示 
I_Don't_Know.exe

 
Question 01 [luogu B3641 Floyd] 
模板
注意判重边 

Code
#include<bits/stdc++.h>
using namespace std;
const int N=1078;
int mp[N][N],n,m;
int main(){
	int l,r,w;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)mp[i][j]=10000000;
	for(int i=1;i<=n;i++)mp[i][i]=0;
	for(int i=1;i<=m;i++)scanf("%d%d%d",&l,&r,&w),mp[l][r]=min(mp[l][r],w),mp[r][l]=min(mp[r][l],w);
	for(int k=1;k<=n;k++)for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)mp[i][j]=min(mp[i][j],mp[i][k]+mp[k][j]);
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++)printf("%d ",mp[i][j]);
		puts("");
	}
	return 0;
}

Question 02 [luogu B3611 传递闭包] 
本题是 Floyd + Bitset 模板题
Bitset 其实就是一个的 Bool 数组 
显然本题求的是可达性而非最短路
于是可写出核心代码

bitset<N> lable[N];
for(int k=1;k<=n;k++)for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(lable[i][k])lable[i][j]|=lable[i][k]&lable[k][j]; 

然而这依然是 O(n^3) 的
考虑优化
如果 lable[i][k]true
那么原式就变成
for(int k=1;k<=n;k++)for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)lable[i][j]|=lable[k][j];
(一个条件与上 true 就相当于条件本身)
简化代码即为
for(int k=1;k<=n;k++)for(int j=1;j<=n;j++)if(lable[j][k])lable[j]|=lable[k];//bitset 特性

Code
#include<bits/stdc++.h>
using namespace std;
const int N=111;
int n;
bitset<N> mp[N];
int main(){
	int tmp;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)scanf("%d",&tmp),mp[i][j]=tmp;
	for(int k=1;k<=n;k++)for(int j=1;j<=n;j++)if(mp[j][k])mp[j]|=mp[k];//抽象循环顺序 
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++)tmp=mp[i][j],printf("%d ",tmp);
		puts("");
	}
	return 0;
}

# 02 Dijkstra
只能处理无负权的单源最短路问题
思路类似 BFS
将 BFS 的队列换成了堆
每次取堆顶进行 Shit 操作
Shit 操作即所谓的松弛
(作者觉得原来的名字不如暴戾语言通俗易懂)
其实本质就是 dp
转移如下 
Distance[r]=min(Distance[r]+length[l][r])

如果 Shit 成功更新答案就入队
否则就拉倒
每个元素只入堆一次 
听起来已经非常美好
但有一个问题

Data
第 0.0000057 s, Dis[5]6 的 value 被插入堆中 
第 0.0000998 s, Dis[5]5 的 value 被插入堆中 
第 0.0001024 s, Dis[5]4 的 value 被插入堆中 
第 0.0099999 s, Dis[5]3 的 value 被插入堆中 
第 0.0114514 s, Dis[5]2 的 value 被插入堆中 
第 0.1919810 s, Dis[5]1 的 value 被插入堆中 
 
本来,重复入堆也没什么
毕竟答案要 update
正确性也没问题
但是从小到大每一次都会给其出边 Shit 个底儿掉 
跑跑就 TLE 了
考虑优化
可以开一个标记数组
记录每一个元素是否曾作为堆顶存在 
如果该元素已被标记过就果断 continue

所以就结束了
但不知你可曾想过
为何他只能维护正权最短路
其实是因为
每个元素一旦已经到了堆顶 
就不会再变小了
因为显然他不会绕一大圈然后 Dis 反而变小了
但负权却存在这种情况
所以如此

好了
不废话了
看题

Question 01[ACP2004 信使]
模板题
最后求个最大值就好了 
memset 只能 set 十六进制 
不如 for 好用
 
Code 
#include<bits/stdc++.h>
using namespace std;
const int N=109;
struct line{int to,len;};
vector<line> k[N];
int n,m,Distance[N];
bool vis[N];
struct node{
	int id,key;
	bool operator <(const node &rhs)const{
		return key>rhs.key;
	}
};
priority_queue<node> q;
void Dijkstra(int start){
	q.push({start,0});
	Distance[start]=0;
	while(!q.empty()){
		node top=q.top();
		q.pop();
		if(vis[top.id])continue;
		vis[top.id]=true;
		for(auto it:k[top.id]){
			if(Distance[it.to]>Distance[top.id]+it.len){
				Distance[it.to]=Distance[top.id]+it.len;
				q.push({it.to,Distance[it.to]});
			}
		} 
	}
}
int main(){
	int l,r,len;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)Distance[i]=10000000;
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&l,&r,&len);
		k[l].push_back({r,len});
		k[r].push_back({l,len});
	}
	Dijkstra(1);
	int maxi=-1;
	for(int i=1;i<=n;i++)maxi=max(maxi,Distance[i]);
	printf("%d",maxi);
	return 0;
}

Question 02 [ACP2003 香甜的黄油]
正权全源最短路 
每个点跑一遍 Dijkstra 即可
只有 O(n^2 log n) 
但 SPFA 不建议这么做
原因大家都懂
极限 O(n^4)

Code
#include<bits/stdc++.h>
using namespace std;
const int N=888;
struct line{int to,len;};
vector<line> k[N];
int c,n,m,Distance[N][N],cow[N];
bool vis[N];
struct node{
	int id,key;
	bool operator <(const node &rhs)const{
		return key>rhs.key;
	}
};
void Dijkstra(int start){
	for(int i=1;i<=n;i++)vis[i]=0;
	Distance[start][start]=0;
	priority_queue<node> q;
	q.push({start,0});
	while(!q.empty()){
		node top=q.top();
		q.pop();
		if(vis[top.id])continue;
		vis[top.id]=true;
		for(auto i:k[top.id]){
			if(Distance[start][i.to]>Distance[start][top.id]+i.len){
				Distance[start][i.to]=Distance[start][top.id]+i.len;
				q.push({i.to,Distance[start][i.to]});
			}
		} 
	}
}
int main(){
	int tmp,l,r,len;
	scanf("%d%d%d",&c,&n,&m);
	for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)Distance[i][j]=1919810;
	for(int i=1;i<=c;i++)scanf("%d",&tmp),cow[tmp]++;
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&l,&r,&len);
		k[l].push_back({r,len});
		k[r].push_back({l,len});
	}
	for(int i=1;i<=n;i++)Dijkstra(i);
	long long mini=100000000000;
	for(int i=1;i<=n;i++){
		long long sum=0;
		for(int j=1;j<=n;j++)sum+=cow[j]*Distance[j][i];
		mini=min(sum,mini);
	}
	printf("%lld",mini);
	return 0;
}

Question 03 [ACP2002 最小花费]
记录从 i 到 j 的因子
跑最长路
加变成乘即可 

Code
#include<bits/stdc++.h>
using namespace std;
const int N=2009; 
struct line{int to;double len;};
int n,m;
vector<line> k[N];
inline double solve(double k){return 1.00-k/100.00;}
double Distance[N];
bool vis[N];
struct node{
	int id;
	double key;
	bool operator <(const node &rhs)const{
		return key<rhs.key;
	} 
};
priority_queue<node> q;
void Dijkstra(int start){
	Distance[start]=1.0;
	q.push({start,Distance[start]});
	while(!q.empty()){
		node top=q.top();
		q.pop();
		if(vis[top.id])continue; 
		vis[top.id]=true;
		for(auto it:k[top.id]){
			if(Distance[it.to]<Distance[top.id]*it.len){
				Distance[it.to]=Distance[top.id]*it.len;
				q.push({it.to,Distance[it.to]});
			}
		}
	}
}
int main(){
	int l,r,len;
	scanf("%d%d",&n,&m); 
	for(int i=1;i<=n;i++)Distance[i]=0.0;
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&l,&r,&len);
		k[l].push_back({r,solve(len)});
		k[r].push_back({l,solve(len)});
	}
	scanf("%d%d",&l,&r);
	Dijkstra(l);
	printf("%.8lf",100.00/Distance[r]);
	return 0; 
}

Question 04[ACP2005 最优乘车]
分层图 + Dijkstra 模板
在每辆车的路线上存边权为 0 的单向图
并在同一个位置的所有 id 互相之间连一条边权为 1 的无向边
最终从每一个 id 为 1 的位置跑 Dijkstra
看到 id 为 n 的节点最小 Distance 即为答案

Code 
#include<bits/stdc++.h>
using namespace std;
const int N=9999; 
int n,m,Distance[N],cnt=0;
bool vis[N];
struct line{int to,len;};
vector<line> k[N];
vector<int> st[N];
struct node{
	int id,key;
	bool operator <(const node &rhs)const{
		return key>rhs.key;
	}
};
priority_queue<node> q;
void Dijkstra(int start){
	for(int i=1;i<=cnt;i++)Distance[i]=1008960,vis[i]=false;
	Distance[start]=0;
	q.push({start,Distance[start]});
	while(!q.empty()){
		node top=q.top();
		q.pop();
		if(vis[top.id])continue; 
		vis[top.id]=true;
		for(auto it:k[top.id]){
			if(Distance[it.to]>Distance[top.id]+it.len){
				Distance[it.to]=Distance[top.id]+it.len;
				q.push({it.to,Distance[it.to]});
			}
		}
	}
}
int ans=1096456;
char tmp[100900];
int cut(int l,int r){
	int ans=0;
	for(int i=l;i<=r;i++)ans=ans*10+tmp[i]-'0';
	return ans;
}
int main(){
	int lim,len;
	bool shit;
	scanf("%d%d",&m,&n);
	getchar();
	for(int i=1;i<=m;i++){
		cin.getline(tmp,100000);
		lim=0,len=strlen(tmp)-1,shit=true;
		for(int j=0;j<=len;j++){
			if(!isdigit(tmp[j])){
				++cnt;
				st[cut(lim,j-1)].push_back(cnt);
				//printf("%d[%d,%d] cut! id: %d\n",cut(lim,j-1),lim,j-1,cnt);
				if(shit==false)k[cnt-1].push_back({cnt,0})/*,printf("Link %d -> %d\n",cnt-1,cnt)*/;
				shit=false;
				lim=j+1;
			}
		}
		if(shit==false){
			++cnt;
			//printf("%d[%d,%d] cut! id: %d\n",cut(lim,len),lim,len,cnt);
			st[cut(lim,len)].push_back(cnt);
			if(shit==false)k[cnt-1].push_back({cnt,0})/*,printf("Link %d -> %d\n",cnt-1,cnt)*/;
		}
	}
	for(int i=1;i<=n;i++){
		if(st[i].empty())continue;
		for(int j=0;j<st[i].size()-1;j++)for(int it=j+1;it<st[i].size();it++){
			k[st[i][j]].push_back({st[i][it],1}),k[st[i][it]].push_back({st[i][j],1});
//			printf("(%d) %d -$> %d\n",i,st[i][it],st[i][j]);
//			printf("(%d) %d -$> %d\n",i,st[i][j],st[i][it]);
		}
	}
	for(int i=0;i<st[1].size();i++){
		Dijkstra(st[1][i]);
		for(int j=0;j<st[n].size();j++)ans=min(ans,Distance[st[n][j]]);
	}
	if(ans<10000)
	printf("%d",ans);
	else puts("NO");
	return 0; 
}

# 03 SPFA
众所周知的负权单源最短路算法
理论复杂度 O(n log n)
卡后复杂度 O(n * n * n)
所以: 十年 OI 一场空,正权 SPFA 见祖宗
However
如果有负权一定要用
因为正经出题人有负权不会卡 SPFA

经典咏流传
NOI Day1 T1 归程
关于SPFA
他死了
(但我此生还能打上 NOI 吗)

所以算法思想是什么呢
显然上一节我们已经讨论过
Dijkstra 对于负权最短路的 Bug
对于一个负权图
Dijkstra 的 priority_queue 不能保证一次入堆即为最小
所以 SPFA 予以了改进
一次入堆不行?
那就多次!

显然多次入堆之后
排序已经没有意义
所以我们把堆删掉
保留 Shit 操作
每次用所有的点把其出边 Shit 一次
直到所有点不能 Shit 就退出
根据我们一会用到的玄学
最多 Shit n-1 轮就可解决
所以这是一个 O(n * n)的算法
还可以根据 Shit 轮数判负环
功能很强大

但知道的朋友就会知道
这个玩意不是 Bellman-Ford 吗

是的
而 SPFA 就是对 Bellman-Ford 算法的队列优化
SPFA 的思想是
只用被 Shit 过的点进行 Shit 操作
而将 Shit 过的点存入队列

OK
但相同的问题又来了

Data
第 0.0000057 s, Dis[5]6 的 value 被插入队列中 
第 0.0000998 s, Dis[5]5 的 value 被插入队列中 
第 0.0001024 s, Dis[5]4 的 value 被插入队列中 
第 0.0099999 s, Dis[5]3 的 value 被插入队列中 
第 0.0114514 s, Dis[5]2 的 value 被插入队列中 
第 0.1919810 s, Dis[5]1 的 value 被插入队列中 
......1.0000057 s, Dis[5]6 的 value 被用于 Shit 
第 1.0000998 s, Dis[5]5 的 value 被用于 Shit
第 1.0001024 s, Dis[5]4 的 value 被用于 Shit
第 1.0099999 s, Dis[5]3 的 value 被用于 Shit
第 1.0114514 s, Dis[5]2 的 value 被用于 Shit
第 1.1919810 s, Dis[5]1 的 value 被用于 Shit
......
TLE #10 -1pts
TLE #11 -1pts
TLE #12 -1pts
TLE #13 -1pts
TLE #14 -1pts
......

是的,Shit 操作只有至多 N 轮
但一个元素可能多次入队
然后让机房里充满快活的空气
让时间复杂度充满 Shit 的气息

我们沿用相同的思想
开一个标记数组 InQueue 记录每个元素是否在队列里
同时把队列变成 int 类型的只负责记录下标
此时 Distance 同样可以改
只不过不需要重复插入了
解决!

其实该算法同样可以判负环
显然如果一个图里有负权回路
那么显然最优方案是在回路里走上 inf 圈在到终点
所以我们只需要判断一旦一个点入队列次数超过一个阈值
那么就一定代表图里有负环
这个阈值根据玄学设成 n-1
但实际使用时除非要卡常
还是建议设为 n, n+1 甚至更大
(As the saying goes,鸡蛋不能放到一套煎饼里)

OK now,
3,2,1,
上例题!

Question 01 [P3371 单源最短路(easy)]
模板题
注意本题随机数据
正常还是要用 Dijkstra 写!
看讨论区的 WA on #3  JC 案例
初始将 INF 设为 INT_MAX 即可

Code
#include<bits/stdc++.h>
using namespace std;
const int N=500989;
int n,m,start_pos,Distance[N];
bool InQueue[N];
struct node{int to,len;};
vector<node> line[N];
queue<int> q;
void SPFA(int start){
	InQueue[start]=true,q.push(start);
	Distance[start]=0;
	while(!q.empty()){
		int top=q.front();
		q.pop(),InQueue[top]=false;
		for(auto it:line[top]){
			if(Distance[it.to]>Distance[top]+it.len){
				Distance[it.to]=Distance[top]+it.len;
				q.push(it.to);
			}
		}
	}
}
int main(){
	int l,r,len;
	scanf("%d%d%d",&n,&m,&start_pos);
	for(int i=1;i<=n;i++)Distance[i]=INT_MAX;
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&l,&r,&len);
		line[l].push_back({r,len});
	}
	SPFA(start_pos);
	for(int i=1;i<=n;i++)printf("%d ",Distance[i]);
	return 0;
}

Question 02 [ACP2111 最优贸易] [P1073 最优贸易]
分层图 + SPFA 模板题
考虑如何处理贸易
发现可以建一个三层的图
第一层表示未买
第二层表示买了未卖
第三层表示卖了
各层内正常连边权为 0 的边
一二层间连边权为 prize 的边
二三层间连边权为 -prize 的边
(代表走完最少亏多少)1 开始跑 SPFA 即可

作者注:
	文中所放代码均为 AC 无删减代码
	由于 AcCoders 数据较弱
	如果作者能够找到都会使用 luogu 进行验证
	本题 AC record Link https://www.luogu.com.cn/record/199792288
	不同网站读入可能不同
	请读者注意
	
Code
#include<bits/stdc++.h>
using namespace std;
const int N=300096;
int n,m;
long long Distance[N];
bool InQueue[N];
struct node{int to;long long len;};
vector<node> line[N];
queue<int> q;
void SPFA(int start){
	InQueue[start]=true,q.push(start);
	Distance[start]=0;
	while(!q.empty()){
		int top=q.front();
		q.pop(),InQueue[top]=false;
		for(auto it:line[top]){
			if(Distance[it.to]>Distance[top]+it.len){
				Distance[it.to]=Distance[top]+it.len;
				q.push(it.to);
			}
		}
	}
}
int main(){
	int edge_type,l,r;
	long long tmp;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)scanf("%lld",&tmp),line[i].push_back({i+n,tmp}),line[i+n].push_back({i+2*n,-tmp});
	for(int i=1;i<=n*3;i++)Distance[i]=10000000000;
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&l,&r,&edge_type);
		if(edge_type==1){
			line[l].push_back({r,0});
			line[l+n].push_back({r+n,0});
			line[l+2*n].push_back({r+2*n,0});
		}else{
			line[l].push_back({r,0}),line[r].push_back({l,0});
			line[l+n].push_back({r+n,0}),line[r+n].push_back({l+n,0});
			line[l+2*n].push_back({r+2*n,0}),line[r+2*n].push_back({l+2*n,0});
		}
	}
	SPFA(1);
	printf("%lld",-Distance[n*3]);
	return 0;
}
posted on   2025ing  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 本地部署 DeepSeek:小白也能轻松搞定!
· 传国玉玺易主,ai.com竟然跳转到国产AI
· 自己如何在本地电脑从零搭建DeepSeek!手把手教学,快来看看! (建议收藏)
· 我们是如何解决abp身上的几个痛点
· 如何基于DeepSeek开展AI项目
点击右上角即可分享
微信分享提示