SPFA–负环
一、模板
参考代码
/*
spfa 判负环
1、原理与bellman-ford一样 抽屉原理
2、dist[x]=dist[t]+w[i] cnt[x]=cnt[t]+1;记录从1到x的边数
若cnt[x]则有抽屉原理可以得出 出现了闭环 又因为有负权边则一定为负环
*/
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int N=2010,M=10010;
int n,m;
int h[N],e[M],ne[M],w[M],idx;
int dist[N],cnt[N];
bool st[N];
void add(int a,int b,int c){
e[idx]=b;
w[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
}
bool spfa(){
queue<int> q;
for(int i=1;i<=n;i++){
q.push(i);
st[i]=true;
}
while(q.size()){
int t=q.front();
q.pop();
st[t]=false;
for(int i=h[t];i!=-1;i=ne[i]){
int j=e[i];
if(dist[j]>(dist[t]+w[i])){
cnt[j]=cnt[t]+1;
dist[j]=dist[t]+w[i];
if(cnt[j]>=n){
return true;
}
if(!st[j]){
q.push(j);
st[j]=true;
}
}
}
}
return false;
}
int main(){
scanf("%d%d",&n,&m);
memset(h,-1,sizeof(h));
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
if(spfa()){
printf("Yes\n");
}
else{
printf("No\n");
}
return 0;
}
二、应用
题面:
思路:
由题意得有两种边,一种为边权为正的双向边,另一种为边权为负的单向边。约翰想要想要看到出发前的自己,则当图中存在负环时约翰一定可以回到过去,而不存在负环时则一定不能回到过去。故本题即为判断负环,若存在负环则输出yes 否则输出no。
参考代码:
#include<iostream>
#include<cstring>
#define int long long
using namespace std;
const int N = 510, M = 1e5 + 10;
int n, m1, m2;
int h[N], e[M], ne[M], w[M], idx;
int dist[N];
int q[N], cnt[N];
bool st[N];
void add(int a,int b, int c){
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx ++;
}
bool spfa(){
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 ++){
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 != -1; 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]){
q[tt ++ ] = j;
if(tt == N){
tt = 0;
}
st[j] = true;
}
}
}
}
return false;
}
signed main(){
int T;
scanf("%lld",&T);
while(T --){
scanf("%lld%lld%lld",&n, &m1, &m2);
memset(h, -1, sizeof(h));
idx = 0;
for(int i = 0; i < m1; i ++){
int a, b, c;
scanf("%lld%lld%lld",&a, &b, &c);
add(a, b, c);
add(b, a, c);
}
for(int i = 0; i < m2; i ++){
int a, b, c;
scanf("%lld%lld%lld",&a, &b, &c);
add(a, b, -c);
}
if(spfa()){
printf("YES\n");
}
else{
printf("NO\n");
}
}
}
01分数规划
在图论中形如 ∑ f i ∑ t i \frac{\sum{f_i}}{\sum{t_i}} ∑ti∑fi求最大值称为01分数规划问题,即有点权又有边权求最短路,则可以将点权放到边权中。
步骤: (1)、 二分定点mid (2)、重新定义边权。
∑ f i ∑ t i > m i d = ∑ f i − m i d ∗ ∑ t i > 0 = ∑ ( f i − m i d ∗ t i ) > 0 \frac{\sum{f_i}}{\sum{t_i}} > mid = \sum{f_i} - mid * \sum{t_i} > 0 = \sum{(f_i - mid * t_i)} > 0 ∑ti∑fi>mid=∑fi−mid∗∑ti>0=∑(fi−mid∗ti)>0
**题意:**给定一张 LL 个点、PP 条边的有向图,每个点都有一个权值 f[i]f[i],每条边都有一个权值 t[i]t[i]。求图中的一个环,使“环上各点的权值之和”除以“环上各边的权值之和”最大。输出这个最大值。注意:数据保证至少存在一个环。
**思路:**浮点数二分查找出答案mid, 若图中存在一个正环则 r = mid (因为是找最大值), 否则 l = mid。
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
typedef pair<int,int> PII;
const int N = 1e4 + 10;
vector<PII> g[N];
int n, m;
int wf[N];
int q[N];
int cnt[N];
int st[N];
double dist[N];
bool check(double mid){
memset(st, false, sizeof st);
memset(dist, 0, sizeof dist);
memset(cnt, 0, sizeof cnt);
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(auto son : g[t]){
int j = son.first;
if(dist[j] < (dist[t] + wf[t] - mid * son.second)){
dist[j] = dist[t] + wf[t] - mid * son.second;
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(){
scanf("%d%d",&n, &m);
for(int i = 1; i <= n; i ++){
scanf("%d",&wf[i]);
}
while(m --){
int a, b, c;
scanf("%d%d%d",&a, &b, &c);
g[a].push_back({b, c});
}
double l = 0, r = 1010;
while(r - l > 1e-4){
double mid = (l + r) / 2.0;
if(check(mid)){
l = mid;
}
else{
r = mid;
}
}
printf("%.2lf\n", r);
return 0;
}