【[HNOI/AHOI2018]毒瘤】
思路非常精妙的一道虚树题
简单题意:给一张图,求这张图上的独立集数量
对于一棵树的情况,可以设\(dp_{u,0/1}\)表示节点\(u\)选/不选的方案数
显然有:
接下来考虑非树边
正常\(dp\)的话那么转移应该是\(O(n)\)的
我们发现非树边很少,所以可以预处理出来,然后枚举其连接的两点之间的状态
不难发现状态只有如下三种:\((0,0),(0,1),(1,0)\)
可以发现\((0,0)\)和\((0,1)\)可以合并
于是我们就只需要枚举所有的非树边其连接的\(\rm dfn\)较小的点的状态就可以了
复杂度\(O(n*2^{11})\)可以得到\(75pts\)的高分
接下来是一个很妙的做法:
我们可以考虑将所有非树边上的点拿下来建虚树
这应该是一个大小\(\le(44+1)\)的虚树
注意到,我们的\(dp\)转移本应如此:
但是放在虚树上这样显然是错的
我们发现虚树是维护了点之间的关系并构建了其\(lca\),所以虚树上的点和其父亲两两之间是一条链的关系
我们仔细思考可以发现这个东西居然只是额外附带了一个系数而已...
不妨设虚树上的一条边\(u,v\)为\(u->k_1->k_2...->k_r->v\),显然中间是没有其他关键点的
我们发现\(v\)对于\(u\)造成的贡献实际上是可以计算出来的
因为没有其他儿子,所以\(k_r\)本身的\(dp\)值可以计算出来,考虑\(dp_{v,0}\)对其造成的贡献,有:\(dp_{k_r,1}*=dp_{v,0},dp_{k_r,0}*=(dp_{v,0}+dp_{v,1})\)
\(dp_{k_{r-1},1}*=dp_{k_r,0}\)即\(dp_{k_{r-1},1}*=(dp_{k_r,0}'*(dp_{v,0}+dp_{v,1}))\)
\(dp_{k_{r-1},0}*=(dp_{k_r,0}+dp_{k_r,1})\)即\(dp_{k_{r-1},0}*=(dp_{k_r,0}'*(dp_{v,0}+dp_{v,1})+dp_{k_r,1}'*dp_{v,0})\)
好像这样类似下去这样的系数是可以预处理出来的欸欸欸?
于是我们可以把虚树上的转移式改成这样:
剩下的工作就是预处理系数了qwq
我们考虑如何求解系数,我们类比于上面展开那个式子的过程,将其中的\(dp_{kr,0/1}'\)换成到那个点时候的系数
就可以得到系数的递推式了,具体展开非常麻烦,就不写了
然后我们考虑在虚树上\(dp\),仅仅给每个点额外附上一个系数就可以了吗?
当然是不行的,每个点的\(dp\)初值也不是我们一开始所说的\(dp\)值\(1\)了
所以我们还要对虚树上的每个点做\(dp\)以计算初值,不过这一步相对简单,因为仔细考虑我们会发现如果一个点的某棵子树内有被标记的点,那么显然这个子树会被表示成虚树上的一条边,系数已经计算出来了,所以我们实际上要处理的\(dp\)值只有那些子树内没有被标记点的\(dp\)值
一道虚树的题我硬生生写了\(5kb\)....汗,不亏是[毒瘤]
不过我的代码应该还比较清晰qwq
设\(s=m-n+1\),尽管建树看似像遍历了\(s\)遍全树,但实际上只有一遍....
所以复杂度为\(O(n+s*2^s)\)
#include<bits/stdc++.h>
using namespace std;
#define Next2( i, x ) for( register int i = fire[x]; i; i = p[i].next )
#define Next( i, x ) for( register int i = head[x]; i; i = e[i].next )
#define rep( i, s, t ) for( register int i = s; i <= t; ++ i )
#define re register
#define int long long
int gi() {
char cc = getchar(); int cn = 0, flus = 1;
while(cc < '0' || cc > '9') { if( cc == '-' ) flus = -flus; cc = getchar(); }
while(cc >= '0' && cc <= '9') cn = cn * 10 + cc - '0', cc = getchar();
return cn * flus;
}
const int P = 998244353 ;
const int N = 1e5 + 5 ;
const int M = 55 ;
int dp[N][2], dfn[N], head[N], fire[N], Ans, n, m, cnt ;
int num, Cnt, Fr[N], Ed[N], vis[N], s[M], top, Rev[N][2] ;
int Fa[N], Top[N], dep[N], sz[N], Son[N], k0[N][2], k1[N][2] ;
struct E {
int to, next ;
} e[N * 2], p[N * 2] ;
void add( int x, int y ) {
e[++ cnt] = (E){ y, head[x] }, head[x] = cnt ,
e[++ cnt] = (E){ x, head[y] }, head[y] = cnt ;
}
void add2( int x, int y ) {
p[++ cnt] = (E){ y, fire[x] }, fire[x] = cnt ,
p[++ cnt] = (E){ x, fire[y] }, fire[y] = cnt ;
}
//建树
inline void dfs( int x, int fa ) {
dfn[x] = ++ num, Fa[x] = fa, dep[x] = dep[fa] + 1, sz[x] = 1 ;
Next( i, x ) {
int v = e[i].to ; if( v == fa ) continue ;
if( !dfn[v] ) {
dfs( v, x ), sz[x] += sz[v] ;
if( sz[v] > sz[Son[x]] ) Son[x] = v ;
}
else if( dfn[v] < dfn[x] ) Fr[++ Cnt] = v, Ed[Cnt] = x, vis[v] = vis[x] = 1 ;
}
}
inline void dfs2( int x, int high ) {
Top[x] = high ;
if( Son[x] ) dfs2( Son[x], high ), vis[x] += vis[Son[x]] ;
Next( i, x ) {
int v = e[i].to ; if( v == Fa[x] || v == Son[x] ) continue ;
dfs2( v, v ), vis[x] += vis[v] ;
}
}
int LCA( int x, int y ) {
while( Top[x] != Top[y] ) {
if( dep[Top[x]] < dep[Top[y]] ) swap( x, y ) ;
x = Fa[Top[x]] ;
}
return ( dep[x] > dep[y] ) ? y : x ;
}
int K[50], tot ;
bool cmp( int x, int y ) {
return dfn[x] < dfn[y] ;
}
inline void insert( int x ) {
if( top == 1 && x != 1 ) { s[++ top] = x ; return ; }
int lca = LCA( s[top], x ) ; if( lca == x ) return ;
while( top > 1 && dfn[lca] < dfn[s[top - 1]] ) add2( s[top - 1], s[top] ), -- top ;
if( dfn[lca] < dfn[s[top]] ) add2( lca, s[top] ), -- top ;
if( dfn[lca] > dfn[s[top]] ) s[++ top] = lca ;
s[++ top] = x ;
}
//建立虚树
void Maker() {
cnt = 0 ;
rep( i, 1, Cnt ) K[++ tot] = Fr[i], K[++ tot] = Ed[i] ;
sort( K + 1, K + tot + 1, cmp ) ; s[top = 1] = 1 ;
rep( i, 1, tot ) insert( K[i] ) ;
while( top > 1 ) add2( s[top - 1], s[top] ), -- top ;
}
void init() {
n = gi(), m = gi() ;
rep( i, 1, m ) add( gi(), gi() ) ; cnt = 0 ;
dfs( 1, 1 ), memset( head, 0, sizeof(head) ) ;
rep( i, 1, n ) if( Fa[i] != i ) add( Fa[i], i ) ;
dfs2( 1, 1 ), Maker() ;
}
//辅助计算系数
inline void DP( int x, int u ) {
dp[x][1] = dp[x][0] = 1 ;
Next( i, x ) {
int v = e[i].to ; if( v == Fa[x] || v == u ) continue ;
DP( v, x ), dp[x][1] *= dp[v][0], dp[x][1] %= P ;
dp[x][0] *= ( dp[v][1] + dp[v][0] ), dp[x][0] %= P ;
}
}
//计算系数...
inline void Get( int u, int fa ) {
int v = u ; k0[u][0] = 1, k0[u][1] = 1, k1[u][0] = 1 ;
while( Fa[v] != fa ) {
DP( Fa[v], v ) ; int r0 = k0[u][0], r1 = k0[u][1] ;
k0[u][0] = ( dp[Fa[v]][0] * r0 + dp[Fa[v]][1] * k1[u][0] ) % P ;
k0[u][1] = ( dp[Fa[v]][0] * r1 + dp[Fa[v]][1] * k1[u][1] ) % P ;
k1[u][1] = dp[Fa[v]][0] * r1 % P, k1[u][0] = dp[Fa[v]][0] * r0 % P ;
v = Fa[v] ;
}
}
//计算dp初值
inline void Get_DP( int x, int fa ) {
Rev[x][0] = Rev[x][1] = 1 ;
Next( i, x ) {
int v = e[i].to ; if( v == fa || vis[v] ) continue ;
Get_DP( v, x ), Rev[x][0] *= ( Rev[v][0] + Rev[v][1] ), Rev[x][0] %= P ;
Rev[x][1] *= Rev[v][0], Rev[x][1] %= P ;
}
}
//计算出每条边的系数,每个点的dp初值
inline void dfs_init( int x, int fa ) {
Get_DP( x, fa ), K[++ tot] = x ;
if( x != fa ) Get( x, fa ) ;
Next2( i, x ) {
int v = p[i].to ; if( v == fa ) continue ;
dfs_init( v, x ) ;
}
}
//真正的dp,前面的都是预处理
inline void Dp( int x, int fa ) {
Next2( i, x ) {
int v = p[i].to ; if( v == fa ) continue ;
Dp( v, x ) ; dp[x][0] *= ( k0[v][0] * dp[v][0] % P + k0[v][1] * dp[v][1] % P ), dp[x][0] %= P ;
dp[x][1] *= ( k1[v][0] * dp[v][0] % P + k1[v][1] * dp[v][1] % P ), dp[x][1] %= P ;
}
}
signed main()
{
init(), tot = 0, dfs_init( 1, 1 ) ;
int maxn = ( 1 << Cnt ) - 1 ;
rep( i, 0, maxn ) {
rep( i, 1, tot ) dp[K[i]][0] = Rev[K[i]][0], dp[K[i]][1] = Rev[K[i]][1] ;
for( re int j = 1; j <= Cnt; ++ j ) {
if( ( 1 << ( j - 1 ) ) & i ) dp[Fr[j]][0] = 0, dp[Ed[j]][1] = 0 ;
else dp[Fr[j]][1] = 0 ;
}
Dp( 1, 1 ), Ans = ( Ans + dp[1][0] + dp[1][1] ) % P ;
}
printf("%d\n", Ans % P ) ;
return 0;
}