学习笔记:负环

上讲习题

AcWing 1144

有强制连边,先连上。因为本题是稀疏图,所以用 K r u s k a l Kruskal Kruskal更优。本题只有两种边权:1、纵向边的边权;2、横向边的边权。所以我们不需要排序,顺序已经出来了:先试纵向边,再试横向边。

#include<bits/stdc++.h>
using namespace std;
int n,m,fa[1000004];
int num(int x,int y)
{
    
    
    return (x-1)*m+y;
}
int find(int u)
{
    
    
    return fa[u]==0?u:fa[u]=find(fa[u]);
}
int main()
{
    
    
    scanf("%d%d",&n,&m);
    int x1,y1,x2,y2;
    while(scanf("%d%d%d%d",&x1,&y1,&x2,&y2)!=EOF)
    {
    
    
        int fx=find(num(x1,y1)),fy=find(num(x2,y2));
        if(fx!=fy)
            fa[fx]=fy;
    }
    int ans=0;
    for(int i=1;i<n;i++)
        for(int j=1;j<=m;j++)
        {
    
    
            int fx=find(num(i,j)),fy=find(num(i+1,j));
            if(fx!=fy)
            {
    
    
                fa[fx]=fy;
                ans++;
            }
        }
    for(int i=1;i<=n;i++)
        for(int j=1;j<m;j++)
        {
    
    
            int fx=find(num(i,j)),fy=find(num(i,j+1));
            if(fx!=fy)
            {
    
    
                fa[fx]=fy;
                ans+=2;
            }
        }
    printf("%d",ans);
    return 0;
}

AcWing 1145

先求出最小生成树,可以给边长最长的几条边建立卫星通信,一共有 k − 1 k-1 k1条边可以用卫星通信。本题是稠密图,用 P r i m Prim Prim。没有用卫星通信的最长的一条边就是 d i − k + 1 d_{i-k+1} dik+1 d d d是执行完 P r i m Prim Prim后从小到大排序的点权的数组。

#include<bits/stdc++.h>
using namespace std;
const int NN=504;
int x[NN],y[NN],d[NN];
bool vis[NN];
int sum(int q,int p)
{
    
    
	return (x[q]-x[p])*(x[q]-x[p])+(y[q]-y[p])*(y[q]-y[p]);
}
int main() 
{
    
    
	int n,k;
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;i++)
		scanf("%d%d",&x[i],&y[i]);
	memset(d,0x3f,sizeof(d));
	d[1]=0;
	for(int i=1;i<=n;i++)
	{
    
    
		int k=0;
		for(int j=1;j<=n;j++)
			if(!vis[j]&&d[j]<d[k])
				k=j;
		vis[k]=true;
		for(int j=1;j<=n;j++)
			if(!vis[j])
				d[j]=min(d[j],sum(k,j));
	}
	sort(d+1,d+1+n);
	printf("%.2lf",sqrt(d[n-k+1]));
	return 0;
}

AcWing 346

既然要求扩充成完全图,那么每两个点都会有边,所以在两个集合合并时这两个集合左右每个点都要有边。如果想要扩充成完全图时,唯一的最小生成树是这棵树,那么连接这两个集合的所有边都必须要比这条边多一。设 s i z i siz_i sizi表示连通块 i i i的点的个数,合并的两个集合是 a a a b b b,合并的边是 k k k,第 i i i条边的边权是 w i w_i wi,则需要新加的边权就是: ( s i z i × s i z j − 1 ) × ( w i + 1 ) (siz_i\times siz_j-1)\times (w_i+1) (sizi×sizj1)×(wi+1)减一的原因是最小生成树的边不是新加的边。最开始所有连通块内结点的个数为 1 1 1

#include<bits/stdc++.h>
using namespace std;
const int NN=6004;
struct node
{
    
    
    int u,v,w;
    bool operator<(const node&it)const
    {
    
    
        return w<it.w;
    }
}edge[NN];
int siz[NN],fa[NN];
int find(int x)
{
    
    
    return fa[x]==x?x:fa[x]=find(fa[x]);
}
int main()
{
    
    
    int t;
    scanf("%d",&t);
    while(t--)
    {
    
    
        int n;
        scanf("%d",&n);
        for(int i=1;i<n;i++)
        {
    
    
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            edge[i]={
    
    u,v,w};
        }
        for(int i=1;i<=n;i++)
        {
    
    
            fa[i]=i;
            siz[i]=1;
        }
        sort(edge+1,edge+n);
        int ans=0;
        for(int i=1;i<n;i++)
        {
    
    
            int fx=find(edge[i].u),fy=find(edge[i].v);
            if(fx!=fy)
            {
    
    
                ans+=(siz[fx]*siz[fy]-1)*(edge[i].w+1);
                siz[fy]+=siz[fx];
                fa[fx]=fy;
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}

概念

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

方法

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

解析和代码在下一篇博客——差分约束给出

猜你喜欢

转载自blog.csdn.net/weixin_44043668/article/details/109048198