题目
题意概要
现有
n
n
n 点
m
m
m 边的简单无向连通图,每次可以选择一个点,将任意两个与该点相邻的点之间连上一条边。问最少操作多少次,使得整张图为完全图。
数据范围与提示
n
≤
22
n\le 22
n≤22,而
m
m
m 无特殊限制。提示:
C
F
CF
CF 是少爷机。
思路
T I P TIP TIP:如果一个点集的导出子图是完全图,那么这个点集就是一个 “团”。
不难发现过程大概是这样的:选择一个点以后,对于一个包含该点的团,加入与该点相邻的点。一开始有很多个团,然后在不断的操作中变大(或者多个团合成了一个团),最终包含整张图。
我们是否可以 只追踪一个团的变化 呢?是可以的。这里给出一种简单的证明。
考虑何时不能只考虑一个团?那就是必须先操作当前的团之外的一个点,使得外面的两个点相连,然后操作团内的点。也就是说,操作团外的一个点 y y y,因为 y y y 和团内点 x x x、团外点 z z z 都有连边,就使得 x , z x,z x,z 相连。此时操作 x x x,就会把 z z z 拉入团。
可是,我们完全可以更改为先操作 x x x 再操作 y y y 。先操作 x x x 时, y y y 加入了 “团”,然后操作 y y y 就使 z z z 加入了 “团”。跟先操作 y y y 的效果是一样的。所以,我们先操作 x x x,也存在一种方式得到最优解。
也就是说,我们如果要让当前的 “团” 最终发展为大小为 n n n,那就不需要操作 “团” 之外的点,每次操作总是在让当前的 “团” 变大。
d p \tt dp dp 思路已经出现。用 f ( S ) f(S) f(S) 表示, S S S 集合中的点形成 “团” 所需最小操作次数。建议刷表法,枚举进行一次何种操作。输出方案存前驱即可。复杂度 O ( n 2 n ) \mathcal O(n2^n) O(n2n) 。
求 “团” 怎么求?任取一个 x ∈ S x\in S x∈S,则 S S S 是 “团” 的充要条件是, S − { x } S-\{x\} S−{x} 是 “团” 且 S − { x } S-\{x\} S−{x} 中任意一个点都与 x x x 相连。怎么判断是否 S S S 中每个点都与 x x x 相连?咱不是用状压的邻接矩阵吗,难道这都做不到?
代码
每个点最多操作一次,可知答案最大为 n n n 。实际上应该是 n − 2 n-2 n−2,在一条链的情况下。
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;
inline int readint(){
int a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
const int MaxN = 22;
int dp[1<<MaxN], g[1<<MaxN];
int pre[1<<MaxN], ans[1<<MaxN]; // 还原方案
void paint(int S){
if(dp[S] == 0) return ; // 生而为人 我很抱歉
paint(pre[S]), printf("%d ",ans[S]+1);
}
int main(){
int n = readint(), m = readint();
for(int i=1,a,b; i<=m; ++i){
a = readint()-1, b = readint()-1;
g[a] ^= 1<<b, g[b] ^= 1<<a;
}
for(int S=1; S<(1<<n); ++S){
dp[S] = n+1; // 极大值
for(int i=0; i<n; ++i)
if(S>>i&1){
int S_ = S^(1<<i);
if((S_&g[i]) == S_)
dp[S] = dp[S_];
break; // 小剪枝
}
}
for(int S=1; S<(1<<n); ++S)
for(int i=0; i<n; ++i) if(S>>i&1)
if(dp[S|g[i]] > dp[S]+1){
dp[S|g[i]] = dp[S]+1;
pre[S|g[i]] = S, ans[S|g[i]] = i;
}
printf("%d\n",dp[(1<<n)-1]);
paint((1<<n)-1);
return 0;
}