省选模拟赛某题 图3.29
LINK:图
求出一张图中 生成树森林或基环树+生成树森林的每个方案的贡献之和。n<=16,m<=(n-1)*n/2.
图中只允许最多出现一个环 一个方案的贡献为 这张图的各个联通块大小之积。
(爆搜能拿很多分数 因为爆搜的上界并非2^m 类似于一个C(m,n)+C(m,n-1)+C(m,n-2)+...的复杂度。
由于最多只有n条边 且图中最多只有一个环 但是由于边数过多考虑状压点。
当求生成树森林的时候 不难想到f[i]表示i个点的集合生成树的数量。
对于某个集合i来说暴力跑矩阵树定理 最后暴力合并起来 发现很难合并 因为不同状态的方案和答案都不同 答案可以合并但是方案对应着答案就很难合并了。
一个trick:虚建源点0 使源点向每个点连一条边最后这种图的生成树的个数等价于我们要求的各个联通块大小乘积之和。
可以这么理解:0连接着几个联通块每个联通块和0相连的方案是他们的点数 而内部则是他们自己的生成树方案。
所有的方案之和 刚好等于刚才的乘积之和。
正确性显然。
考虑图中存在一个环怎么做?至少一张图的生成树个数+一条边这个问题的解决是异常困难的 需要拟阵什么的。
考虑先枚举环 环+一些点就可以变成上面的那个情况。
把环缩点!然后就变成链上面的生成树森林的情况了 还是那样做。
如何求环?一张无向图求出点集为i的环的个数 可以跑dp 考虑一个环有多个起点我们定义编号最小的那个点为起点暴力跑状压dp即可。
由于一个环可以正反跑两次 所以要除以2.
考虑变成生成树森林后答案怎么求 每个联通块对答案的贡献不变。外面再乘以一个环的方案即可。
const int MAXN=17;
int n,m,cnt,maxx;
int a[MAXN][MAXN],id[MAXN],pos[1<<MAXN];
ll f[1<<MAXN][MAXN],c[1<<MAXN],b[MAXN][MAXN],ans;
inline void add(int x,int y)
{
++b[x][x];++b[y][y];
--b[x][y];--b[y][x];
}
inline ll GAUSS(int w)
{
memset(b,0,sizeof(b));
rep(1,n,i)
{
rep(1,i,j)
{
if(id[i]==id[j])continue;
if(a[i][j])add(id[i],id[j]);
}
add(0,id[i]);
}
ll sum=1;
rep(0,w-1,i)
{
rep(i+1,w-1,j)
{
while(b[j][i])
{
int d=b[i][i]/b[j][i];
rep(i,w-1,k)b[i][k]=(b[i][k]-b[j][k]*d+mod)%mod;
rep(0,w-1,k)swap(b[i][k],b[j][k]);
sum=-sum;
}
}
if(!b[i][i]){return 0;}
sum=sum*b[i][i]%mod;
}
return (sum+mod)%mod;
}
int main()
{
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
get(n);get(m);maxx=(1<<n)-1;
rep(1,m,i)
{
int x,y;
get(x);get(y);
a[x][y]=a[y][x]=1;
}
rep(1,n,i)pos[1<<(i-1)]=i,f[(1<<(i-1))][i]=1,id[i]=i;
rep(1,maxx,i)
{
int s=i,st=pos[i&(-i)];
while(s)
{
int w=s&(-s);
int tn=pos[w];
if(!f[i][tn]){s-=w;continue;}
int s1=maxx^i;
while(s1)
{
int ww=s1&(-s1);
if(pos[ww]>st&&a[tn][pos[ww]])
f[i|ww][pos[ww]]+=f[i][tn];
s1-=ww;
}
s-=w;
if(a[tn][st])c[i]+=f[i][tn];
}
}
ans=GAUSS(n);
rep(1,maxx,i)
{
int w=i&(-i);
int s=s-w;
w=s&(-s);
if(w==s)continue;
cnt=1;
rep(1,n,j)
{
if(i&(1<<(j-1)))id[j]=1;
else id[j]=++cnt;
}
ans=(ans+GAUSS(cnt)*(c[i]>>1))%mod;
}
putl(ans);return 0;
}