題目大意
給出一個個點條邊的無向聯通圖,每次可以選中一個點,將這個點和它相鄰的點縮成一個點,求最少需要多少次才能把圖縮成一個點。
解題分析
首先要發現,以不同的順序選中同樣的點的結果其實是相同的,所以我們關注的就是選出哪些點,又由於,所以可以考慮二進制枚舉選出的點集S,然後是結論:
如果S內點聯通,且任意一個S外點都至少與一個S內點相連,那麼S合法。
那麼這兩個條件如何判斷?S外點與S內點相連可以事先用二進制求出每個點相連的點,用或運算就可以判斷,但是如何判斷聯通,只能BFS或DFS,但是複雜度是,最壞情況爲,總複雜度爲,對於有點懸(雖然加一些神奇優化也能過),子集枚舉只能,所以只能在BFS上優化,這裏就要引用一個不常見的BFS小優化。
對於正常的BFS,需要開兩個數組que和vis分別表示隊列內元素和是否已進入隊列,但如果對於此題特別小的情況下可以考慮兩個都用二進制優化。每次先取出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;
}