学习笔记:负环

概念

负环,即指环上边权和为负数的环。在最短路中如果存在从源点能到的负环,最短路就是负无穷或者说不存在最短路。

方法

S P F A SPFA SPFA可以判断负环。如果某个点入队了 n n n次及以上,那么就有负环。其他的算法也是这样,只要某个点被更新了 n n n次以上就有负环。只有 d i j k s t r a dijkstra dijkstra不能判断负环。

例题

AcWing 904

想要回到起点时比出发时间早,那么只有出现了以下两种情况才可以:1、转了一圈回来比出发时间早;2、在某个地方有一个负环,在那里转了很多圈再回来。我们发现,其实第一种情况也是一个负环,所以问题就变成了判断负环。题目没有说连通,要枚举每个连通块。

#include<bits/stdc++.h>
using namespace std;
const int NN=504,MM=5204;
int head[NN],e[MM],ne[MM],w[MM],d[NN],cnt[NN],idx,n,m,k;
bool vis[NN],st[NN];
void add(int u,int v,int l)
{
    e[++idx]=v;
    ne[idx]=head[u];
    w[idx]=l;
    head[u]=idx;
}
bool spfa(int s)
{
    memset(d,0x3f,sizeof(d));
    memset(cnt,0,sizeof(cnt));
    memset(st,false,sizeof(st));
    queue<int>q;
    q.push(s);
    d[s]=0;
    while(q.size())
	{
        int t=q.front();
        vis[t]=true;
        st[t]=false;
        q.pop();
        for(int i=head[t];i;i=ne[i])
        {
            int v=e[i];
            if(d[v]>d[t]+w[i])
			{
                d[v]=d[t]+w[i];
                cnt[v]=cnt[t]+1;
                if(cnt[v]>=n)
				{
                    puts("YES");
                    return true;
                }
                if(!st[v])
				{
                    st[v]=true;
                    q.push(v);
                }
            }
        }
    }
    return false;
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
	{
        memset(vis,false,sizeof(vis));
        memset(head,0,sizeof(head));
        idx=0;
        scanf("%d%d%d",&n,&m,&k);
        for(int i=1;i<=m;i++)
		{
			int u,v,l;
            scanf("%d%d%d",&u,&v,&l);
            add(u,v,l);
            add(v,u,l);
        }
        for(int i=1;i<=k;i++)
		{
			int u,v,l;
            scanf("%d%d%d",&u,&v,&l);
            add(u,v,-l);
        }
        bool ok=false;
        for(int i=1;i<=n;i++)
            if(!vis[i]&&spfa(i))
            {
                ok=true;
                break;
            }
        if(!ok)
            puts("NO");
    }
    return 0;
}

AcWing 361

这个题是一个 a b \cfrac{a}{b} ba求最值的形式,可以用 01 01 01分数规划来求。设 E E E是边集, V V V是点集, ∣ X ∣ |X| X表示集合 X X X内的权值之和,则原式为 ∣ V ∣ ∣ E ∣ \cfrac{|V|}{|E|} EV。设一个值 g g g为原式的值,则有: h ( g ) = ∣ V ∣ − ∣ E ∣ × g h(g)=|V|-|E|\times g h(g)=VE×g得到了这个式子,思考如何二分出来这个最小的 g g g。如果最优方案下如果这个式子小于0,那么说明平均值太大了,要更小。如果这个式子大于零,那么就还可以更大。如果这个式子等于 0 0 0,那么就说明 g g g刚刚好。接下来思考这个式子在图中的最大值怎么算,先考虑建图。原式拆出来之后: h ( g ) = ∑ v ∈ V p i − ∑ e ∈ E w i × g h(g)=\displaystyle\sum_{v\in V}p_i-\displaystyle\sum_{e\in E}w_i\times g h(g)=vVpieEwi×g把边权代入原式,则每条边边权为 p i − w j × g p_i-w_j\times g piwj×g。如果能找到一个环权值和是正数,原式就大于 0 0 0,全都是负环就没有办法找到一个环使 h ( g ) h(g) h(g)大于 0 0 0。于是,求 c h e c k check check函数就是找正环,可以求最长路再用找负环的方法找正环。可以从任何地方出发找环,初始化每个点为 0 0 0,每个点最开始都入队。

