[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!

posted @ 2025-04-15 20:19  lyas145  阅读(15)  评论(0)    收藏  举报