acwing提高课正好看到负环,干脆写篇博客记录一下。
一、负环
定义:环中所有边权值和为负数的环,称为负环
负环可以有两种求法:bellman_ford和spfa
spfa是对bellman_ford的优化,一般是用spfa求负环
时间复杂度最好O(m),最坏O(km)
给出判负环的两种方法:
(1) 统计每个点入队次数,如果某个点入队n次,说明存在n条边的路径,那么必然有n+1个点,则存在负环
(2) 只要某个点最短路所含的边数大于等于n,说明至少n+1个点,则存在负环
注意: 负环不一定从起点走到,它可能在图中任意一个位置,一种解决办法是把所有点入队。
把所有点入队是显然的,相当于建立一个虚拟源点,直接在新图上找负环。
注:spfa 求负环时是可以忽略dist的初始值的
例题:P2850
spfa求负环模板题
#include<bits/stdc++.h>
using namespace std;
const int N = 1510,M = 5510;
int h[N], e[M], w[M], ne[M], idx;
int dist[N],cnt[N];
int q[N];
bool st[N];
int t,n,m,k;
void init()
{
memset(h, -1, sizeof h);
idx = 0;
memset(cnt,0,sizeof cnt);
// memset(dist,0,sizeof dist);
memset(st, 0, sizeof st);
}
void add(int a,int b,int c)
{
e[idx] = b,w[idx] = c,ne[idx] = h[a],h[a] = idx++;
}
bool spfa()
{
//这里使用的是数组模拟循环队列
//数组模拟队列一般会比STL快一些,有时候大概快一倍左右
int hh = 0,tt = 0;
for(int i = 1;i<=n;i++)
{
q[tt++] = i;
st[i] = true;
}
while(hh!=tt)
{
int t = q[hh++];
if(hh==N) hh = 0;
st[t] = false;
for(int i = h[t];~i;i = ne[i])
{
int j = e[i];
if(dist[j]>dist[t]+w[i])
{
dist[j] = dist[t]+w[i];
cnt[j] = cnt[t]+1;
if(cnt[j]>=n) return true;
if(!st[j])
{
st[j] = true;
q[tt++] = j;
if(tt==N) tt = 0;
}
}
}
}
return false;
}
int main()
{
cin>>t;
while(t--)
{
int a,b,c;
init();
cin>>n>>m>>k;
for(int i = 0;i<m;i++)
{
cin>>a>>b>>c;
add(a,b,c);
add(b,a,c);
}
for(int i = 0;i<k;i++)
{
cin>>a>>b>>c;
add(a,b,-c);
}
if(spfa())
{
cout<<"YES\n";
}
else
{
cout<<"NO\n";
}
}
return 0;
}
二、习题
图论也喜欢和其他知识点联系起来,给出一个练习题:
这是图论与01分数规划结合的好题,顺便给出01分数规划常规的思考流程:
凡是遇到
∑
f
i
g
i
\sum\frac{f_i}{g_i}
∑gifi的式子,一般考虑二分,然后转换式子,把式子与其他定理建立联系,比如上题:
另外注意一点:对于点和边都有权值的图,可以把点权放到边权上统一考虑,简化问题。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2e3+10,M = 1e4+10;
int h[N],e[M],ne[M],idx;
int wf[N],wt[M];
int n,m;
double dist[N];
int q[N],cnt[N];
bool st[N];
void add(int a, int b, int c) // 添加一条边a->b,边权为c
{
e[idx] = b, wt[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
bool check(double mid)
{
memset(dist,0,sizeof dist);
memset(st,0,sizeof st);
memset(cnt,0,sizeof cnt);
//循环队列
int hh = 0,tt = 0;
for(int i = 1;i<=n;i++)
{
st[i] = true;
q[tt++] = i;
}
while(hh!=tt)//循环队列要写成hh!=tt
{
int t = q[hh++];
if(hh==N) hh = 0;
st[t] = false;
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] < dist[t] + wf[t] - mid * wt[i])
{
dist[j] = dist[t] + wf[t] - mid * wt[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
if (!st[j])
{
q[tt ++ ] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
return false;
}
int main()
{
memset(h, -1, sizeof h);
cin>>n>>m;
for(int i = 1;i<=n;i++)
cin>>wf[i];
for(int i = 1;i<=m;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
double l = 0,r = 100000;
//一般来说保留两位小数即大于1e-4就行,精度一般高两位
while(r-l>1e-4)
{
double mid = (l+r)/2;
if(check(mid)) l = mid;
else r = mid;
}
printf("%.2lf",r);
return 0;
}