题解 P2783 【有机化学之神偶尔会做作弊】

· · 题解

前言

[WA]10次之后,我终于过了这道题……

读完题后思路就已经很明朗了:

  1. 找环,缩点
  2. 利用LCA寻找两点之间碳的个数

但是……找环的过程?由于这是个无向图,所以是不存在强连通分量的。强连通分量是有向图中的定义。对于无向图只有双连通分量。而双连通分量又分两种:

从题目描述来看,本题需要把所有的碳环全部缩成一个点。但是由于我对双连通分量的缩点不熟,于是我专门去查看了资料:点-双连通分量的缩点是基于割点的,而边-双连通分量是基于的。

问题来了:是写 点-双连通分量 还是 边-双连通分量 呢?仔细查看定义后,我发现:点-双连通分量 缩点后的结构是不符合本题的要求的。因为它缩点后是会把割点看作单独的一个点,而本题是要求把所有的简单环变成一个点,此时这显然是不对的。

至于题解里的各位大佬写的“点-双连通分量”,要么是我学艺不精,没有正确get 点-双连通分量 的缩点的真正定义,要么就是那个不能叫 点-双连通分量 的缩点。不管怎么说,我放弃了写 点-双连通分量 的想法。

P.S.

这里仅仅是发表了个人的观点,如有异议欢迎在讨论区指教。个人感觉应该是我理解错了,因为那个感觉是强行把无向图写成了有向图的强连通分量,这当然是正确的,而且十分巧妙,只是说法可能不一样。如有冒犯,请多多包涵。(瑟瑟发抖)

经过一番深思熟虑,我决定写 边-双连通分量。边-双连通分量 的求解方法如下:

  1. 找到该图中的所有的桥(删去后使得原图不连通的边)
  2. 把桥删掉,此时图中的每一个连通的部分组成一个 边-双连通分量。

然而这种做法有一个巨大的bug摆在面前:两点间的重边只能算一条边。而在桥的定义中,一旦有重边的存在,这些重边就都不能叫做桥了。但是在本题中,这是不对的。

解决的办法:去掉重边即可

