[OI] 关于最小环和负权环
最小环-Floyed
< \(O(N^{3})\) (\(N\)为节点数)>
关于Floyed能用于求解最小环的证明
对于一个环,我们总可以在环上找到任意三个点 \(i,j,k\) . 这三个点会把一个环分成三部分
我们设从 \(x\) 到 \(y\) 的最短距离为 \(dis[i][j]\),那么环的长度可以由这三部分的长度相加,即
显然,对于有向图,我们也有
这恰好就是Floyed的写法,而上述式子便是松弛方程.
但是有一点不太一样,在进行遍历时,一个点 \(k\) 可以进行松弛的点只能比他本身小,否则 \(dis[i][j]\) 将不是一个确定的值,且对于有向图来说,为了不重复遍历,其中一个点的值不应比另一个值要小,即假如有 \(i=2,j=3\) 那么接下来的遍历不应有 \(i=3,j=2\) .很显然,假如存在最小环,这样做纯粹在浪费时间.
另一点需要注意的是,这里可能代码里可能会出现 \(inf+inf+inf\) 的情况,所以 \(inf\) 的取值不宜太大.
代码实现
int Floyd(int n){
int mc=inf;
for(int k=0;k<=n;++k){
for(int i=0;i<k;++i){
for(int j=i+1;j<k;++j){
mc=min(mc,e[i][k]+e[k][j]+dis[i][j]);
}
}
for(int i=0; i<=n;++i){
for(int j=0;j<=n;++j){
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
}
}
}
return mc;
}
下面给出了存图和初始化的示例
for(int i=0;i<=n;++i){
for(int j=0;j<=n;++j){
if(i==j){
w[i][j]=dis[i][j]=0;
}
else{
w[i][j]=dis[i][j]=inf;
}
}
}
for(int i=0;i<m;++i){
cin>>u>>v>>y;
w[u][v]=dis[u][v]=y;
w[v][u]=dis[v][u]=y;
}
负权环-SPFA
< $ \Theta (kn,mn)$ (\(N\)为节点数,\(k\)为很小的常数,\(M\)为边数)>
关于SPFA
SPFA 我觉得和Dijkstra还挺像的 ,但是我背不过 ,所以简单写一下
很显然,只有上一次被松弛的结点,所连接的边,才有可能引起下一次的松弛操作。
那么我们用队列来维护「哪些结点可能会引起松弛操作」,就能只访问必要的边了。
SPFA的简单步骤大致如下 (\(Node\) 表示点的数量):
- 需要的变量 : 存图变量,\(dis[Node]\) <用于存储起点到各点的距离>,\(vis[Node]\) <用于记录节点是否在队列里> 和一个队列.
- 初始化 : \(dis\) 数组需要全部初始化为一个很大的值 (大于最大边权,小于1,073,741,823,否则两个数相加会超 \(int\) 范围变成负的,假如你用 \(memset\) ,推荐使用“\(0x3f\)”将其初始化为 \(0x3f3f3f3f\)) ,如果需要重复使用,建议初始化 \(vis\) .
- 起点的 \(dis\) 设为0, \(vis\)设为1,然后放入队列 .
- 随后,每次从队列中拿出一个值,将其 \(vis\) 修改为 0,对其所有的边进行松弛操作,如果成功松弛的边不在队列里 (\(vis\) 就是在这里用的),那么入队,\(vis\) 修改为1.
这里对SPFA和SPFA能用于判断负环的正确性不再说明.
SPFA判断负环的方法
假如我们有 \(n\) 个点,那么显然最短路经过的点没办法超过 \(n\) 个,假如超过 \(n\) 个了说明一定重复走了,所以存在负环
所以我们需要想办法记录当前最短路经过的节点数
我们只需要加上这么两句 (假设 \(i\) 刚刚松弛了 \(j\)):
cont[j]=cont[i]+1;
if(cont[j]=>n) return false;
其中 \(cont\) 是我们新开的数组,初始化为 0.
代码实现
bool spfa(int s){
for(int i=1;i<=n;++i){
dis[i]=1000000000;
vis[i]=false;
cont[i]=0;
}
dis[s]=0;
vis[s]=true;
q.push(s);
cont[s]++; //这别忘了
while(!q.empty()){
int u=q.front();
q.pop();
vis[u]=false;
for(edge ed:e[u]){
int to=ed.to,w=ed.w;
if(dis[to]>dis[u]+w){
dis[to]=dis[u]+w;
if(!vis[to]){
q.push(to);
cont[to]++; //注意,这一段一定要在if里面
if(cont[to]>=n){
return true; //有负权环
}
vis[to]=true;
}
}
}
}
return false;
}
例题 P2850 [USACO06DEC] Wormholes G
#include<bits/stdc++.h>
using namespace std;
struct edge{
int to,w;
};
vector<edge> e[501];
int dis[501],cont[501];
bool vis[501];
queue<int> q;
int f,n,m,w;
bool spfa(int s){
for(int i=1;i<=n;++i){
dis[i]=1000000000;
vis[i]=false;
cont[i]=0;
}
dis[s]=0;
vis[s]=true;
q.push(s);
cont[s]++;
while(!q.empty()){
int u=q.front();
q.pop();
vis[u]=false;
for(edge ed:e[u]){
int to=ed.to,w=ed.w;
if(dis[to]>dis[u]+w){
dis[to]=dis[u]+w;
if(!vis[to]){
q.push(to);
cont[to]++;
if(cont[to]>=n){
return false;
}
vis[to]=true;
}
}
}
}
return true;
}
int main(){
cin>>f;
for(int k=1;k<=f;++k){
for(int i=1;i<=500;++i){
e[i].clear();
}
cin>>n>>m>>w;
for(int i=1;i<=m;++i){
int x,y,z;
cin>>x>>y>>z;
e[x].push_back(edge{y,z});
e[y].push_back(edge{x,z});
}
for(int i=1;i<=w;++i){
int x,y,z;
cin>>x>>y>>z;
e[x].push_back(edge{y,-1*z});
}
bool flag=false;
for(int i=1;i<=n;++i){
if(spfa(i)==false){
cout<<"YES"<<endl;
flag=true;
break;
}
}
if(flag==false){
cout<<"NO"<<endl;
}
}
}
负权环-BellmanFord
< \(O(mn)\) (\(N\)为节点数,\(M\)为边数)>
当然,SPFA的代码不是很好背,但是我们可以用伟大的Bellman-Ford,这两种算法起码对菊花图来说一样快,思路十分简单:遍历 \(n-1\) 次每条边并松弛,如果还能松弛那么就有负权环 .
代码实现
bool bf(){
memset(dis,0x3f,sizeof(dis));
dis[1]=0;
for(int k=1;k<=n;++k){
for(edge i:e){ //把所有边扔到结构体vector e里
if(dis[i.to]>dis[i.start]+i.w){
dis[i.to]=dis[i.start]+i.w;
}
}
}
for(edge i:e){
if(dis[i.to]>dis[i.start]+i.w){
return true; //有负权环
}
}
return false;
}
例题(见上SPFA)
#include<bits/stdc++.h>
using namespace std;
struct edge{
int start,to,w;
};
vector<edge> e;
int dis[501];
int f,n,m,w;
bool bf(){
memset(dis,0x3f,sizeof(dis));
dis[1]=0;
for(int k=1;k<=n;++k){
for(edge i:e){
if(dis[i.to]>dis[i.start]+i.w){
dis[i.to]=dis[i.start]+i.w;
}
}
}
for(edge i:e){
if(dis[i.to]>dis[i.start]+i.w){
return true;
}
}
return false;
}
int main(){
cin>>f;
for(int k=1;k<=f;++k){
e.clear();
cin>>n>>m>>w;
for(int i=1;i<=m;++i){
int x,y,z;
cin>>x>>y>>z;
e.push_back(edge{x,y,z});
e.push_back(edge{y,x,z});
}
for(int i=1;i<=w;++i){
int x,y,z;
cin>>x>>y>>z;
e.push_back(edge{x,y,-z});
}
if(bf()==false){
cout<<"NO"<<endl;
}
else{
cout<<"YES"<<endl;
}
}
}