题意
给定一个 \(2n\) 个点的完全图,并给出这些点的一个生成树,问图中没有任何一个边在该生成树上的完美匹配个数。
思路
容斥
考虑容斥,全集为不带任何限制的完美匹配个数,令属性 \(P_i\) 表示树上第 \(i\) 个边在匹配里面,拥有属性 \(P_i\) 的元素构成集合 \(S_i\),那么答案为:
\[|U|-\left|\bigcup_{i}{S_i} \right|=|U|-\sum_{i}{|S_i|} +\sum_{i<j}{|S_i \cap S_j|} - \sum_{i<j<k}{|S_i \cap S_j \cap S_k|}+\cdots \](此处来源:oi-wiki_容斥原理)
考虑计算 \(n\) 个点的完全图的匹配个数:枚举第一个点配对的对象,然后剩下 \(n-2\) 个点变成子问题,则有递推式 \(f(n)=(n-1)\cdot f(n-2)\)。那么满足了 \(k\) 个属性的所有集合的元素个数之和,即为在树上任选 \(k\) 个不相邻的边的方案数乘以剩下 \(2n-2k\) 个点任意匹配的方案数。前者用树上背包实现,后者即 \(f(2n-2k)\)。
树上背包
令 \(dp(u,k,0)\) 表示子树选了 \(k\) 条边,\((u,fa)\) 不被选中的方案数,对应地,在 \(dp(u,k,1)\) 中则有 \((u, fa)\) 被选中。
那么 \(dp(u,k,1)\) 只能取子树的 \(0\) 状态,而 \(dp(u,k,0)\) 转移过程至多取一个子树的 \(1\) 状态,剩下全取 \(0\) 状态(也可以全取 \(0\) 状态)。
由该规则可知在枚举子树进行转移过程中,\(dp(u, *, 1)\) 只能从自己的背包里面加上当前子树的 \(0\) 状态进行转移,而 \(dp(u,*,0)\) 可以从自己的背包里面加上子树的 \(0\) 状态转移,也可以从 \(dp(u,*,1)\) 的背包里面加上子树的 \(1\) 状态转移。
还有一个细节,本来 \(1\) 状态中,\((u, fa)\) 会贡献一个选中的边。但由于转移的时候 \(0\) 的背包要用到 \(1\) 的背包,则转移完了之后再把 \(1\) 的背包右移。
代码
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a), __ ## i = (b); i <= __ ## i; ++i)
#define per(i, b, a) for(int i = (b), __ ## i = (a); i >= __ ## i; --i)
#define vi vector<int>
#define ll long long
#define pb emplace_back
using namespace std;
inline void rd(int &x){scanf("%d", &x);}
const int maxn = 4321;
const ll mod = 998244353;
vi ed[maxn];
int n;
ll dp[maxn][maxn][2];
int sz[maxn];
void dfs(int u, int fa)
{
dp[u][0][0] = dp[u][0][1] = 1;
for(auto v : ed[u]) if(v != fa)
{
dfs(v, u);
per(i, sz[u] + sz[v], 0)
{
rep(j, max(1, i - sz[u]), min(i, sz[v])) // 注意下界不能为0,否则会产生重复计算
{
dp[u][i][0] = (dp[u][i][0] + dp[u][i - j][0] * dp[v][j][0] + dp[u][i - j][1] * dp[v][j][1]) % mod;
dp[u][i][1] = (dp[u][i][1] + dp[u][i - j][1] * dp[v][j][0]) % mod;
}
}
sz[u] += sz[v];
}
per(i, ++sz[u], 1) dp[u][i][1] = dp[u][i - 1][1];
dp[u][0][1] = 0;
}
ll f[maxn], ans;
int main()
{
rd(n);
int u, v; rep(i, 1, 2 * n - 1) rd(u), rd(v), ed[u].pb(v), ed[v].pb(u);
dfs(1, 1);
f[0] = 1; rep(i, 1, n) f[i * 2] = (i * 2 - 1) * f[i * 2 - 2] % mod;
rep(i, 0, n)
ans = (ans + f[2 * n - 2 * i] * dp[1][i][0] * (i % 2 ? -1 : 1) % mod) % mod;
ans = (ans + mod) % mod;
cout << ans << endl;
}