# 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];
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);
if(shit==false)k[cnt-1].push_back({cnt,0});
shit=false;
lim=j+1;
}
}
if(shit==false){
++cnt;
st[cut(lim,len)].push_back(cnt);
if(shit==false)k[cnt-1].push_back({cnt,0});
}
}
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});
}
}
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:
不同网站读入可能不同
请读者注意
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;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 本地部署 DeepSeek:小白也能轻松搞定!
· 传国玉玺易主,ai.com竟然跳转到国产AI
· 自己如何在本地电脑从零搭建DeepSeek!手把手教学,快来看看! (建议收藏)
· 我们是如何解决abp身上的几个痛点
· 如何基于DeepSeek开展AI项目