CF906C(#454div1C)——状压+位运算广搜

题目传送门
n n n个点 m m m条边的图,每次选择一个点,把它和与它相邻的点连接成一个完全图。问最少需要选几个点,就可以将整张图连接成一个完全图。请输出点数和任意一种最优方案

这里有一个结论,就是如果一个点集,它向点集外连出的边能保证整个图联通,并且点集自身内部联通,那么这个点集就是一个可行的答案。
这个证明其实也很简单,因为只要连通,那么无论从哪个点开始扩展,都能保证连出一个完全图,然后继续扩展、扩展,就可以将整个图变为一个完全图(还没理解的可以自己手算一下)
n n n又只有 22 22 22,所以我们可以用一个二进制数来表示点集的状态,用 f i f_i fi表示点 i i i相连的点集,那么只要目前的点集的 f i f_i fi的或和为整个序列,那么就保证向外与整个图联通,而自身内部联通可以用 B F S BFS BFS来验证。普通的验证理论复杂度为 O ( n 2 ) O(n^2) O(n2),但是今天的一个骚操作可以把这个复杂度将为 O ( n ) O(n) O(n)
因为每个点只能入队一次,因此我们可以用二进制来记录队列里的点和 v i s vis vis数组的状况。那么我们每次选择队列状态的最低非零为进行转移,直接用位运算算出下面的状态。由于只关心点集内部的情况,所以要 & i \&i &i i i i表示点集),又因为进过队的不再进,所以还要 & \& &~ v i s vis vis,那么这样一直转移,知道 Q Q Q 0 0 0 的时候,如果 v i s vis vis已经能表示整个点集,也就是vis==i,那么这个状态内部就是连通的,也就是可行的。
因此每次枚举,广搜,取最小的点集即可。
#include<bits/stdc++.h>
#define MAXN 4200000
using namespace std;
int read(){
	char c;int x;while(c=getchar(),c<'0'||c>'9');x=c-'0';
	while(c=getchar(),c>='0'&&c<='9') x=x*10+c-'0';return x;
}
int n,m,ans,f[25],lst[MAXN],num[MAXN];
int main()
{
	n=read();m=read();
	for(int i=1;i<(1<<n);i++)num[i]=num[i>>1]+(i&1);
	for(int i=1;i<(1<<n);i++)lst[i]=(i&1)?1:lst[i>>1]+1;
	for(int i=1;i<=n;i++) f[i]|=(1<<i-1);
	for(int i=1;i<=m;i++){
		int x=read(),y=read();
		f[x]|=(1<<y-1);f[y]|=(1<<x-1);
	}
	for(int i=1;i<=n;i++)
	  if(f[i]!=(1<<n)-1) ans=(1<<i)-1;   //如果每个点都能直接与所有点连通,那么这步后ans=0
	for(int i=1;i<(1<<n);i++){
		int cheq=0,vis=0,Q=i&-i;
		for(int j=1;j<=n;j++)
		 if((i&(1<<j-1))) cheq|=f[j];
		if(cheq!=(1<<n)-1) continue;   //向外不能整个连接就弹出
		while(Q){
			int p=lst[Q];
			Q^=(1<<p-1);vis|=(1<<p-1);
			Q|=f[p]&i&~vis;
		}
		if(vis==i&&num[i]<num[ans]) ans=i;
	}
	printf("%d\n",num[ans]);
	for(int p=1;ans;ans/=2,p++)
	   if(ans&1) printf("%d ",p);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值