原题: https://www.luogu.org/problemnew/show/P3385
题意:
n个点,m条边,判断是否存在负环。(输出YE5不是YES,N0不是NO)
解析:
这个题目真的是累,网上也没多少正确的题解,说的一些我都懂,但是时间复杂度不优秀。在洛谷和csdn上找了好久。真的是,洛谷时间复杂度前几的只有两种算法。第一种:百来行的建树做的,很优秀but看不懂;第二种:special judge,嗯,也很优秀。
注意细节: 题目要求环必须带上点1,所以初始时只有dis[1]=0,其他点设为inf。这样做松弛操作必须从点1开始。如果不要求带上某个点,则全部赋值为0。
dfs就是往dis更优的方向搜,如果搜会到起点就是一个环了。计算了复杂度,不行啊。
TLE dfs
#include<bits/stdc++.h>
using namespace std;
const int N=2010;
const int M=3010;
int n,m;
int head[N],nex[2*M],to[2*M],val[2*M],now;
void init(){
now=0;
memset(head,-1,sizeof head);
}
void add(int a,int b,int v){
nex[++now]=head[a],head[a]=now,to[now]=b,val[now]=v;
}
int dis[N];
bool vis[N];
bool flag;
void spfa(int p){
if(vis[p]){
flag=1;
return;
}
vis[p]=1;
for(int i=head[p];~i;i=nex[i]){
int u=to[i];
if(dis[u]>dis[p]+val[i]){
dis[u]=dis[p]+val[i];
if(u==1){flag=1;return;}
if(flag)return;
spfa(u);
if(flag)return;
}
}
vis[p]=0;
}
bool HaveNeg(){
memset(dis,0x3f,sizeof(dis));
dis[1]=0;
memset(vis,0,sizeof(vis));
flag=0;
spfa(1);
return flag;
}
int main(){
int t;scanf("%d",&t);
while(t--){
init();
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int a,b,v;scanf("%d%d%d",&a,&b,&v);
add(a,b,v);
if(v>=0)add(b,a,v);
}
if(HaveNeg())printf("YE5\n");
else printf("N0\n");
}
}
650ms 最常规spfa+bfs
常规思路,如果有一个负环,那么这个负环上的点的dis会被一直刷的更低。因为一个点正常情况下(无负环)一条持续更新的链顶多串N个点。那么当串N+1个点时,说明有负环(在更新别人后又更新回来了)。
主要来讲一讲这种写法的细节。
可以看到,首先只将1入队,dis[1]=0,其他为inf,那么效果怎么样呢?
起初我以为可以检测出带有1节点的负环,但是很明显,第三组案例不是。再看一下算法,每次取队列中的点,把后面的点拉进来,一进一出之时num数组慢慢增加。
所以算法正确性的前提为:每个联通块必须有一个点进队列,并且将此点的初始dis设为0。
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int maxn=198391;
struct Edge{
int u,v,w,nxt;
}edge[maxn<<1];
int fst[maxn],cnt;
inline void addedge(int u,int v,int w){
edge[++cnt].u=u,edge[cnt].v=v,edge[cnt].w=w,
edge[cnt].nxt=fst[u],fst[u]=cnt;
}
int N,M;
int dis[maxn];int ins[maxn];
int spfa(){
memset(dis,0x3f,sizeof(dis));
memset(ins,0,sizeof(ins));
queue<int>q;
q.push(1);
int num[maxn];
ins[1]=1;
num[1]=1;
dis[1]=0;
while(!q.empty()){
int u=q.front();q.pop();
ins[u]=0;
for(int i=fst[u];i;i=edge[i].nxt){
int v=edge[i].v;
if(dis[v]>dis[u]+edge[i].w){
dis[v]=dis[u]+edge[i].w;
num[v]=num[u]+1;
if(num[v]>N)return 1;
if(!ins[v]){
ins[v]=1;
q.push(v);
}
}
}
}
return 0;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
int T;cin>>T;
while(T--){int x,y,ww;
cin>>N>>M;
while(M--){
cin>>x>>y>>ww;
addedge(x,y,ww);
if(ww>=0)addedge(y,x,ww);
}
if(spfa())cout<<"YE5\n";
else cout<<"N0\n";
for(int i=1;i<=N;i++)fst[i]=0;
}
}
扒到一个看起来较舒服的写法,直接做n次再判断。
400ms bellam-ford加优化
这种算法得出的结果同上,算法正确性的前提为:每个联通块必须有一个点初始dis设为0。
这个算法,如果不要求某个点可以到达此负环,dis直接全为0即可。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2010;
const int M = 3010;
int a[M], b[M], c[M];
int dis[N];
void relax(int a, int b, int c) {
if(dis[a] != inf)
dis[b] = min(dis[b], dis[a] + c);
}
bool test1(int a, int b, int c) {
return (dis[b] > dis[a] + c);
}
bool test(int a, int b, int c) {
if(dis[a] == inf)
return 0;
if(c < 0)
return test1(a, b, c);
return test1(a, b, c) || test1(b, a, c);
}
bool solve() {
int n, m;
scanf("%d%d", &n, &m);
memset(dis, 0x3f, sizeof(int) * (n + 2));
dis[1] = 0;
for(int i = 1; i <= m; i++)
scanf("%d%d%d", &a[i], &b[i], &c[i]);
for(int r = 1; r < n; r++)
for(int i = 1; i <= m; i++) {
relax(a[i], b[i], c[i]);
if(c[i] >= 0)
relax(b[i], a[i], c[i]);
}
for(int i = 1; i <= m; i++)
if(test(a[i], b[i], c[i])) {
return 1;
}
return 0;
}
int main() {
int t;
cin >> t;
while(t--)
if(solve())//有负环
printf("YE5\n");
else
printf("N0\n");
return 0;
}
看不懂级别
68ms 神仙代码
#pragma GCC optimize(3)//手动Ox优化
#include<cstdio>
#include<cstring>
#include<queue>
#include<vector>
using namespace std;
/*
思路 :免得打到一半忘记了
1.建树 u->v 能松弛就建边 建树带上深度
2.删除不是最优的入队节点(该节点代表的树,该节点标记要加上) 新的节点由该节点重新延申
3.判断负环是看该树以下有么有该树的根节点 存在的话出现负环 ?????
4.开始 码 代码!
*/
const int N=2050,M=6050;
struct side // 边
{
int nxt,to,d;
} e[M];
struct tree // 树
{
int c,fa,dep; // 标记,父亲,深度
vector < int > son; // 儿子
}f[N];
int n,m,head[N],tot,dis[N];
inline void Add(int u,int v,int d)
{
e[++tot].nxt=head[u];head[u]=tot;e[tot].d=d;e[tot].to=v;
}
inline void Change(int u,int k)
{
f[u].c=k;
int l=f[u].son.size();
for(int i=0;i<l;i++) Change(f[u].son[i],k);
}
inline void Work(int u)
{
int l=f[u].son.size();
for(int i=0;i<l;i++)
{
f[f[u].son[i]].dep=f[u].dep+1;
Work(f[u].son[i]);
}
}
inline void Cut(int u)
{
int fa=f[u].fa;
int l=f[fa].son.size();
for(int i=0;i<l;i++)
{
if(u==f[fa].son[i])
{
for(int j=i;j<l-1;j++) f[fa].son[j]=f[fa].son[j+1];
f[fa].son.pop_back();
break;
}
}
f[u].fa=0; // 自由! !
}
inline void Link(int v,int u)
{
f[v].dep=f[u].dep+1;
f[v].fa=u;
Work(v);
f[u].son.push_back(v);
}
inline bool Lca(int u,int goal)
{
if(u==goal) return 1;
int l=f[u].son.size();
for(int i=0;i<l;i++)
{
if( Lca( f[u].son[i] , goal ) ) return 1;
}
return 0;
}
inline bool Spfa()
{
queue < int > q;q.push(1);
memset(dis,127/3,sizeof(dis));
dis[1]=0;f[1].c=f[1].dep=1;
while(!q.empty())
{
int u=q.front();q.pop();
if(!f[u].c) continue; //不存在潜力
for(int j=head[u];j;j=e[j].nxt)
{
int v=e[j].to,d=e[j].d;
if(dis[v]>dis[u]+d)
{
if(Lca(v,u)) return 1; // 在新儿子中找到自己就存在负环
dis[v]=dis[u]+d;
q.push(v);
Change(v,0);
f[v].c=1;
Cut(v);
Link(v,u);
}
}
}
return 0;
}
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
memset(head,0,sizeof(head));tot=0;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) // 初始化
{
f[i].son.clear();
f[i].fa=f[i].c=f[i].dep=0;
}
for(int i=1,u,v,d;i<=m;i++)
{
scanf("%d%d%d",&u,&v,&d);
if(d>=0) Add(u,v,d),Add(v,u,d);
else Add(u,v,d);
}
if(Spfa()) printf("YE5\n");
else printf("N0\n");
}
return 0;
}