[OI] 关于最小环和负权环

最小环-Floyed

< \(O(N^{3})\) (\(N\)为节点数)>

关于Floyed能用于求解最小环的证明

对于一个环,我们总可以在环上找到任意三个点 \(i,j,k\) . 这三个点会把一个环分成三部分

我们设从 \(x\)\(y\) 的最短距离为 \(dis[i][j]\),那么环的长度可以由这三部分的长度相加,即

\[l=dis[i][j]+w[i][k]+w[j][k] \]

显然,对于有向图,我们也有

\[l=min(dis[j][i]+w[i][k]+w[k][j],dis[i][j]+w[j][k]+w[k][i]) \]

这恰好就是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\) 表示点的数量):

  1. 需要的变量 : 存图变量,\(dis[Node]\) <用于存储起点到各点的距离>,\(vis[Node]\) <用于记录节点是否在队列里> 和一个队列.
  2. 初始化 : \(dis\) 数组需要全部初始化为一个很大的值 (大于最大边权,小于1,073,741,823,否则两个数相加会超 \(int\) 范围变成负的,假如你用 \(memset\) ,推荐使用“\(0x3f\)”将其初始化为 \(0x3f3f3f3f\)) ,如果需要重复使用,建议初始化 \(vis\) .
  3. 起点的 \(dis\) 设为0, \(vis\)设为1,然后放入队列 .
  4. 随后,每次从队列中拿出一个值,将其 \(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;
		}
	}
}
posted @ 2024-02-08 21:11  HaneDaniko  阅读(36)  评论(0编辑  收藏  举报