#include<bits/stdc++.h>
using namespace std;
const int NN=1004;
vector<pair<int,int> >g[NN];
int p[NN],cnt[NN],n,m;
double d[NN];
bool st[NN];
bool check(double mid)
{
    memset(st,false,sizeof(st));
    memset(d,0,sizeof(d));
    memset(cnt,0,sizeof(cnt));
    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=0;i<g[t].size();i++)
        {
            int v=g[t][i].first,w=g[t][i].second;
            if(d[v]<d[t]+p[t]-mid*w)
            {
                d[v]=d[t]+p[t]-mid*w;
                cnt[v]=cnt[t]+1;
                if(cnt[v]>=n)
                    return true;
                if(!st[v])
                {
                    st[v]=true;
                    q.push(v);
                }
            }
        }
    }
    return false;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&p[i]);
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        g[u].push_back({v,w});
    }
    double l=0,r=1e9;
    while(r-l>1e-6)
    {
        double mid=l+(r-l)/2;
        if(check(mid))
            l=mid;
        else
            r=mid;
    }
    printf("%.2lf",l);
    return 0;
}

AcWing 1165

从一个字符串的前两个字符向最后两个字符连一条权为这个字符串长度的边,表示用这个字符串对平均值的贡献,一个路径就是一个接龙,一个环就是题目要求的。接下来思考解决这个题,本题又是 a b \cfrac{a}{b} ba的形式,可以 01 01 01分数规划。设 V V V为用的字符串的集合, ∣ X ∣ |X| X表示集合 X X X的大小, l e n X len_X lenX表示集合 X X X内所有元素的长度之和,则原式为 l e n V ∣ V ∣ \cfrac{len_V}{|V|} VlenV。思考如何二分,设 g g g表示原式的平均值,则: h ( g ) = l e n V − ∣ V ∣ × g h(g)=len_V-|V|\times g h(g)=lenVV×g如果 h ( g ) < 0 h(g)<0 h(g)<0就说明平均值太大了,反之就是小于或者等于答案。现在考虑如何算出这个式子的最大值,先拆成一条一条的边: h ( g ) = ∑ v ∈ V l e n { v } − ∑ v ∈ V 1 × g h(g)=\displaystyle\sum_{v\in V}len_{\{v\}}-\displaystyle\sum_{v\in V}1\times g h(g)=vVlen{v}vV1×g于是,每条边的边权就是 l e n { v } − g len_{\{v\}}-g len{v}g。建了图后,尝试对于题目要求计算这个式子的值。如果存在一个正环,那么就有使 h ( g ) h(g) h(g)大于 0 0 0的环,反之就没有。所以,计算这个式子的最值就是判断正环。因为本题数据范围较大, S P F A SPFA SPFA只有用 d f s dfs dfs优化才能过。而且, d f s dfs dfs优化对于判环有一个优势,那就是标记每个搜到点,如果一直能更新某个点,而且又绕回去了,就直接可以判断为有环,会快很多。

#include<bits/stdc++.h>
using namespace std;
const int NN=680;
vector<pair<int,int> >g[NN];
char s[1004];
double d[NN];
bool vis[NN];
int num(char a,char b)
{
	return 26*(a-'a')+(b-'a'+1);
}
bool spfa(int u,double mid)
{
	vis[u]=true;
	for(int i=0;i<g[u].size();i++)
	{
		int v=g[u][i].first,w=g[u][i].second;
		if(d[v]<d[u]+w-mid)
		{
			d[v]=d[u]+w-mid;
			if(vis[v]||spfa(v,mid))
			{
				vis[u]=false;
				return true;
			}
		}
	}
	vis[u]=false;
	return false;
}
bool chick(double mid)
{
	for(int i=1;i<=26*26;i++)
		if(spfa(i,mid))
			return true;
	return false;
}
int main()
{
	int n;
	while(scanf("%d",&n)&&n)
	{
		for(int i=1;i<=NN;i++)
		{
			d[i]=0;
			g[i].clear();
		}
		for(int i=1;i<=n;i++)
		{
			scanf("%s",s+1);
			int len=strlen(s+1);
			if(len<2)
				continue;
			g[num(s[1],s[2])].push_back({num(s[len-1],s[len]),len});
		}
		double l=0,r=1000;
		while(r-l>1e-5)
		{
			double mid=(l+r)/2;
			if(chick(mid))
				l=mid;
			else
				r=mid;
		}
		if(l!=0)
			printf("%.2lf\n",l);
		else
			puts("No solution");
	}
}
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值