[ABC401] E - Reachable Set 题解
题目在这里。
Rating 涨到 1135 了(终于快青了),写篇题解纪念一下。
题目描述
给你一个 \(N\) 个点、\(M\) 条边的图。
对于每个 \(k=1,2,\ldots,N\),解决以下问题:
考虑下列操作。
- 选择一个点,并删除该点和所有与它连接的边(注:以下讲解中简称为删掉该点)。
确定是否可以重复这项操作以符合下列条件:
- 通过遍历边从点 \(1\) 到达的点集正好由 \(k\) 个点(编号为 \(1\) 到 \(k\) 的点)组成。
如果可能的话,找到这样做所需的最少操作数。
思路
注意是 \(1\) 到 \(k\) 这 \(k\) 个点。
我们先抛去求最少操作次数这个问题。
现在我们把题目改成对于一个 \(k\),是否存在满足题目条件的点集。
如果想要有这样的点集,那么点 \(1\) 到点 \(k\) 这 \(k\) 个点必须能够构成一个只有这 \(k\) 个点的连通块。也相当于删掉所有其他的点和与其相连的边,所剩下的点能够构成一个连通块。
相当于删掉所有其他节点?并且 \(k\in[1,N]\)?
或许我们可以从点 \(1\) 到点 \(N\),依次把这些点加入到另一个图中(刚开始这图里啥也没有),如果新图中已加入的点在原图中与这个点有直接相连的边,那么把这些边也给加入到新图中。
不过边并不重要,重要的是连通块,如果加入一个点后并连完了该连的边,连通块数量变成了 \(1\),那么对于目前的这个 \(k\),是存在满足题目条件的点集的。
连通块?并且只加点加边?——并查集!
于是在加点过程中,连边的操作就成了点集的合并。
现在我们再把求最小操作次数这个问题加上。
可以发现答案就是(有解情况下)点 \(1\) 到点 \(k\) 这 \(k\) 个点与其他多少点有直接相连的边。
直接讲不太好讲,还是对着代码讲吧。
void merge(int x,int y) { //合并两个点集。
if ((x=find(x))^(y=find(y))) {
fa[x]=y;
bk--;
//两个连通块合并成一个,连通块数 -1。
}
}
int main() {
//前面略,完整代码在最下面。
for (int u=1;u<=n;u++) { //加入 u 这个点。
bk++;
//点 u 刚开始自己就是一个连通块,连通块数 +1。
ans-=vis[u];
//vis[x] 如果为 true,则表示点 x 是需要删掉的点。
//如果 u 是之前需要删掉的点,那么 u 肯定现在不能删了,所以 ans++。
vis[u]=0; //u 不能删呦。
for (int i=h[u];i;i=ne[i]) { //遍历原图中与 u 有直接相连的边的点。
int v=e[i]; //v 表示与 u 有直接相连的边的点。
if (v<u) {merge(u,v);}
//如果 v 是新图中已加入的点,那么 u 所在的连通块的点集与 v 所在的连通块的点集合并。
else {ans+=!vis[v];vis[v]=1;}
//否则,该点是需要被删掉的。
}
if (bk^1) {puts("-1");}
//连通块数不为 1(新图中已加入的点不能构成一整个连通块),无解。
else {printf("%d\n",ans);}
//否则一定有解,输出 ans。
}
return 0;
}
蛤?这不是 \(\mathcal{O}(NM\log N)\) 的吗?不会 TLE 吗?
实际上……

时间复杂度其实不是 \(\mathcal{O}(NM\log N)\)。
我们设 \(deg_u\) 表示点 \(u\) 的度数,因为对于每个点,我们都会遍历一遍原图中与它直接相连的边,因此真正的时间复杂度为 \(\mathcal{O}((\sum\limits_{i=1}^ndeg_i)\log N)\) 也就是 \(\mathcal{O}(2M\log N)\)。
码儿
赛时写的代码有点抽象,这是我新打的代码。
#include<bits/stdc++.h>
#define qwq return
using namespace std;
const int N=2e5+5,M=6e5+5;
int n,m,bk,ans;
int fa[N];
bool vis[N];
int h[N],e[M],ne[M],idx=1;
inline int read() { //快读,可直接跳过。
int x=0,f=1;
char c=getchar();
while (!isdigit(c)) {f=(c=='-'?-1:1);c=getchar();}
while (isdigit(c)) {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
qwq x*f;
}
void add(int a,int b) { //链式前向星。
e[idx]=b;ne[idx]=h[a];h[a]=idx++;
}
int find(int x) { //这个函数我不用讲了吧……
qwq fa[x]==x?x:fa[x]=find(fa[x]);
}
void merge(int x,int y) { //合并两个点集。
if ((x=find(x))^(y=find(y))) {
fa[x]=y;
bk--;
//两个连通块合并成一个,连通块数 -1。
}
}
int main() {
n=read();m=read();
iota(fa+1,fa+1+n,1);
for (int i=1;i<=m;i++) {
int a=read(),b=read();
add(a,b);
add(b,a);
}
for (int u=1;u<=n;u++) { //加入 u 这个点。
bk++;
//点 u 刚开始自己就是一个连通块,连通块数 +1。
ans-=vis[u];
//vis[x] 如果为 true,则表示点 x 是需要删掉的点。
//如果 u 是之前需要删掉的点,那么 u 肯定现在不能删了,所以 ans++。
vis[u]=0; //u 不能删呦。
for (int i=h[u];i;i=ne[i]) { //遍历原图中与 u 有直接相连的边的点。
int v=e[i]; //v 表示与 u 有直接相连的边的点。
if (v<u) {merge(u,v);}
//如果 v 是新图中已加入的点,那么 u 所在的连通块的点集与 v 所在的连通块的点集合并。
else {ans+=!vis[v];vis[v]=1;}
//否则,该点是需要被删掉的。
}
if (bk^1) {puts("-1");}
//连通块数不为 1(新图中已加入的点不能构成一整个连通块),无解。
else {printf("%d\n",ans);}
//否则一定有解,输出 ans。
}
qwq 0;
}
Thanks for reading!

浙公网安备 33010602011771号