题目大意
给出一个 n n n个点 e e e条边的无向联通图,每次可以选中一个点,将这个点和它相邻的点缩成一个点,求最少需要多少次才能把图缩成一个点。
解题分析
首先要发现,以不同的顺序选中同样的点的结果其实是相同的,所以我们关注的就是选出哪些点,又由于 n ≤ 22 n\le22 n≤22,所以可以考虑二进制枚举选出的点集S,然后是结论:
如果S内点联通,且任意一个S外点都至少与一个S内点相连,那么S合法。
那么这两个条件如何判断?S外点与S内点相连可以事先用二进制求出每个点相连的点,用或运算就可以 O ( n ) O(n) O(n)判断,但是如何判断联通,只能BFS或DFS,但是复杂度是 O ( e ) O(e) O(e),最坏情况为 O ( n 2 ) O(n^2) O(n2),总复杂度为 O ( 2 n n 2 ) O(2^nn^2) O(2nn2),对于 n ≤ 22 n\le22 n≤22有点悬(虽然加一些神奇优化也能过),子集枚举只能 O ( 2 2 ) O(2^2) O(22),所以只能在BFS上优化,这里就要引用一个不常见的BFS小优化。
对于正常的BFS,需要开两个数组que和vis分别表示队列内元素和是否已进入队列,但如果对于此题 n n n特别小的情况下可以考虑两个都用二进制优化。每次先取出que内一个元素x,然后加入vis内,将x相连的边加入队列,这些都可以用异或或或(c++ ^或 |)运算解决,但是将相邻元素加入队列时要防止再度入队,所以可以对询问值再&~vs(vs二进制取反)。
具体代码如下
int vs=0,que=s&(-s); //初始选一个入队
while (que){
int c=lst[que]; //lst[x]表示x的二进制最右是1的一位
vs|=1<<(c-1); //打标记
que^=1<<(c-1); //在队列中去除该元素
que|=f[c]&s&~vs; //加入相邻元素
}
示例代码
#include<cstdio>
using namespace std;
const int maxn=(1<<22)+5;
int n,e,ans,lst[maxn],ct[maxn],f[30];
int main()
{
freopen("party.in","r",stdin);
freopen("party.out","w",stdout);
scanf("%d%d",&n,&e);
for (int i=1;i<(1<<n);i++){
ct[i]=ct[i>>1]+(i&1);
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,x,y;i<=e;i++){
scanf("%d%d",&x,&y);
f[x]|=(1<<(y-1)); f[y]|=(1<<(x-1));
}
ans=0;
for (int i=1;i<=n;i++)
if (f[i]!=(1<<n)-1) ans=(1<<n)-1;
for (int s=1;s<(1<<n)-1;s++){
int S=0;
for (int i=1;i<=n;i++)
if (s>>(i-1)&1) S|=f[i];
if (S!=(1<<n)-1) continue;
int vs=0,que=s&(-s);
while (que){
int c=lst[que];
vs|=1<<(c-1);
que^=1<<(c-1);
que|=f[c]&s&~vs;
}
if (vs==s&&ct[ans]>ct[s]) ans=s;
}
printf("%d\n",ct[ans]);
for (int i=1;i<=n;i++)
if (ans>>(i-1)&1) printf("%d ",i);
return 0;
}