重边怎么去掉呢?我想到了三种思路:(我因为太菜了不会写vector

  1. 若用邻接表存图,则可利用哈希表的方式,在添加边之前先看有没有这条边存在。但是时间复杂度为O(n^{2}),超时。
  2. 若采用邻接矩阵存图,则可以O(1)去掉重边,但是空间无法承受。
  3. 采用双链表存图,并且把所有的边复制一遍,按照按照起点优先,终点其次,编号最后的关键字排序,然后O(m)判重。总时间复杂度:O(m\cdot log_{2}m),可行。

    注意,一定要按照起点,终点,编号的关键字,这样才能保证相同的边相邻。至于为什么要在最后加一个编号,是因为一条无向边是当作两条有向边存的,如果编号从2开始,则一条边 e 的对应边为 e xor 1 。必须要在最后的关键字中加入编号,才能保证前后属于一条无向边的两条有向边是一一对应的。比如前面有两个 3 -> 10 ,编号为 69 ;后面有两个 10 -> 3 ,编号为 87 ,没有一 一对应,去重后就会出错。(这个错我查了一下午,心酸)

    至于去重,直接用双链表就可以了。

    接下来是缩点。去掉桥后,每一个连通块里的所有点标记上同一个颜色,然后查看所有边,把两条边的两点的颜色连起来重新建图。缩完点后图就变成了一棵树,随便指定一个点为根,跑LCA就行了。我用的是倍增,复杂度为O(n\cdot log_{2}n)

最后统计出答案后转为二进制即可,总时间复杂度O(m\cdot log_{2}m+tot\cdot n\cdot log_{2}n)

```cpp #include<bits/stdc++.h> using namespace std; inline void read(int &x)//读入优化 { int w=x=0; char ch=0; while(ch<'0'||'9'<ch) w|=(ch=='-'),ch=getchar(); while('0'<=ch&&ch<='9') x=(x<<3)+(x<<1)+(ch^'0'),ch=getchar(); x=w?-x:x; } const int N=1e5,M=5e5;//怒开10倍 //<吐槽>: //感觉最后一个点超出了题目描述的数据范围 struct Edge { int u,v; }E[M<<1|1],e[M<<1|1]; struct node { int no,u,v; }cop[M<<1|1]; queue<int> q; int last=1,fst[N+1],nxt[M<<1|1],bef[M<<1|1];//原图 int num,head[N+1],to[M<<1|1];//缩点图 int n,m,dep,dfn[N+1],low[N+1];//tarjan int tot,cnt,typ[N+1]; //缩点 bool flag[M<<1|1],bri[M<<1|1],vis[N+1],rvis[N+1];//重边 , 桥 , 缩点 int lg2[N+1],as[25][N+1],depth[N+1];//lca void add(int x,int y) { E[++last]=(Edge){x,y}; bef[fst[x]]=last,nxt[last]=fst[x],fst[x]=last;//使用双链表便于删边 cop[last]=(node){last,x,y}; } bool operator<(const node &x,const node &y)//重载运算符以便于排序删边 { if(x.u!=y.u) return x.u<y.u; if(x.v!=y.v) return x.v<y.v; return x.no<y.no; } void del(int x)//利用双链表删边 { flag[x]=1; if(bef[x]) nxt[bef[x]]=nxt[x]; else { int y=E[x].u; fst[y]=nxt[x]; } } void tarjan(int x,int e)//找桥 { dfn[x]=low[x]=++dep; for(int i=fst[x];i;i=nxt[i]) { int y=E[i].v; if(i==(e^1)) continue; if(!dfn[y]) { tarjan(y,i); low[x]=min(low[x],low[y]); if(low[y]>dfn[x]) bri[i]=bri[i^1]=1;//桥的判定法则 } else low[x]=min(low[x],dfn[y]); } } void run(int x)//遍历找连通块 { vis[x]=1; q.push(x); for(int i=fst[x];i;i=nxt[i]) { int y=E[i].v; if(!vis[y]&&!bri[i]) run(y); } } void radd(int x,int y)//重新建图 { e[++num]=(Edge){x,y}; to[num]=head[x],head[x]=num; } void dfs(int x,int fa)//lca预处理 { rvis[x]=1,depth[x]=depth[fa]+1,as[0][x]=fa; for(int i=1;i<=lg2[depth[x]]+1;++i) as[i][x]=as[i-1][as[i-1][x]]; for(int i=head[x];i;i=to[i]) { int y=e[i].v; if(!rvis[y]&&y!=fa) dfs(y,x); } } int lca(int x,int y)//倍增求lca { if(depth[x]<depth[y]) swap(x,y); for(int i=lg2[depth[x]]+1;i>=0;--i) { if(depth[as[i][x]]>=depth[y]) x=as[i][x]; if(x==y) return x; } for(int i=lg2[depth[x]]+1;i>=0;--i) if(as[i][x]!=as[i][y]) x=as[i][x],y=as[i][y]; return as[0][x]; } void print(int x) { if(x==0) { printf("0"); return; } if(x==1) { printf("1"); return; } print(x>>1); printf("%d",x%2); } int main() { read(n),read(m); for(int i=1;i<=m;++i) { int a,b; read(a),read(b); if(a!=b) add(a,b),add(b,a); } sort(cop+2,cop+last+1);//排序去重边 for(int i=3;i<=last;++i) if(cop[i].u==cop[i-1].u&&cop[i].v==cop[i-1].v) del(cop[i].no); for(int i=1;i<=n;++i) if(!dfn[i]) tarjan(i,1); for(int i=1;i<=n;++i) if(!vis[i]) { run(i); ++cnt; while(!q.empty())//利用队列存储连通块 { typ[q.front()]=cnt; q.pop(); } } for(int i=2;i<=last;++i) { if(flag[i]) continue;//不要忘了去掉重边 int a=E[i].u,b=E[i].v; a=typ[a],b=typ[b]; if(a!=b) radd(a,b),radd(b,a); } for(int i=2;i<=cnt;++i) lg2[i]=lg2[i>>1]+1; dfs(1,0); read(tot); while(tot--) { int a,b; read(a),read(b); a=typ[a],b=typ[b]; int c=lca(a,b); int ans=depth[a]+depth[b]-(depth[c]<<1)+1;//计算碳的个数,记得+1 print(ans); printf("\n"); } return 0; } ``` 当然,我这种写法其实并不优,复杂度没有其他大佬们的方法优,请谅解!