Nameless Nervu 题解

Nameless Nervu 题解

U224669 Nameless Nervu

#5185 YAOI Round #25 (Div.1) C. Nameless Nervu

题意分析

(预先说明:本文所述的不等式组均满足题目给定的条件,即对于 \(a_u - a_v \le w\) 保证不出现 \(u=v\)\(a_u - a_v \le w_1,a_v - a_u \le w_2\) 的情况,且 \(u_i,v_i\) 不重复。对于整个不等式组,保证是一个完整的、任意两点不独立的不等式组。)

给定一个不等式组,删除一个不等式将其分成两个互不相关的,且两者都有解。

思路

判断一个不等式组是否有解,可以想到差分约束系统。因此这个问题需要转换为图论问题进行求解。

将一个不等式组分为两个不等式组,相当于把一个图分成两个,因此删掉的不等式就是图的桥。

因此,很容易想到先跑一遍 tarjan 求出所有割边,再跑 SPFA 计算是否有解。

优化

为了进一步优化,我们需要先简单证明关于不等式组的两点。

  1. 对于两个独立且有解的不等式组,将它们用任意一个不等式连起来,组合成的不等式组也一定有解。

    证明:我们知道将一个有解的不等式组中所有数同时加或减任意一个数,不等式仍然有解。因此,只需要对这两个独立且有解的不等式组各自进行同加减的操作,就可以满足组合成的不等式组也有解。

    因此,对于一个有解的不等式组,删除一个条件使其变为两个独立的不等式组,这两者一定也有解。

  2. 对于一个无解的不等式组,删除一个不等式使其变为两个独立的不等式组,这两者至少有一个无解。

    证明:不等式无解只在其中出现负环时发生。如果删除的桥破坏了负环,则说明桥必定在一个环上,此时环上的节点在桥的两边。因此环需要经过这个桥两次,而题目规定不会出现这种情况(保证不出现 \(a_u - a_v \le w_1,a_v - a_u \le w_2\))。因此破坏桥不可能影响到负环,不等式仍然无解。

    因此,对于一个无解的不等式组,删除一个不等式使其变为两个独立的不等式组,这两者至少有一个无解。

有了这两点,我们可以发现如果不等式原本就无解,就不必再计算每个桥了,可以直接结束程序、判断无法做到。而如果原不等式有解,答案便是这个图所有的桥。

细节

边的问题

数组首先需要开到两倍的 \(n\),因为在跑 tarjan 时需要连双向的边(变为无向图)。

但是差分约束系统需要的不等式是有向边,因此我们在读入时先建一条边,再在跑 tarjan 之前连另一条。而差分约束时连的 \(n\) 条超级原点的边,我们可以在连反向边之前将边数 \(-n\),进行删除。

在割边时候我们需要记录边的编号,原有的边为 \(1 - m\) 由于反向边是后来加的,编号是 \(m+1 - 2m\)。割边不一定割的是哪条边,我们便需要判断编号是否大于 \(m\)。实现如 i>m?i-m:i

(不过也可以建两个图)

顺序输出

sort 一下很方便,但是在第五个测试点会被卡,因此还需要写个桶排。

正解

#include<bits/stdc++.h>
using namespace std;

inline int read(){
    int w=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){
        if(ch=='-')f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        w=w*10+ch-48;
        ch=getchar();
    }
    return w*f;
}//读入优化,但是不加也勉强可以过 

#define MAXN (int)1e6

int n,m;

struct edge{
    int u,v,w,nxt;//为了连反边,需要储存下起始点 
}e[MAXN*2];
int head[MAXN],ecnt=0;
void build(int u,int v,int w){
    e[++ecnt].v=v,e[ecnt].w=w,e[ecnt].u=u;
    e[ecnt].nxt=head[u],head[u]=ecnt;
}//建图用 

int dfn[MAXN],low[MAXN],fa[MAXN],dcnt;
int br[MAXN],bcnt=0;//割边用 

int dis[MAXN],vis[MAXN],c[MAXN];//差分约束用 

int t[MAXN];//桶排用 

void tarjan(int u){
    dfn[u]=low[u]=++dcnt;
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].v;
          if(!dfn[v]){
              fa[v]=u;
              tarjan(v);
              low[u]=min(low[u],low[v]);
              if(low[v]>dfn[u])br[++bcnt]=(i>m?i-m:i);//注意算出正确的编号 
        }else if(fa[u]!=v)low[u]=min(low[u],dfn[v]);
    }
}

bool spfa(){
    queue<int> q;
    for(int i=1;i<=n;i++)build(0,i,0);//n 条超级原点的边在这里 
    memset(dis,-2e9,sizeof(dis));
    dis[0]=0,vis[0]=1;
    q.push(0);
    while(!q.empty()){
        int u=q.front();
        q.pop();
        vis[u]=0;
        for(int i=head[u];i;i=e[i].nxt){
            int v=e[i].v;
            if(dis[v]>=dis[u]+e[i].w){
                dis[v]=dis[u]+e[i].w;
                if(!vis[v]){
                    vis[v]=1,++c[v];
                    if(c[v]==n)return 0;
                    q.push(v);
                }
            }
        }
    }
    return 1;
}

int main(){
    n=read(),m=read();
    for(int i=1;i<=m;i++){
        int u=read(),v=read(),w=read();
        build(v,u,w);//先连差分约束需要的边 
    }
    if(!spfa()){
        puts("-1");
        return 0;
    }//如果无解结束程序 
    ecnt-=n;//删除超级原点的边
    for(int i=1;i<=m;i++)build(e[i].v,e[i].u,0);//连反边 
    tarjan(1);//由于保证图连通,只需要跑一遍即可 
    if(!bcnt){
        puts("-1");
        return 0;
    }//如果无割边结束程序 
    for(int i=1;i<=bcnt;i++)t[br[i]]=1;
    for(int i=1;i<=m;i++)if(t[i])printf("%d ",i);//桶排,输出 
    return 0;
}
posted @ 2022-03-22 14:53  ofbwyx  阅读(70)  评论(0)    收藏  举报