AT_arc101_c
AT_arc101_c
子集容斥的一道题。
正难则反是好想到的事实上覆盖一次以上就满足题意,这种一般都是反着做,也就是说一边是有限个,另一边是无限个考虑有限个的,可以枚举哪些边没被覆盖,然后做。
\[\sum_{i=0}^n \binom{n}{i}ways(i)(-1)^i
\]
其中ways(i)表示选出i个边段掉的剩下点配对方案数,事实上由于边互不相同,所以这个binom不必要乘,因该写成
\[\sum_{i=0}^n ways(i)(-1)^i
\]
问题在于求ways,把边断掉后形成若干连通块,那么每个连通块点配对书相乘就是答案。
当然可以2^n枚举,考虑优化。
容易想到dp,一个显然的dp思路是把所有东西记进去\(dp_{u,i,j}\)表示以u为根子树,有i个断边,u所在连通块大小为j,方案数(不包含u所在连通块配对方案)
转移就是
\[dp_{u,i,j}+=dp_{v,k,j1}dp_{u,i-k,j-j1}+dp_{v,k,j1}dp_{u,i-k-1,j}sum_{j1}
\]
但是直接T飞。
问题在于记的太多,一个经典解决方法就是直接将容斥系数都融入到dp中(还有一种就是开科技NTT)
那么记\(dp_{u,i}\)表示以u为根子树,u所在连通块大小为j,方案数(不包含u所在连通块配对方案)
那么转移
- u与v不断,\(dp_{u,k}\)<-\(dp_{v,j}dp_{u,k-j}\)
- u与v断,\(dp_{u,k}\)<-\((-sum_j)dp_{v,j}dp_{u,k}\)
那么最后就是求和,注意把1所在连通块给算进去
\[\sum_{i=0}^ndp_{1,i}sum_{i}
\]
注意
- 树形dp上下界优化,否则就是\(n^3\)
- 由于每次dp要在旧的值基础上,而新旧值放在一个数组中就会紊乱所以要开一个中间数组
code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
void rd(ll& x)
{
char ch=getchar();
ll f=1;x=0;
while(ch<'0'||ch>'9'){
if(ch=='-')f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+ch-48;
ch=getchar();
}
x=x*f;
}
const ll N_ = 5005,mod=1e9+7;
ll ans,N,dp[N_][N_],S[N_],siz[N_],tmp[N_];
vector<ll> q[N_];
inline ll MOD(ll x)
{
x+=mod;
return x-x/mod*mod;
}
void dfs(ll u,ll f)
{
siz[u]=1;
dp[u][1]=1;
for(auto v:q[u])
{
if(v==f)continue;
dfs(v,u);
ll A=siz[u];
siz[u]+=siz[v];
for(int i=0;i<=siz[u];i++)tmp[i]=0;
for(int k=siz[u];k>=1;k--)
{
for(int j=max(1ll,k-A);j<=min(k-1*1ll,siz[v]);j++)tmp[k]=MOD(tmp[k]+MOD(dp[u][k-j]*1ll*dp[v][j]));
if(k<=siz[u])for(int j=1;j<=siz[v];j++)tmp[k]=MOD(tmp[k]+MOD(MOD(dp[v][j]*1ll*dp[u][k])*1ll*(-S[j])));
}
for(int i=1;i<=siz[u];i++)dp[u][i]=tmp[i];
}
}
int main()
{
// freopen("data.in","r",stdin);
rd(N);
for(int i=1;i<N;i++)
{
ll u,v;
rd(u);rd(v);
q[u].push_back(v);
q[v].push_back(u);
}
S[0]=1;
for(int i=2;i<=N;i+=2)S[i]=S[i-2]*1ll*(i-1)%mod;
dfs(1,-1);
for(int i=1;i<=N;i++)ans=(ans+dp[1][i]*1ll*S[i])%mod,ans%=mod;
cout<<ans;
return 0;
}
特别的如果用填表法,上下界比较复杂,用刷表法直接1-siz[u],1-siz[v]枚举即可,所以树形dp建议用刷表法
树上容斥,一般都要开一个中间数组
小结:1.覆盖问题,算有限个的。2.dp维度多考虑将容斥系数融入

浙公网安备 33010602011771号