P3452 [POI2007]BIU-Offices 题解

本文讲解了一种高效的算法,用于解决含有百万级节点和边的图中补图的连通块数量及其大小问题。通过BFS生成树策略,利用链表技巧减少查找未标记节点的复杂度,最终实现O(n+m)的时间复杂度。关键步骤包括维护补图的连通性并仅处理树边。

Description

给定一张 nnn 个点 mmm 条边的图,你需要求出其补图的连通块个数以及各个连通块的大小。

原数据范围: 1≤n≤105,1≤m≤2×1061 \le n \le 10^5,1 \le m \le 2 \times 10^61n105,1m2×106
加强版数据范围: 1≤n,m≤5×1061 \le n,m \le 5 \times 10^61n,m5×106

Solution

补图的边数是 n2n^2n2 级别的,若我们建出整个补图则无法通过。

注意到,我们只需要连通块的信息,所以求出补图上各个连通块的生成树就足够了。该选择什么生成树呢?试试 bfs\text{bfs}bfs 树!

不难得到下面的大体思路——枚举一个 vis\text{vis}vis000 的点 uuu,标记 visu=1\text{vis}_u=1visu=1 并将它放入队列。然后,我们枚举 uuu 在补图上相邻且 vis\text{vis}vis000vvv,再将 vvv 加入队列,以此类推。仔细分析复杂度,不难发现——若能够快速找到所有这样的点 vvv,并仅枚举它们,那么所涉及到的边就只有树边,复杂度就正确了。

结合链表的思想,维护出每个点之后第一个未被打标记的点,于是每次从 uuu 往后跳就能找到所有 vvv 了。注意到,我们还需维护删除操作,所以需要使用链表维护。

特别的,若此时 vvvuuu 相邻,那么我们就忽略这个 vvv 并继续向后跳;所以对于每个 uuu,还要将它在原图上的相邻点打标记,便于在跳动过程中判断 vvv 是否合法。虽然这样并没有彻底做到 ⌈\lceil 涉及到的只有树边 ⌋\rfloor,但新增的一部分复杂度等于枚举原图上的边的复杂度,是 O(m)O(m)O(m) 的。

本题被解决。总复杂度 O(n+m)O(n+m)O(n+m)

Code

建议非紧急情况不要阅读代码,因为这与 Solution 中的某些定义不符

#include <bits/stdc++.ducati>
#define int long long
using namespace std;
const int maxn=100005,maxm=2000005;

int read(){
	int s=0,w=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-')  w=-w;ch=getchar();}
	while (ch>='0'&&ch<='9'){s=(s<<1)+(s<<3)+(ch^'0');ch=getchar();}
	return s*w;
}
int n,m,l,r,len,cnt;
int head[maxn],frt[maxn],nxt[maxn],vis[maxn],q[maxn],ans[maxn];

struct edge{int nxt,to;}e[maxm<<1];
 
void add_edge(int u,int v){
	cnt++;
	e[cnt].to=v,e[cnt].nxt=head[u],head[u]=cnt;
}

void del(int now){
	frt[nxt[now]]=frt[now];
	nxt[frt[now]]=nxt[now];
}

signed main(){
	n=read(),m=read();
	for (int i=0;i<=n;i++)  nxt[i]=i+1,frt[i]=i-1;
	for (int i=1;i<=m;i++){
		int u=read(),v=read();
		add_edge(u,v),add_edge(v,u);
	}
	nxt[n]=0,frt[0]=0;
	while (nxt[0]){
		int now=nxt[0],tot=0;
		del(now),l=1,r=0,q[++r]=now;
		while (l<=r){
			int cur=q[l++],saver=cur;
			tot++;
			for (int i=head[cur];i;i=e[i].nxt)  vis[e[i].to]=1;
			
			cur=0;
			while (1){
				cur=nxt[cur];
				if (!cur)  break;
				if (!vis[cur])  q[++r]=cur,del(cur);
			}
			for (int i=head[saver];i;i=e[i].nxt)  vis[e[i].to]=0;
		}
		ans[++len]=tot;
	}
	sort(ans+1,ans+len+1);
	printf("%d\n",len);
	for (int i=1;i<=len;i++)  cout<<ans[i]<<' ';
	
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值