题目
点这里看题目。
分析
一个简单的初始想法是:计算所有最终不是强连通的方案,然后再用总方案减去。
那么非强连通的方案经过缩点后,必然会变成 DAG 的形状。我们可以枚举所有 DAG 的形态,计算方案数:
- 每个强连通块的方案数:子问题,递归即可;
- 外部 DAG 的数量;
考虑求解 DAG 的数量。这里假设缩点后图为 (G'=(V,E')),且设 (operatorname{cnt}_E(S,T)=sum_{(u,v)in E}[uin Sland vin T])。
可以设 (f_S) 为使得点集 (S) 构成 DAG 的边集数量注意到,对于任意的 DAG,必然存在若干个入度为 0 的点,这些点之间不会连边而可以向其余点任意连边,我们可以枚举这些入度为 0 的点:
这个转移其实相当于按照拓扑序删除图中的点。
但是,同一层的点会因为子集的多次枚举而导致算重。为了解决这个问题,我们需要容斥:
此时我们就得到了一个比较暴力的算法,复杂度应该在 (O(sum_k{nrace k}2^k)) 左右。
为了加速计算过程,我们需要将原先“缩点后”的图上的 DP,转移到原图上来。
可以发现,原图上一个点集 (S) 如果通过某种方法被划分成了 (t) 个强连通分量,那么它在“缩点后”的图上 DP 的时候,贡献就是 ((-1)^{t+1})。也就是说,它只会和 (t) 的奇偶性相关。
这样不难导出一种状态设计:设 (g_S) 表示原图上使得 (S) 点集为强连通的边集数量,(h_{0/1,S}) 表示原图上使得 (S) 点集变为偶数个/奇数个点集的边集数量。相似地,我们仍然可以用总方案数减去 DAG 数;计算 DAG 数则枚举最终 DAG 上入度为 0 的点集,算上容斥系数:
而 (h) 的转移则比较显然:
需要注意的是,在转移的时候,(h_{1,S}) 里面不应该计入 (g_S),这样不会形成 DAG。这一点分析可以帮助我们规避环形转移。
此外,(operatorname{cnt}_E) 需要精细实现一下,比如使用 bitset
。最终复杂度可以做到 (O(frac{3^nm}{omega}))。
小结:
- 先思考一些暴力,再尝试优化的思路;
- 通过分析最终的形态,入手计算方案数,这是很常见的思路,从结果的特征来入手;
- 抓住 DAG 的常用特征之一——拓扑序;
- 从“缩点后”的图转移到原图的时候,需要理清楚哪些是可以直接套用的,同时有必要丢弃一些不重要的信息;
代码
#include <bitset>
#include <cstdio>
#include <iostream>
#define rep( i, a, b ) for( int i = (a) ; i <= (b) ; i ++ )
#define per( i, a, b ) for( int i = (a) ; i >= (b) ; i -- )
const int mod = 1e9 + 7;
const int MAXN = 20, MAXM = 215, MAXS = ( 1 << 15 ) + 5;
template<typename _T>
void read( _T &x )/*{{{*/
{
x = 0; char s = getchar(); int f = 1;
while( ! ( '0' <= s && s <= '9' ) ) { f = 1; if( s == '-' ) f = -1; s = getchar(); }
while( '0' <= s && s <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ( s - '0' ), s = getchar(); }
x *= f;
}/*}}}*/
template<typename _T>
void write( _T x )/*{{{*/
{
if( x < 0 ) putchar( '-' ), x = -x;
if( 9 < x ) write( x / 10 );
putchar( x % 10 + '0' );
}/*}}}*/
std :: bitset<MAXM> out[MAXS], in[MAXS];
int grp[MAXS];
int f[MAXS], g[2][MAXS];
int pw[MAXM];
int N, M;
inline int Mul( int x, int v ) { return 1ll * x * v % mod; }
inline int Sub( int x, int v ) { return ( x -= v ) < 0 ? x + mod : x; }
inline int Add( int x, int v ) { return ( x += v ) >= mod ? x - mod : x; }
int Count( const int S, const int T )/*{{{*/
{
return ( out[S] & in[T] ).count();
}/*}}}*/
int main()
{
read( N ), read( M ), pw[0] = 1;
rep( i, 1, M )
{
pw[i] = Mul( pw[i - 1], 2 );
int u, v; read( u ), read( v ), u --, v --;
for( int S = 0 ; S < ( 1 << N ) ; S ++ )
{
if( S >> u & 1 ) out[S].set( i - 1 );
else out[S].reset( i - 1 );
if( S >> v & 1 ) in[S].set( i - 1 );
else in[S].reset( i - 1 );
}
}
for( int S = 0 ; S < ( 1 << N ) ; S ++ ) grp[S] = Count( S, S );
f[0] = g[0][0] = 1;
for( int S = 1 ; S < ( 1 << N ) ; S ++ )
{
f[S] = pw[grp[S]]; int low = S & ( - S );
for( int T = ( S - 1 ) & S ; T ; T = ( T - 1 ) & S )
{
if( ! ( T & low ) ) continue;
g[0][S] = Add( g[0][S], Mul( g[1][S ^ T], f[T] ) );
g[1][S] = Add( g[1][S], Mul( g[0][S ^ T], f[T] ) );
}
for( int T = S ; T ; T = ( T - 1 ) & S )
f[S] = Sub( f[S], Mul( pw[Count( T, S ^ T ) + grp[S ^ T]], Sub( g[1][T], g[0][T] ) ) );
g[1][S] = Add( g[1][S], f[S] );
}
write( f[( 1 << N ) - 1] ), putchar( '
' );
return 0;
}